From 37ff17b0cbefd8a0056b131621728123b292e211 Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Thu, 26 Aug 2010 18:27:18 -0400 Subject: [PATCH 01/17] Added unit test for _fix_ns for handling namespaces with forward slashes. --- tests/test_elementbase.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tests/test_elementbase.py b/tests/test_elementbase.py index 6b0c076..8de7a16 100644 --- a/tests/test_elementbase.py +++ b/tests/test_elementbase.py @@ -3,6 +3,21 @@ from sleekxmpp.xmlstream.stanzabase import ElementBase class TestElementBase(SleekTest): + def testFixNs(self): + """Test fixing namespaces in an XPath expression.""" + + e = ElementBase() + ns = "http://jabber.org/protocol/disco#items" + result = e._fix_ns("{%s}foo/bar/{abc}baz/{%s}more" % (ns, ns)) + + expected = "/".join(["{%s}foo" % ns, + "{%s}bar" % ns, + "{abc}baz", + "{%s}more" % ns]) + self.failUnless(expected == result, + "Incorrect namespace fixing result: %s" % str(result)) + + def testExtendedName(self): """Test element names of the form tag1/tag2/tag3.""" From ca6ce26b0dd4975d3f5e0b5209a6a66a7dadbed5 Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Thu, 26 Aug 2010 18:40:58 -0400 Subject: [PATCH 02/17] Added comments to _fix_ns to clarify the cleaning procedure. --- sleekxmpp/xmlstream/stanzabase.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/sleekxmpp/xmlstream/stanzabase.py b/sleekxmpp/xmlstream/stanzabase.py index 8814df7..9785da0 100644 --- a/sleekxmpp/xmlstream/stanzabase.py +++ b/sleekxmpp/xmlstream/stanzabase.py @@ -765,17 +765,24 @@ class ElementBase(object): False, which returns a flat string path. """ fixed = [] + # Split the XPath into a series of blocks, where a block + # is started by an element with a namespace. ns_blocks = xpath.split('{') for ns_block in ns_blocks: if '}' in ns_block: + # Apply the found namespace to following elements + # that do not have namespaces. namespace = ns_block.split('}')[0] elements = ns_block.split('}')[1].split('/') else: + # Apply the stanza's namespace to the following + # elements since no namespace was provided. namespace = self.namespace elements = ns_block.split('/') for element in elements: if element: + # Skip empty entry artifacts from splitting. fixed.append('{%s}%s' % (namespace, element)) if split: From a2c515bc978cbabb1e6ac4398a8f0a11bbc9d9f8 Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Fri, 27 Aug 2010 11:07:20 -0400 Subject: [PATCH 03/17] Updated StanzaBase with documentation. --- sleekxmpp/xmlstream/stanzabase.py | 139 +++++++++++++++++++++++++++--- 1 file changed, 126 insertions(+), 13 deletions(-) diff --git a/sleekxmpp/xmlstream/stanzabase.py b/sleekxmpp/xmlstream/stanzabase.py index 9785da0..51966a4 100644 --- a/sleekxmpp/xmlstream/stanzabase.py +++ b/sleekxmpp/xmlstream/stanzabase.py @@ -893,6 +893,48 @@ class ElementBase(object): class StanzaBase(ElementBase): + + """ + StanzaBase provides the foundation for all other stanza objects used by + SleekXMPP, and defines a basic set of interfaces common to nearly + all stanzas. These interfaces are the 'id', 'type', 'to', and 'from' + attributes. An additional interface, 'payload', is available to access + the XML contents of the stanza. Most stanza objects will provided more + specific interfaces, however. + + Stanza Interface: + from -- A JID object representing the sender's JID. + id -- An optional id value that can be used to associate stanzas + with their replies. + payload -- The XML contents of the stanza. + to -- A JID object representing the recipient's JID. + type -- The type of stanza, typically will be 'normal', 'error', + 'get', or 'set', etc. + + Attributes: + stream -- The XMLStream instance that will handle sending this stanza. + tag -- The namespaced version of the stanza's name. + + Methods: + setType -- Set the type of the stanza. + getTo -- Return the stanza recipients JID. + setTo -- Set the stanza recipient's JID. + getFrom -- Return the stanza sender's JID. + setFrom -- Set the stanza sender's JID. + getPayload -- Return the stanza's XML contents. + setPayload -- Append to the stanza's XML contents. + delPayload -- Remove the stanza's XML contents. + clear -- Reset the stanza's XML contents. + reply -- Reset the stanza and modify the 'to' and 'from' + attributes to prepare for sending a reply. + error -- Set the stanza's type to 'error'. + unhandled -- Callback for when the stanza is not handled by a + stream handler. + exception -- Callback for if an exception is raised while + handling the stanza. + send -- Send the stanza using the stanza's stream. + """ + name = 'stanza' namespace = 'jabber:client' interfaces = set(('type', 'to', 'from', 'id', 'payload')) @@ -901,6 +943,17 @@ class StanzaBase(ElementBase): def __init__(self, stream=None, xml=None, stype=None, sto=None, sfrom=None, sid=None): + """ + Create a new stanza. + + Arguments: + stream -- Optional XMLStream responsible for sending this stanza. + xml -- Optional XML contents to initialize stanza values. + stype -- Optional stanza type value. + sto -- Optional string or JID object of the recipient's JID. + sfrom -- Optional string or JID object of the sender's JID. + sid -- Optional ID value for the stanza. + """ self.stream = stream if stream is not None: self.namespace = stream.default_ns @@ -914,22 +967,69 @@ class StanzaBase(ElementBase): self.tag = "{%s}%s" % (self.namespace, self.name) def setType(self, value): + """ + Set the stanza's 'type' attribute. + + Only type values contained in StanzaBase.types are accepted. + + Arguments: + value -- One of the values contained in StanzaBase.types + """ if value in self.types: self.xml.attrib['type'] = value return self + def getTo(self): + """Return the value of the stanza's 'to' attribute.""" + return JID(self._getAttr('to')) + + def setTo(self, value): + """ + Set the 'to' attribute of the stanza. + + Arguments: + value -- A string or JID object representing the recipient's JID. + """ + return self._setAttr('to', str(value)) + + def getFrom(self): + """Return the value of the stanza's 'from' attribute.""" + return JID(self._getAttr('from')) + + def setFrom(self, value): + """ + Set the 'from' attribute of the stanza. + + Arguments: + from -- A string or JID object representing the sender's JID. + """ + return self._setAttr('from', str(value)) + def getPayload(self): + """Return a list of XML objects contained in the stanza.""" return self.xml.getchildren() def setPayload(self, value): + """ + Add XML content to the stanza. + + Arguments: + value -- An XML object, or a stanza object. + """ self.xml.append(value) return self def delPayload(self): + """Remove the XML contents of the stanza.""" self.clear() return self def clear(self): + """ + Remove all XML element contents and plugins. + + Any attribute values will be preserved. + """ for child in self.xml.getchildren(): self.xml.remove(child) for plugin in list(self.plugins.keys()): @@ -937,6 +1037,12 @@ class StanzaBase(ElementBase): return self def reply(self): + """ + Reset the stanza and swap its 'from' and 'to' attributes to prepare + for sending a reply stanza. + + For client streams, the 'from' attribute is removed. + """ # if it's a component, use from if self.stream and hasattr(self.stream, "is_component") and \ self.stream.is_component: @@ -948,35 +1054,42 @@ class StanzaBase(ElementBase): return self def error(self): + """Set the stanza's type to 'error'.""" 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): + """ + Called when no handlers have been registered to process this + stanza. + + Meant to be overridden. + """ pass def exception(self, e): + """ + Handle exceptions raised during stanza processing. + + Meant to be overridden. + """ logging.exception('Error handling {%s}%s stanza' % (self.namespace, self.name)) def send(self): + """Queue the stanza to be sent on the XML stream.""" self.stream.sendRaw(self.__str__()) def __copy__(self): - return self.__class__(xml=copy.deepcopy(self.xml), stream=self.stream) + """ + Return a copy of the stanza object that does not share the + same underlying XML object, but does share the same XML stream. + """ + return self.__class__(xml=copy.deepcopy(self.xml), + stream=self.stream) def __str__(self): + """Serialize the stanza's XML to a string.""" return tostring(self.xml, xmlns='', stanza_ns=self.namespace, stream=self.stream) From 6677df39f2ed3a949fa31df847bb08122190a26d Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Fri, 27 Aug 2010 11:29:48 -0400 Subject: [PATCH 04/17] Updated xmlstream.filesocket. --- sleekxmpp/xmlstream/filesocket.py | 35 ++++++++++++++++++++++--------- sleekxmpp/xmlstream/xmlstream.py | 3 +-- 2 files changed, 26 insertions(+), 12 deletions(-) diff --git a/sleekxmpp/xmlstream/filesocket.py b/sleekxmpp/xmlstream/filesocket.py index 07b395d..b0c1556 100644 --- a/sleekxmpp/xmlstream/filesocket.py +++ b/sleekxmpp/xmlstream/filesocket.py @@ -5,21 +5,36 @@ See the file LICENSE for copying permission. """ + from socket import _fileobject import socket -class filesocket(_fileobject): +class FileSocket(_fileobject): - def read(self, size=4096): - data = self._sock.recv(size) - if data is not None: - return data + """ + Create a file object wrapper for a socket to work around + issues present in Python 2.6 when using sockets as file objects. + + The parser for xml.etree.cElementTree requires a file, but we will + be reading from the XMPP connection socket instead. + """ + + def read(self, size=4096): + """Read data from the socket as if it were a file.""" + data = self._sock.recv(size) + if data is not None: + return data class Socket26(socket._socketobject): - def makefile(self, mode='r', bufsize=-1): - """makefile([mode[, bufsize]]) -> file object - Return a regular file object corresponding to the socket. The mode - and bufsize arguments are as for the built-in open() function.""" - return filesocket(self._sock, mode, bufsize) + """ + A custom socket implementation that uses our own FileSocket class + to work around issues in Python 2.6 when using sockets as files. + """ + + def makefile(self, mode='r', bufsize=-1): + """makefile([mode[, bufsize]]) -> file object + Return a regular file object corresponding to the socket. The mode + and bufsize arguments are as for the built-in open() function.""" + return FileSocket(self._sock, mode, bufsize) diff --git a/sleekxmpp/xmlstream/xmlstream.py b/sleekxmpp/xmlstream/xmlstream.py index bf39bb3..415567e 100644 --- a/sleekxmpp/xmlstream/xmlstream.py +++ b/sleekxmpp/xmlstream/xmlstream.py @@ -139,8 +139,7 @@ class XMLStream(object): self.socket = ssl.wrap_socket(self.socket, ssl_version=ssl.PROTOCOL_TLSv1, do_handshake_on_connect=False) self.socket.do_handshake() if sys.version_info < (3,0): - from . filesocket import filesocket - self.filesocket = filesocket(self.socket) + self.filesocket = filesocket.FileSocket(self.socket) else: self.filesocket = self.socket.makefile('rb', 0) return True From bb6f4af8e24f940a837c227b0f2fab2b64e4dc7e Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Fri, 27 Aug 2010 12:22:35 -0400 Subject: [PATCH 05/17] Added unit tests for StanzaBase. --- sleekxmpp/xmlstream/stanzabase.py | 8 +++- tests/test_stanzabase.py | 79 +++++++++++++++++++++++++++++++ 2 files changed, 85 insertions(+), 2 deletions(-) create mode 100644 tests/test_stanzabase.py diff --git a/sleekxmpp/xmlstream/stanzabase.py b/sleekxmpp/xmlstream/stanzabase.py index 51966a4..d7c7c7b 100644 --- a/sleekxmpp/xmlstream/stanzabase.py +++ b/sleekxmpp/xmlstream/stanzabase.py @@ -1014,9 +1014,13 @@ class StanzaBase(ElementBase): Add XML content to the stanza. Arguments: - value -- An XML object, or a stanza object. + value -- Either an XML or a stanza object, or a list + of XML or stanza objects. """ - self.xml.append(value) + if not isinstance(value, list): + value = [value] + for val in value: + self.append(val) return self def delPayload(self): diff --git a/tests/test_stanzabase.py b/tests/test_stanzabase.py new file mode 100644 index 0000000..682068d --- /dev/null +++ b/tests/test_stanzabase.py @@ -0,0 +1,79 @@ +from . sleektest import * +import sleekxmpp +from sleekxmpp.xmlstream.stanzabase import ET, StanzaBase + +class TestStanzaBase(SleekTest): + + def testTo(self): + """Test the 'to' interface of StanzaBase.""" + stanza = StanzaBase() + stanza['to'] = 'user@example.com' + self.failUnless(str(stanza['to']) == 'user@example.com', + "Setting and retrieving stanza 'to' attribute did not work.") + + def testFrom(self): + """Test the 'from' interface of StanzaBase.""" + stanza = StanzaBase() + stanza['from'] = 'user@example.com' + self.failUnless(str(stanza['from']) == 'user@example.com', + "Setting and retrieving stanza 'from' attribute did not work.") + + def testPayload(self): + """Test the 'payload' interface of StanzaBase.""" + stanza = StanzaBase() + self.failUnless(stanza['payload'] == [], + "Empty stanza does not have an empty payload.") + + stanza['payload'] = ET.Element("{foo}foo") + self.failUnless(len(stanza['payload']) == 1, + "Stanza contents and payload do not match.") + + stanza['payload'] = ET.Element('{bar}bar') + self.failUnless(len(stanza['payload']) == 2, + "Stanza payload was not appended.") + + del stanza['payload'] + self.failUnless(stanza['payload'] == [], + "Stanza payload not cleared after deletion.") + + stanza['payload'] = [ET.Element('{foo}foo'), + ET.Element('{bar}bar')] + self.failUnless(len(stanza['payload']) == 2, + "Adding multiple elements to stanza's payload did not work.") + + def testClear(self): + """Test clearing a stanza.""" + stanza = StanzaBase() + stanza['to'] = 'user@example.com' + stanza['payload'] = ET.Element("{foo}foo") + stanza.clear() + + self.failUnless(stanza['payload'] == [], + "Stanza payload was not cleared after calling .clear()") + self.failUnless(str(stanza['to']) == "user@example.com", + "Stanza attributes were not preserved after calling .clear()") + + def testReply(self): + """Test creating a reply stanza.""" + stanza = StanzaBase() + stanza['to'] = "recipient@example.com" + stanza['from'] = "sender@example.com" + stanza['payload'] = ET.Element("{foo}foo") + + stanza.reply() + + self.failUnless(str(stanza['to'] == "sender@example.com"), + "Stanza reply did not change 'to' attribute.") + self.failUnless(stanza['payload'] == [], + "Stanza reply did not empty stanza payload.") + + def testError(self): + """Test marking a stanza as an error.""" + stanza = StanzaBase() + stanza['type'] = 'get' + stanza.error() + self.failUnless(stanza['type'] == 'error', + "Stanza type is not 'error' after calling error()") + + +suite = unittest.TestLoader().loadTestsFromTestCase(TestStanzaBase) From 906aa0bd6896d119bcbabc6e21de31c2171316b9 Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Fri, 27 Aug 2010 15:48:48 -0400 Subject: [PATCH 06/17] Fixed SleekTest compare method to check XML text. Corrected resulting test failures. All pass again. --- tests/sleektest.py | 13 ++++++++++++- tests/test_elementbase.py | 4 ++-- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/tests/sleektest.py b/tests/sleektest.py index 801253d..66535bc 100644 --- a/tests/sleektest.py +++ b/tests/sleektest.py @@ -504,7 +504,18 @@ class SleekTest(unittest.TestCase): if xml.attrib != other.attrib: return False - # Step 3: Recursively check children + # Step 3: Check text + if xml.text is None: + xml.text = "" + if other.text is None: + other.text = "" + xml.text = xml.text.strip() + other.text = other.text.strip() + + if xml.text != other.text: + return False + + # Step 4: Recursively check children for child in xml: child2s = other.findall("%s" % child.tag) if child2s is None: diff --git a/tests/test_elementbase.py b/tests/test_elementbase.py index 8de7a16..b6d7c6f 100644 --- a/tests/test_elementbase.py +++ b/tests/test_elementbase.py @@ -347,7 +347,7 @@ class TestElementBase(SleekTest): """) - stanza._setSubText('bar', text='', keep=True) + stanza._setSubText('wrapper/bar', text='', keep=True) self.checkStanza(TestStanza, stanza, """ @@ -358,7 +358,7 @@ class TestElementBase(SleekTest): """, use_values=False) stanza['bar'] = 'a' - stanza._setSubText('bar', text='') + stanza._setSubText('wrapper/bar', text='') self.checkStanza(TestStanza, stanza, """ From 89fb15e8962640a34e24418216e156188032bfa8 Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Fri, 27 Aug 2010 16:42:26 -0400 Subject: [PATCH 07/17] Updated the suite of handler classes with documentation. Updated XMLStream to return True or False from removeHandler to indicate if the handler existed and was removed. Waiter handlers now unregister themselves after timing out. --- sleekxmpp/__init__.py | 36 +++---- sleekxmpp/basexmpp.py | 2 +- sleekxmpp/xmlstream/handler/base.py | 91 ++++++++++++++--- sleekxmpp/xmlstream/handler/callback.py | 100 +++++++++++++----- sleekxmpp/xmlstream/handler/waiter.py | 106 ++++++++++++++----- sleekxmpp/xmlstream/handler/xmlcallback.py | 32 +++++- sleekxmpp/xmlstream/handler/xmlwaiter.py | 28 +++++- sleekxmpp/xmlstream/xmlstream.py | 10 +- tests/test_handlers.py | 112 +++++++++++++++++++++ 9 files changed, 419 insertions(+), 98 deletions(-) create mode 100644 tests/test_handlers.py diff --git a/sleekxmpp/__init__.py b/sleekxmpp/__init__.py index d2f5765..afb7d9d 100644 --- a/sleekxmpp/__init__.py +++ b/sleekxmpp/__init__.py @@ -37,7 +37,7 @@ except ImportError: #class PresenceStanzaType(object): -# +# # def fromXML(self, xml): # self.ptype = xml.get('type') @@ -69,24 +69,24 @@ class ClientXMPP(basexmpp, XMLStream): 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('Stream Features', MatchXPath('{http://etherx.jabber.org/streams}features'), self._handleStreamFeatures)) + self.registerHandler(Callback('Roster Update', MatchXPath('{%s}iq/{jabber:iq:roster}query' % self.default_ns), self._handleRoster)) #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) @@ -104,7 +104,7 @@ class ClientXMPP(basexmpp, XMLStream): 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 + # 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 @@ -128,18 +128,18 @@ class ClientXMPP(basexmpp, XMLStream): 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)) @@ -157,12 +157,12 @@ class ClientXMPP(basexmpp, XMLStream): 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: @@ -173,7 +173,7 @@ class ClientXMPP(basexmpp, XMLStream): #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) @@ -187,7 +187,7 @@ class ClientXMPP(basexmpp, XMLStream): 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 @@ -209,7 +209,7 @@ class ClientXMPP(basexmpp, XMLStream): #if 'sasl:DIGEST-MD5' in self.features: # self._auth_digestmd5() return True - + def handler_auth_success(self, xml): self.authenticated = True self.features = [] @@ -219,7 +219,7 @@ class ClientXMPP(basexmpp, XMLStream): 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() @@ -238,7 +238,7 @@ class ClientXMPP(basexmpp, XMLStream): 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) @@ -249,7 +249,7 @@ class ClientXMPP(basexmpp, XMLStream): 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']: diff --git a/sleekxmpp/basexmpp.py b/sleekxmpp/basexmpp.py index b7b605b..f83fc06 100644 --- a/sleekxmpp/basexmpp.py +++ b/sleekxmpp/basexmpp.py @@ -123,7 +123,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, once=disposable, instream=instream)) def getId(self): return "%x".upper() % self.id diff --git a/sleekxmpp/xmlstream/handler/base.py b/sleekxmpp/xmlstream/handler/base.py index 720846d..3ae82a8 100644 --- a/sleekxmpp/xmlstream/handler/base.py +++ b/sleekxmpp/xmlstream/handler/base.py @@ -6,23 +6,82 @@ See the file LICENSE for copying permission. """ + class BaseHandler(object): + """ + Base class for stream handlers. Stream handlers are matched with + incoming stanzas so that the stanza may be processed in some way. + Stanzas may be matched with multiple handlers. - def __init__(self, name, matcher): - self.name = name - self._destroy = False - self._payload = None - self._matcher = matcher - - def match(self, xml): - return self._matcher.match(xml) - - def prerun(self, payload): - self._payload = payload + Handler execution may take place in two phases. The first is during + the stream processing itself. The second is after stream processing + and during SleekXMPP's main event loop. The prerun method is used + for execution during stream processing, and the run method is used + during the main event loop. - def run(self, payload): - self._payload = payload - - def checkDelete(self): - return self._destroy + Attributes: + name -- The name of the handler. + stream -- The stream this handler is assigned to. + + Methods: + match -- Compare a stanza with the handler's matcher. + prerun -- Handler execution during stream processing. + run -- Handler execution during the main event loop. + checkDelete -- Indicate if the handler may be removed from use. + """ + + def __init__(self, name, matcher, stream=None): + """ + Create a new stream handler. + + Arguments: + name -- The name of the handler. + matcher -- A matcher object from xmlstream.matcher that will be + used to determine if a stanza should be accepted by + this handler. + stream -- The XMLStream instance the handler should monitor. + """ + self.name = name + self.stream = stream + self._destroy = False + self._payload = None + self._matcher = matcher + if stream is not None: + stream.registerHandler(self) + + def match(self, xml): + """ + Compare a stanza or XML object with the handler's matcher. + + Arguments + xml -- An XML or stanza object. + """ + return self._matcher.match(xml) + + def prerun(self, payload): + """ + Prepare the handler for execution while the XML stream is being + processed. + + Arguments: + payload -- A stanza object. + """ + self._payload = payload + + def run(self, payload): + """ + Execute the handler after XML stream processing and during the + main event loop. + + Arguments: + payload -- A stanza object. + """ + self._payload = payload + + def checkDelete(self): + """ + Check if the handler should be removed from the list of stream + handlers. + """ + return self._destroy diff --git a/sleekxmpp/xmlstream/handler/callback.py b/sleekxmpp/xmlstream/handler/callback.py index 889b0aa..04a4eed 100644 --- a/sleekxmpp/xmlstream/handler/callback.py +++ b/sleekxmpp/xmlstream/handler/callback.py @@ -5,30 +5,80 @@ See the file LICENSE for copying permission. """ -from . import base -import logging -class Callback(base.BaseHandler): - - def __init__(self, name, matcher, pointer, thread=False, once=False, instream=False): - base.BaseHandler.__init__(self, name, matcher) - self._pointer = pointer - self._thread = thread - self._once = once - self._instream = instream +from sleekxmpp.xmlstream.handler.base import BaseHandler - def prerun(self, payload): - base.BaseHandler.prerun(self, payload) - if self._instream: - self.run(payload, True) - - def run(self, payload, instream=False): - if not self._instream or instream: - base.BaseHandler.run(self, payload) - #if self._thread: - # x = threading.Thread(name="Callback_%s" % self.name, target=self._pointer, args=(payload,)) - # x.start() - #else: - self._pointer(payload) - if self._once: - self._destroy = True + +class Callback(BaseHandler): + + """ + The Callback handler will execute a callback function with + matched stanzas. + + The handler may execute the callback either during stream + processing or during the main event loop. + + Callback functions are all executed in the same thread, so be + aware if you are executing functions that will block for extended + periods of time. Typically, you should signal your own events using the + SleekXMPP object's event() method to pass the stanza off to a threaded + event handler for further processing. + + Methods: + prerun -- Overrides BaseHandler.prerun + run -- Overrides BaseHandler.run + """ + + def __init__(self, name, matcher, pointer, thread=False, + once=False, instream=False, stream=None): + """ + Create a new callback handler. + + Arguments: + name -- The name of the handler. + matcher -- A matcher object for matching stanza objects. + pointer -- The function to execute during callback. + threaded -- DEPRECATED. Remains only for backwards compatibility. + once -- Indicates if the handler should be used only + once. Defaults to False. + instream -- Indicates if the callback should be executed + during stream processing instead of in the + main event loop. + stream -- The XMLStream instance this handler should monitor. + """ + BaseHandler.__init__(self, name, matcher, stream) + self._pointer = pointer + self._once = once + self._instream = instream + + def prerun(self, payload): + """ + Execute the callback during stream processing, if + the callback was created with instream=True. + + Overrides BaseHandler.prerun + + Arguments: + payload -- The matched stanza object. + """ + BaseHandler.prerun(self, payload) + if self._instream: + self.run(payload, True) + + def run(self, payload, instream=False): + """ + Execute the callback function with the matched stanza payload. + + Overrides BaseHandler.run + + Arguments: + payload -- The matched stanza object. + instream -- Force the handler to execute during + stream processing. Used only by prerun. + Defaults to False. + """ + if not self._instream or instream: + BaseHandler.run(self, payload) + self._pointer(payload) + if self._once: + self._destroy = True diff --git a/sleekxmpp/xmlstream/handler/waiter.py b/sleekxmpp/xmlstream/handler/waiter.py index 7c4330a..0e5206b 100644 --- a/sleekxmpp/xmlstream/handler/waiter.py +++ b/sleekxmpp/xmlstream/handler/waiter.py @@ -5,32 +5,86 @@ See the file LICENSE for copying permission. """ -from . import base -try: - import queue -except ImportError: - import Queue as queue + import logging -from .. stanzabase import StanzaBase +try: + import queue +except ImportError: + import Queue as queue -class Waiter(base.BaseHandler): - - def __init__(self, name, matcher): - base.BaseHandler.__init__(self, name, matcher) - self._payload = queue.Queue() - - def prerun(self, payload): - self._payload.put(payload) - - def run(self, payload): - pass +from sleekxmpp.xmlstream import StanzaBase, RESPONSE_TIMEOUT +from sleekxmpp.xmlstream.handler.base import BaseHandler - def wait(self, timeout=60): - try: - return self._payload.get(True, timeout) - except queue.Empty: - logging.warning("Timed out waiting for %s" % self.name) - return False - - def checkDelete(self): - return True + +class Waiter(BaseHandler): + + """ + The Waiter handler allows an event handler to block + until a particular stanza has been received. The handler + will either be given the matched stanza, or False if the + waiter has timed out. + + Methods: + checkDelete -- Overrides BaseHandler.checkDelete + prerun -- Overrides BaseHandler.prerun + run -- Overrides BaseHandler.run + wait -- Wait for a stanza to arrive and return it to + an event handler. + """ + + def __init__(self, name, matcher, stream=None): + BaseHandler.__init__(self, name, matcher) + self._payload = queue.Queue() + + def prerun(self, payload): + """ + Store the matched stanza. + + Overrides BaseHandler.prerun + + Arguments: + payload -- The matched stanza object. + """ + self._payload.put(payload) + + def run(self, payload): + """ + Do not process this handler during the main event loop. + + Overrides BaseHandler.run + + Arguments: + payload -- The matched stanza object. + """ + pass + + def wait(self, timeout=RESPONSE_TIMEOUT): + """ + Block an event handler while waiting for a stanza to arrive. + + Be aware that this will impact performance if called from a + non-threaded event handler. + + Will return either the received stanza, or False if the waiter + timed out. + + Arguments: + timeout -- The number of seconds to wait for the stanza to + arrive. Defaults to the global default timeout + value sleekxmpp.xmlstream.RESPONSE_TIMEOUT. + """ + try: + stanza = self._payload.get(True, timeout) + except queue.Empty: + stanza = False + logging.warning("Timed out waiting for %s" % self.name) + self.stream.removeHandler(self.name) + return stanza + + def checkDelete(self): + """ + Always remove waiters after use. + + Overrides BaseHandler.checkDelete + """ + return True diff --git a/sleekxmpp/xmlstream/handler/xmlcallback.py b/sleekxmpp/xmlstream/handler/xmlcallback.py index 67879df..11607ff 100644 --- a/sleekxmpp/xmlstream/handler/xmlcallback.py +++ b/sleekxmpp/xmlstream/handler/xmlcallback.py @@ -5,10 +5,32 @@ See the file LICENSE for copying permission. """ -import threading -from . callback import Callback + +from sleekxmpp.xmlstream.handler import Callback + class XMLCallback(Callback): - - def run(self, payload, instream=False): - Callback.run(self, payload.xml, instream) + + """ + The XMLCallback class is identical to the normal Callback class, + except that XML contents of matched stanzas will be processed instead + of the stanza objects themselves. + + Methods: + run -- Overrides Callback.run + """ + + def run(self, payload, instream=False): + """ + Execute the callback function with the matched stanza's + XML contents, instead of the stanza itself. + + Overrides BaseHandler.run + + Arguments: + payload -- The matched stanza object. + instream -- Force the handler to execute during + stream processing. Used only by prerun. + Defaults to False. + """ + Callback.run(self, payload.xml, instream) diff --git a/sleekxmpp/xmlstream/handler/xmlwaiter.py b/sleekxmpp/xmlstream/handler/xmlwaiter.py index cf90751..5201caf 100644 --- a/sleekxmpp/xmlstream/handler/xmlwaiter.py +++ b/sleekxmpp/xmlstream/handler/xmlwaiter.py @@ -5,9 +5,29 @@ See the file LICENSE for copying permission. """ -from . waiter import Waiter + +from sleekxmpp.xmlstream.handler import Waiter + class XMLWaiter(Waiter): - - def prerun(self, payload): - Waiter.prerun(self, payload.xml) + + """ + The XMLWaiter class is identical to the normal Waiter class + except that it returns the XML contents of the stanza instead + of the full stanza object itself. + + Methods: + prerun -- Overrides Waiter.prerun + """ + + def prerun(self, payload): + """ + Store the XML contents of the stanza to return to the + waiting event handler. + + Overrides Waiter.prerun + + Arguments: + payload -- The matched stanza object. + """ + Waiter.prerun(self, payload.xml) diff --git a/sleekxmpp/xmlstream/xmlstream.py b/sleekxmpp/xmlstream/xmlstream.py index 415567e..28aee2b 100644 --- a/sleekxmpp/xmlstream/xmlstream.py +++ b/sleekxmpp/xmlstream/xmlstream.py @@ -357,8 +357,10 @@ class XMLStream(object): return False def registerHandler(self, handler, before=None, after=None): - "Add handler with matcher class and parameters." - self.__handlers.append(handler) + "Add handler with matcher class and parameters." + if handler.stream is None: + self.__handlers.append(handler) + handler.stream = self def removeHandler(self, name): "Removes the handler." @@ -366,8 +368,10 @@ class XMLStream(object): for handler in self.__handlers: if handler.name == name: self.__handlers.pop(idx) - return + return True idx += 1 + return False + def registerStanza(self, stanza_class): "Adds stanza. If root stanzas build stanzas sent in events while non-root stanzas build substanza objects." diff --git a/tests/test_handlers.py b/tests/test_handlers.py new file mode 100644 index 0000000..c6262c6 --- /dev/null +++ b/tests/test_handlers.py @@ -0,0 +1,112 @@ +from . sleektest import * +import sleekxmpp +from sleekxmpp.xmlstream.handler import * +from sleekxmpp.xmlstream.matcher import * + +class TestHandlers(SleekTest): + """ + Test that we can simulate and test a stanza stream. + """ + + def setUp(self): + self.streamStart() + + def tearDown(self): + self.streamClose() + + def testCallback(self): + """Test using stream callback handlers.""" + + def callback_handler(stanza): + self.xmpp.sendRaw(""" + + Success! + + """) + + callback = Callback('Test Callback', + MatchXPath('{test}tester'), + callback_handler) + + self.xmpp.registerHandler(callback) + + self.streamRecv("""""") + + msg = self.Message() + msg['body'] = 'Success!' + self.streamSendMessage(msg) + + def testWaiter(self): + """Test using stream waiter handler.""" + + def waiter_handler(stanza): + iq = self.xmpp.Iq() + iq['id'] = 'test' + iq['type'] = 'set' + iq['query'] = 'test' + reply = iq.send(block=True) + if reply: + self.xmpp.sendRaw(""" + + Successful: %s + + """ % reply['query']) + + self.xmpp.add_event_handler('message', waiter_handler, threaded=True) + + # Send message to trigger waiter_handler + self.streamRecv(""" + + Testing + + """) + + # Check that Iq was sent by waiter_handler + iq = self.Iq() + iq['id'] = 'test' + iq['type'] = 'set' + iq['query'] = 'test' + self.streamSendIq(iq) + + # Send the reply Iq + self.streamRecv(""" + + + + """) + + # Check that waiter_handler received the reply + msg = self.Message() + msg['body'] = 'Successful: test' + self.streamSendMessage(msg) + + def testWaiterTimeout(self): + """Test that waiter handler is removed after timeout.""" + + def waiter_handler(stanza): + iq = self.xmpp.Iq() + iq['id'] = 'test2' + iq['type'] = 'set' + iq['query'] = 'test2' + reply = iq.send(block=True, timeout=0) + + self.xmpp.add_event_handler('message', waiter_handler, threaded=True) + + # Start test by triggerig waiter_handler + self.streamRecv("""Start Test""") + + # Check that Iq was sent to trigger start of timeout period + iq = self.Iq() + iq['id'] = 'test2' + iq['type'] = 'set' + iq['query'] = 'test2' + self.streamSendIq(iq) + + # Check that the waiter is no longer registered + waiter_exists = self.xmpp.removeHandler('IqWait_test2') + + self.failUnless(waiter_exists == False, + "Waiter handler was not removed.") + + +suite = unittest.TestLoader().loadTestsFromTestCase(TestHandlers) From f5ae27da4f542670560a4ecc0373d7001366b496 Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Fri, 27 Aug 2010 18:16:09 -0400 Subject: [PATCH 08/17] Fix some documentation typos. --- sleekxmpp/xmlstream/handler/callback.py | 2 +- sleekxmpp/xmlstream/handler/waiter.py | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/sleekxmpp/xmlstream/handler/callback.py b/sleekxmpp/xmlstream/handler/callback.py index 04a4eed..f0a7285 100644 --- a/sleekxmpp/xmlstream/handler/callback.py +++ b/sleekxmpp/xmlstream/handler/callback.py @@ -38,7 +38,7 @@ class Callback(BaseHandler): name -- The name of the handler. matcher -- A matcher object for matching stanza objects. pointer -- The function to execute during callback. - threaded -- DEPRECATED. Remains only for backwards compatibility. + thread -- DEPRECATED. Remains only for backwards compatibility. once -- Indicates if the handler should be used only once. Defaults to False. instream -- Indicates if the callback should be executed diff --git a/sleekxmpp/xmlstream/handler/waiter.py b/sleekxmpp/xmlstream/handler/waiter.py index 0e5206b..12827d3 100644 --- a/sleekxmpp/xmlstream/handler/waiter.py +++ b/sleekxmpp/xmlstream/handler/waiter.py @@ -33,6 +33,14 @@ class Waiter(BaseHandler): """ def __init__(self, name, matcher, stream=None): + """ + Create a new Waiter. + + Arguments: + name -- The name of the waiter. + matcher -- A matcher object to detect the desired stanza. + stream -- Optional XMLStream instance to monitor. + """ BaseHandler.__init__(self, name, matcher) self._payload = queue.Queue() From 9c62bce2060e30e41c3710587f6bd9992625b245 Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Mon, 30 Aug 2010 14:55:30 -0400 Subject: [PATCH 09/17] Updated ElementBase.match to respect namespaces with slashes. Required adding option to _fix_ns to not propagate namespaces to child elements. --- sleekxmpp/xmlstream/stanzabase.py | 30 +++++++++++++++++++++--------- tests/test_elementbase.py | 4 ++-- 2 files changed, 23 insertions(+), 11 deletions(-) diff --git a/sleekxmpp/xmlstream/stanzabase.py b/sleekxmpp/xmlstream/stanzabase.py index d7c7c7b..8f4874c 100644 --- a/sleekxmpp/xmlstream/stanzabase.py +++ b/sleekxmpp/xmlstream/stanzabase.py @@ -586,7 +586,8 @@ class ElementBase(object): string or a list of element names with attribute checks. """ if isinstance(xpath, str): - xpath = xpath.split('/') + xpath = self._fix_ns(xpath, split=True, propagate_ns=False) + # Extract the tag name and attribute checks for the first XPath node. components = xpath[0].split('@') @@ -754,15 +755,20 @@ class ElementBase(object): """ return self - def _fix_ns(self, xpath, split=False): + def _fix_ns(self, xpath, split=False, propagate_ns=True): """ Apply the stanza's namespace to elements in an XPath expression. Arguments: - xpath -- The XPath expression to fix with namespaces. - split -- Indicates if the fixed XPath should be left as a - list of element names with namespaces. Defaults to - False, which returns a flat string path. + xpath -- The XPath expression to fix with namespaces. + split -- Indicates if the fixed XPath should be left as a + list of element names with namespaces. Defaults to + False, which returns a flat string path. + propagate_ns -- Overrides propagating parent element namespaces + to child elements. Useful if you wish to simply + split an XPath that has non-specified namespaces, + and child and parent namespaces are known not to + always match. Defaults to True. """ fixed = [] # Split the XPath into a series of blocks, where a block @@ -774,17 +780,23 @@ class ElementBase(object): # that do not have namespaces. namespace = ns_block.split('}')[0] elements = ns_block.split('}')[1].split('/') - else: + elif use_ns: # Apply the stanza's namespace to the following # elements since no namespace was provided. namespace = self.namespace elements = ns_block.split('/') + else: + # We don't want to propagate namespaces. + elements = ns_block.split('/') for element in elements: if element: # Skip empty entry artifacts from splitting. - fixed.append('{%s}%s' % (namespace, - element)) + if use_ns: + tag = '{%s}%s' % (namespace, element) + else: + tag = element + fixed.append(tag) if split: return fixed return '/'.join(fixed) diff --git a/tests/test_elementbase.py b/tests/test_elementbase.py index b6d7c6f..8898b3f 100644 --- a/tests/test_elementbase.py +++ b/tests/test_elementbase.py @@ -459,7 +459,7 @@ class TestElementBase(SleekTest): class TestStanzaPlugin(ElementBase): name = "plugin" - namespace = "bar" + namespace = "http://test/slash/bar" interfaces = set(('attrib',)) registerStanzaPlugin(TestStanza, TestStanzaPlugin) @@ -483,7 +483,7 @@ class TestElementBase(SleekTest): self.failUnless(stanza.match("foo/plugin@attrib=c"), "Stanza did not match with plugin and attribute.") - self.failUnless(stanza.match("foo/{bar}plugin"), + self.failUnless(stanza.match("foo/{http://test/slash/bar}plugin"), "Stanza did not match with namespaced plugin.") substanza = TestSubStanza() From 998741b87e7babc6e0af9bcf79f10f4422ba96f1 Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Mon, 30 Aug 2010 15:25:59 -0400 Subject: [PATCH 10/17] Fixed typos in ElementBase._fix_ns --- 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 8f4874c..7eb3f97 100644 --- a/sleekxmpp/xmlstream/stanzabase.py +++ b/sleekxmpp/xmlstream/stanzabase.py @@ -780,7 +780,7 @@ class ElementBase(object): # that do not have namespaces. namespace = ns_block.split('}')[0] elements = ns_block.split('}')[1].split('/') - elif use_ns: + elif propagate_ns: # Apply the stanza's namespace to the following # elements since no namespace was provided. namespace = self.namespace @@ -792,7 +792,7 @@ class ElementBase(object): for element in elements: if element: # Skip empty entry artifacts from splitting. - if use_ns: + if propagate_ns: tag = '{%s}%s' % (namespace, element) else: tag = element From 3749c1b88c4774b85fcee1e26e8bce8dbeef23c7 Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Mon, 30 Aug 2010 17:12:10 -0400 Subject: [PATCH 11/17] Fixed ElementBase.match to match using sub_interface elements. --- sleekxmpp/xmlstream/stanzabase.py | 6 ++++++ tests/test_elementbase.py | 20 +++++++++++++++++++- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/sleekxmpp/xmlstream/stanzabase.py b/sleekxmpp/xmlstream/stanzabase.py index 7eb3f97..86b528d 100644 --- a/sleekxmpp/xmlstream/stanzabase.py +++ b/sleekxmpp/xmlstream/stanzabase.py @@ -614,6 +614,12 @@ class ElementBase(object): if self[name] != value: return False + # Check sub interfaces. + if len(xpath) > 1: + next_tag = xpath[1] + if next_tag in self.sub_interfaces and self[next_tag]: + return True + # Attempt to continue matching the XPath using the stanza's plugins. if not matched_substanzas and len(xpath) > 1: # Convert {namespace}tag@attribs to just tag diff --git a/tests/test_elementbase.py b/tests/test_elementbase.py index 8898b3f..dfd37b5 100644 --- a/tests/test_elementbase.py +++ b/tests/test_elementbase.py @@ -454,9 +454,16 @@ class TestElementBase(SleekTest): class TestStanza(ElementBase): name = "foo" namespace = "foo" - interfaces = set(('bar','baz')) + interfaces = set(('bar','baz', 'qux')) + sub_interfaces = set(('qux',)) subitem = (TestSubStanza,) + def setQux(self, value): + self._setSubText('qux', text=value) + + def getQux(self): + return self._getSubText('qux') + class TestStanzaPlugin(ElementBase): name = "plugin" namespace = "http://test/slash/bar" @@ -479,6 +486,17 @@ class TestElementBase(SleekTest): self.failUnless(stanza.match("foo@bar=a@baz=b"), "Stanza did not match its own name with multiple attributes.") + stanza['qux'] = 'c' + self.failUnless(stanza.match("foo/qux"), + "Stanza did not match with subelements.") + + stanza['qux'] = '' + self.failUnless(stanza.match("foo/qux") == False, + "Stanza matched missing subinterface element.") + + self.failUnless(stanza.match("foo/bar") == False, + "Stanza matched nonexistent element.") + stanza['plugin']['attrib'] = 'c' self.failUnless(stanza.match("foo/plugin@attrib=c"), "Stanza did not match with plugin and attribute.") From aebd115ba2a8dfab7b345dfc121ec73b5dc7a5a1 Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Wed, 1 Sep 2010 14:20:34 -0400 Subject: [PATCH 12/17] A few cleanups to make things simpler. --- sleekxmpp/xmlstream/handler/waiter.py | 2 +- sleekxmpp/xmlstream/stanzabase.py | 9 +++------ 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/sleekxmpp/xmlstream/handler/waiter.py b/sleekxmpp/xmlstream/handler/waiter.py index 12827d3..1e101ed 100644 --- a/sleekxmpp/xmlstream/handler/waiter.py +++ b/sleekxmpp/xmlstream/handler/waiter.py @@ -41,7 +41,7 @@ class Waiter(BaseHandler): matcher -- A matcher object to detect the desired stanza. stream -- Optional XMLStream instance to monitor. """ - BaseHandler.__init__(self, name, matcher) + BaseHandler.__init__(self, name, matcher, stream=stream) self._payload = queue.Queue() def prerun(self, payload): diff --git a/sleekxmpp/xmlstream/stanzabase.py b/sleekxmpp/xmlstream/stanzabase.py index 86b528d..f824200 100644 --- a/sleekxmpp/xmlstream/stanzabase.py +++ b/sleekxmpp/xmlstream/stanzabase.py @@ -594,8 +594,8 @@ class ElementBase(object): tag = components[0] attributes = components[1:] - if tag not in (self.name, "{%s}%s" % (self.namespace, self.name), - self.plugins, self.plugin_attrib): + if tag not in (self.name, "{%s}%s" % (self.namespace, self.name)) and \ + tag not in self.plugins and tag not in self.plugin_attrib: # The requested tag is not in this stanza, so no match. return False @@ -786,14 +786,11 @@ class ElementBase(object): # that do not have namespaces. namespace = ns_block.split('}')[0] elements = ns_block.split('}')[1].split('/') - elif propagate_ns: + else: # Apply the stanza's namespace to the following # elements since no namespace was provided. namespace = self.namespace elements = ns_block.split('/') - else: - # We don't want to propagate namespaces. - elements = ns_block.split('/') for element in elements: if element: From 576eefb09740959a1d4307010e42880acdc1954a Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Wed, 1 Sep 2010 14:25:30 -0400 Subject: [PATCH 13/17] Fixed line spacing in filesocket.py to please pep8. --- sleekxmpp/xmlstream/filesocket.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sleekxmpp/xmlstream/filesocket.py b/sleekxmpp/xmlstream/filesocket.py index b0c1556..441ff87 100644 --- a/sleekxmpp/xmlstream/filesocket.py +++ b/sleekxmpp/xmlstream/filesocket.py @@ -9,6 +9,7 @@ from socket import _fileobject import socket + class FileSocket(_fileobject): """ @@ -25,6 +26,7 @@ class FileSocket(_fileobject): if data is not None: return data + class Socket26(socket._socketobject): """ @@ -37,4 +39,3 @@ class Socket26(socket._socketobject): Return a regular file object corresponding to the socket. The mode and bufsize arguments are as for the built-in open() function.""" return FileSocket(self._sock, mode, bufsize) - From 5c3066ba305c25bd1d9d8e262f3a119ed434bd72 Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Wed, 1 Sep 2010 14:28:43 -0400 Subject: [PATCH 14/17] Updated all of the matcher classes in sleekxmpp.xmlstream.matcher. Matchers are now PEP8 compliant and have documentation. --- sleekxmpp/xmlstream/matcher/base.py | 30 +++- sleekxmpp/xmlstream/matcher/id.py | 29 +++- sleekxmpp/xmlstream/matcher/many.py | 39 ++++- sleekxmpp/xmlstream/matcher/stanzapath.py | 34 +++- sleekxmpp/xmlstream/matcher/xmlmask.py | 198 ++++++++++++++++------ sleekxmpp/xmlstream/matcher/xpath.py | 93 +++++++--- 6 files changed, 321 insertions(+), 102 deletions(-) diff --git a/sleekxmpp/xmlstream/matcher/base.py b/sleekxmpp/xmlstream/matcher/base.py index 51da094..701ab32 100644 --- a/sleekxmpp/xmlstream/matcher/base.py +++ b/sleekxmpp/xmlstream/matcher/base.py @@ -5,10 +5,30 @@ See the file LICENSE for copying permission. """ + + class MatcherBase(object): - def __init__(self, criteria): - self._criteria = criteria - - def match(self, xml): - return False + """ + Base class for stanza matchers. Stanza matchers are used to pick + stanzas out of the XML stream and pass them to the appropriate + stream handlers. + """ + + def __init__(self, criteria): + """ + Create a new stanza matcher. + + Arguments: + criteria -- Object to compare some aspect of a stanza + against. + """ + self._criteria = criteria + + def match(self, xml): + """ + Check if a stanza matches the stored criteria. + + Meant to be overridden. + """ + return False diff --git a/sleekxmpp/xmlstream/matcher/id.py b/sleekxmpp/xmlstream/matcher/id.py index 43972c2..0c8ce2d 100644 --- a/sleekxmpp/xmlstream/matcher/id.py +++ b/sleekxmpp/xmlstream/matcher/id.py @@ -5,9 +5,28 @@ See the file LICENSE for copying permission. """ -from . import base -class MatcherId(base.MatcherBase): - - def match(self, xml): - return xml['id'] == self._criteria +from sleekxmpp.xmlstream.matcher.base import MatcherBase + + +class MatcherId(MatcherBase): + + """ + The ID matcher selects stanzas that have the same stanza 'id' + interface value as the desired ID. + + Methods: + match -- Overrides MatcherBase.match. + """ + + def match(self, xml): + """ + Compare the given stanza's 'id' attribute to the stored + id value. + + Overrides MatcherBase.match. + + Arguments: + xml -- The stanza to compare against. + """ + return xml['id'] == self._criteria diff --git a/sleekxmpp/xmlstream/matcher/many.py b/sleekxmpp/xmlstream/matcher/many.py index ff0c4e4..f470ec9 100644 --- a/sleekxmpp/xmlstream/matcher/many.py +++ b/sleekxmpp/xmlstream/matcher/many.py @@ -5,13 +5,36 @@ See the file LICENSE for copying permission. """ -from . import base -from xml.etree import cElementTree -class MatchMany(base.MatcherBase): +from sleekxmpp.xmlstream.matcher.base import MatcherBase - def match(self, xml): - for m in self._criteria: - if m.match(xml): - return True - return False + +class MatchMany(MatcherBase): + + """ + The MatchMany matcher may compare a stanza against multiple + criteria. It is essentially an OR relation combining multiple + matchers. + + Each of the criteria must implement a match() method. + + Methods: + match -- Overrides MatcherBase.match. + """ + + def match(self, xml): + """ + Match a stanza against multiple criteria. The match is successful + if one of the criteria matches. + + Each of the criteria must implement a match() method. + + Overrides MatcherBase.match. + + Arguments: + xml -- The stanza object to compare against. + """ + for m in self._criteria: + if m.match(xml): + return True + return False diff --git a/sleekxmpp/xmlstream/matcher/stanzapath.py b/sleekxmpp/xmlstream/matcher/stanzapath.py index e315445..f8ff283 100644 --- a/sleekxmpp/xmlstream/matcher/stanzapath.py +++ b/sleekxmpp/xmlstream/matcher/stanzapath.py @@ -5,10 +5,34 @@ See the file LICENSE for copying permission. """ -from . import base -from xml.etree import cElementTree -class StanzaPath(base.MatcherBase): +from sleekxmpp.xmlstream.matcher.base import MatcherBase - def match(self, stanza): - return stanza.match(self._criteria) + +class StanzaPath(MatcherBase): + + """ + The StanzaPath matcher selects stanzas that match a given "stanza path", + which is similar to a normal XPath except that it uses the interfaces and + plugins of the stanza instead of the actual, underlying XML. + + In most cases, the stanza path and XPath should be identical, but be + aware that differences may occur. + + Methods: + match -- Overrides MatcherBase.match. + """ + + def match(self, stanza): + """ + Compare a stanza against a "stanza path". A stanza path is similar to + an XPath expression, but uses the stanza's interfaces and plugins + instead of the underlying XML. For most cases, the stanza path and + XPath should be identical, but be aware that differences may occur. + + Overrides MatcherBase.match. + + Arguments: + stanza -- The stanza object to compare against. + """ + return stanza.match(self._criteria) diff --git a/sleekxmpp/xmlstream/matcher/xmlmask.py b/sleekxmpp/xmlstream/matcher/xmlmask.py index 89fd642..2967a2a 100644 --- a/sleekxmpp/xmlstream/matcher/xmlmask.py +++ b/sleekxmpp/xmlstream/matcher/xmlmask.py @@ -5,63 +5,151 @@ See the file LICENSE for copying permission. """ -from . import base -from xml.etree import cElementTree + from xml.parsers.expat import ExpatError -ignore_ns = False +from sleekxmpp.xmlstream.stanzabase import ET +from sleekxmpp.xmlstream.matcher.base import MatcherBase -class MatchXMLMask(base.MatcherBase): - def __init__(self, criteria): - base.MatcherBase.__init__(self, criteria) - if type(criteria) == type(''): - self._criteria = cElementTree.fromstring(self._criteria) - self.default_ns = 'jabber:client' - - def setDefaultNS(self, ns): - self.default_ns = ns +# Flag indicating if the builtin XPath matcher should be used, which +# uses namespaces, or a custom matcher that ignores namespaces. +# Changing this will affect ALL XMLMask matchers. +IGNORE_NS = False - def match(self, xml): - if hasattr(xml, 'xml'): - xml = xml.xml - return self.maskcmp(xml, self._criteria, True) - - def maskcmp(self, source, maskobj, use_ns=False, default_ns='__no_ns__'): - """maskcmp(xmlobj, maskobj): - Compare etree xml object to etree xml object mask""" - use_ns = not ignore_ns - #TODO require namespaces - if source == None: #if element not found (happens during recursive check below) - return False - if not hasattr(maskobj, 'attrib'): #if the mask is a string, make it an xml obj - try: - maskobj = cElementTree.fromstring(maskobj) - except ExpatError: - logging.log(logging.WARNING, "Expat error: %s\nIn parsing: %s" % ('', maskobj)) - if not use_ns and source.tag.split('}', 1)[-1] != maskobj.tag.split('}', 1)[-1]: # strip off ns and compare - return False - if use_ns and (source.tag != maskobj.tag and "{%s}%s" % (self.default_ns, maskobj.tag) != source.tag ): - return False - if maskobj.text and source.text != maskobj.text: - return False - for attr_name in maskobj.attrib: #compare attributes - if source.attrib.get(attr_name, "__None__") != maskobj.attrib[attr_name]: - return False - #for subelement in maskobj.getiterator()[1:]: #recursively compare subelements - for subelement in maskobj: #recursively compare subelements - if use_ns: - if not self.maskcmp(source.find(subelement.tag), subelement, use_ns): - return False - else: - if not self.maskcmp(self.getChildIgnoreNS(source, subelement.tag), subelement, use_ns): - return False - return True - - def getChildIgnoreNS(self, xml, tag): - tag = tag.split('}')[-1] - try: - idx = [c.tag.split('}')[-1] for c in xml.getchildren()].index(tag) - except ValueError: - return None - return xml.getchildren()[idx] + +class MatchXMLMask(MatcherBase): + + """ + The XMLMask matcher selects stanzas whose XML matches a given + XML pattern, or mask. For example, message stanzas with body elements + could be matched using the mask: + + + + Use of XMLMask is discouraged, and XPath or StanzaPath should be used + instead. + + The use of namespaces in the mask comparison is controlled by + IGNORE_NS. Setting IGNORE_NS to True will disable namespace based matching + for ALL XMLMask matchers. + + Methods: + match -- Overrides MatcherBase.match. + setDefaultNS -- Set the default namespace for the mask. + """ + + def __init__(self, criteria): + """ + Create a new XMLMask matcher. + + Arguments: + criteria -- Either an XML object or XML string to use as a mask. + """ + MatcherBase.__init__(self, criteria) + if isinstance(criteria, str): + self._criteria = ET.fromstring(self._criteria) + self.default_ns = 'jabber:client' + + def setDefaultNS(self, ns): + """ + Set the default namespace to use during comparisons. + + Arguments: + ns -- The new namespace to use as the default. + """ + self.default_ns = ns + + def match(self, xml): + """ + Compare a stanza object or XML object against the stored XML mask. + + Overrides MatcherBase.match. + + Arguments: + xml -- The stanza object or XML object to compare against. + """ + if hasattr(xml, 'xml'): + xml = xml.xml + return self._mask_cmp(xml, self._criteria, True) + + def _mask_cmp(self, source, mask, use_ns=False, default_ns='__no_ns__'): + """ + Compare an XML object against an XML mask. + + Arguments: + source -- The XML object to compare against the mask. + mask -- The XML object serving as the mask. + use_ns -- Indicates if namespaces should be respected during + the comparison. + default_ns -- The default namespace to apply to elements that + do not have a specified namespace. + Defaults to "__no_ns__". + """ + use_ns = not IGNORE_NS + + if source is None: + # If the element was not found. May happend during recursive calls. + return False + + # Convert the mask to an XML object if it is a string. + if not hasattr(mask, 'attrib'): + try: + mask = ET.fromstring(mask) + except ExpatError: + logging.log(logging.WARNING, + "Expat error: %s\nIn parsing: %s" % ('', mask)) + + if not use_ns: + # Compare the element without using namespaces. + source_tag = source.tag.split('}', 1)[-1] + mask_tag = mask.tag.split('}', 1)[-1] + if source_tag != mask_tag: + return False + else: + # Compare the element using namespaces + mask_ns_tag = "{%s}%s" % (self.default_ns, mask.tag) + if source.tag not in [mask.tag, mask_ns_tag]: + return False + + # If the mask includes text, compare it. + if mask.text and source.text != mask.text: + return False + + # Compare attributes. The stanza must include the attributes + # defined by the mask, but may include others. + for name, value in mask.attrib.items(): + if source.attrib.get(name, "__None__") != value: + return False + + # Recursively check subelements. + for subelement in mask: + if use_ns: + if not self._mask_cmp(source.find(subelement.tag), + subelement, use_ns): + return False + else: + if not self._mask_cmp(self._get_child(source, subelement.tag), + subelement, use_ns): + return False + + # Everything matches. + return True + + def _get_child(self, xml, tag): + """ + Return a child element given its tag, ignoring namespace values. + + Returns None if the child was not found. + + Arguments: + xml -- The XML object to search for the given child tag. + tag -- The name of the subelement to find. + """ + tag = tag.split('}')[-1] + try: + children = [c.tag.split('}')[-1] for c in xml.getchildren()] + index = children.index(tag) + except ValueError: + return None + return xml.getchildren()[index] diff --git a/sleekxmpp/xmlstream/matcher/xpath.py b/sleekxmpp/xmlstream/matcher/xpath.py index 7f3d20b..669c9f1 100644 --- a/sleekxmpp/xmlstream/matcher/xpath.py +++ b/sleekxmpp/xmlstream/matcher/xpath.py @@ -5,30 +5,75 @@ See the file LICENSE for copying permission. """ -from . import base -from xml.etree import cElementTree -ignore_ns = False +from sleekxmpp.xmlstream.stanzabase import ET +from sleekxmpp.xmlstream.matcher.base import MatcherBase -class MatchXPath(base.MatcherBase): - def match(self, xml): - if hasattr(xml, 'xml'): - xml = xml.xml - x = cElementTree.Element('x') - x.append(xml) - if not ignore_ns: - if x.find(self._criteria) is not None: - return True - return False - else: - criteria = [c.split('}')[-1] for c in self._criteria.split('/')] - xml = x - for tag in criteria: - children = [c.tag.split('}')[-1] for c in xml.getchildren()] - try: - idx = children.index(tag) - except ValueError: - return False - xml = xml.getchildren()[idx] - return True +# Flag indicating if the builtin XPath matcher should be used, which +# uses namespaces, or a custom matcher that ignores namespaces. +# Changing this will affect ALL XPath matchers. +IGNORE_NS = False + + +class MatchXPath(MatcherBase): + + """ + The XPath matcher selects stanzas whose XML contents matches a given + XPath expression. + + Note that using this matcher may not produce expected behavior when using + attribute selectors. For Python 2.6 and 3.1, the ElementTree find method + does not support the use of attribute selectors. If you need to support + Python 2.6 or 3.1, it might be more useful to use a StanzaPath matcher. + + If the value of IGNORE_NS is set to true, then XPath expressions will + be matched without using namespaces. + + Methods: + match -- Overrides MatcherBase.match. + """ + + def match(self, xml): + """ + Compare a stanza's XML contents to an XPath expression. + + If the value of IGNORE_NS is set to true, then XPath expressions + will be matched without using namespaces. + + Note that in Python 2.6 and 3.1 the ElementTree find method does + not support attribute selectors in the XPath expression. + + Arguments: + xml -- The stanza object to compare against. + """ + if hasattr(xml, 'xml'): + xml = xml.xml + x = ET.Element('x') + x.append(xml) + + if not IGNORE_NS: + # Use builtin, namespace respecting, XPath matcher. + if x.find(self._criteria) is not None: + return True + return False + else: + # Remove namespaces from the XPath expression. + criteria = [] + for ns_block in self._criteria.split('{'): + criteria.extend(ns_block.split('}')[-1].split('/')) + + # Walk the XPath expression. + xml = x + for tag in criteria: + if not tag: + # Skip empty tag name artifacts from the cleanup phase. + continue + + children = [c.tag.split('}')[-1] for c in xml.getchildren()] + try: + index = children.index(tag) + except ValueError: + return False + xml = xml.getchildren()[index] + return True From 9bef4b4d4d54768cb753e4bbfec049e97d063882 Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Wed, 1 Sep 2010 14:47:42 -0400 Subject: [PATCH 15/17] Move the examples to a top-level examples directory. --- .../component_example.py => examples/component.py | 0 example.py => examples/echo_client.py | 12 ++++++------ 2 files changed, 6 insertions(+), 6 deletions(-) rename sleekxmpp/component_example.py => examples/component.py (100%) rename example.py => examples/echo_client.py (93%) diff --git a/sleekxmpp/component_example.py b/examples/component.py similarity index 100% rename from sleekxmpp/component_example.py rename to examples/component.py diff --git a/example.py b/examples/echo_client.py similarity index 93% rename from example.py rename to examples/echo_client.py index 4eb88b3..5e6314f 100644 --- a/example.py +++ b/examples/echo_client.py @@ -14,12 +14,12 @@ if sys.version_info < (3,0): 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() @@ -36,18 +36,18 @@ if __name__ == '__main__': 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_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(): + if xmpp.connect(('talk.google.com', 5222)): + #khif xmpp.connect(): xmpp.process(threaded=False) print("done") else: From 0b4320a19610ab5c3f8d543a4a1ee7969c9e8db6 Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Wed, 1 Sep 2010 18:18:30 -0400 Subject: [PATCH 16/17] Updated the client and component examples. The component example now actually uses a config.xml file for its connection information, and to initialize a roster. --- INSTALL | 9 +- examples/component.py | 41 -------- examples/config.xml | 10 ++ examples/config_component.py | 190 +++++++++++++++++++++++++++++++++++ examples/echo_client.py | 123 ++++++++++++++++++----- 5 files changed, 304 insertions(+), 69 deletions(-) delete mode 100644 examples/component.py create mode 100644 examples/config.xml create mode 100755 examples/config_component.py mode change 100644 => 100755 examples/echo_client.py diff --git a/INSTALL b/INSTALL index f081a35..82f8712 100644 --- a/INSTALL +++ b/INSTALL @@ -1,11 +1,12 @@ Pre-requisites: -Python 3.1 or 2.6 +- Python 3.1 or 2.6 Install: -python3 setup.py install +> python3 setup.py install Root install: -sudo python3 setup.py install +> sudo python3 setup.py install To test: -python example.py -v -j [USER@example.com] -p [PASSWORD] +> cd examples +> python echo_client.py -v -j [USER@example.com] -p [PASSWORD] diff --git a/examples/component.py b/examples/component.py deleted file mode 100644 index f24216c..0000000 --- a/examples/component.py +++ /dev/null @@ -1,41 +0,0 @@ -import sleekxmpp.componentxmpp -import logging -from optparse import OptionParser -import time - -class Example(sleekxmpp.componentxmpp.ComponentXMPP): - - def __init__(self, jid, password): - sleekxmpp.componentxmpp.ComponentXMPP.__init__(self, jid, password, 'vm1', 5230) - self.add_event_handler("session_start", self.start) - self.add_event_handler("message", self.message) - - def start(self, event): - #self.getRoster() - #self.sendPresence(pto='admin@tigase.netflint.net/sarkozy') - #self.sendPresence(pto='tigase.netflint.net') - pass - - def message(self, event): - self.sendMessage("%s/%s" % (event['jid'], event['resource']), "Thanks for sending me, \"%s\"." % event['message'], mtype=event['type']) - -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('component.vm1', 'secreteating') - xmpp.registerPlugin('xep_0004') - xmpp.registerPlugin('xep_0030') - xmpp.registerPlugin('xep_0060') - xmpp.registerPlugin('xep_0199') - if xmpp.connect(): - xmpp.process(threaded=False) - print("done") - else: - print("Unable to connect.") diff --git a/examples/config.xml b/examples/config.xml new file mode 100644 index 0000000..4ca3a3d --- /dev/null +++ b/examples/config.xml @@ -0,0 +1,10 @@ + + component.localhost + ssshh + localhost + 8888 + + + + + diff --git a/examples/config_component.py b/examples/config_component.py new file mode 100755 index 0000000..1850b46 --- /dev/null +++ b/examples/config_component.py @@ -0,0 +1,190 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" + 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 logging +import time +from optparse import OptionParser + +import sleekxmpp +from sleekxmpp.componentxmpp import ComponentXMPP +from sleekxmpp.stanza.roster import Roster +from sleekxmpp.xmlstream import ElementBase +from sleekxmpp.xmlstream.stanzabase import ET, registerStanzaPlugin + +# Python versions before 3.0 do not use UTF-8 encoding +# by default. To ensure that Unicode is handled properly +# throughout SleekXMPP, we will set the default encoding +# ourselves to UTF-8. +if sys.version_info < (3,0): + reload(sys) + sys.setdefaultencoding('utf8') + + +class Config(ElementBase): + + """ + In order to make loading and manipulating an XML config + file easier, we will create a custom stanza object for + our config XML file contents. See the documentation + on stanza objects for more information on how to create + and use stanza objects and stanza plugins. + + We will reuse the IQ roster query stanza to store roster + information since it already exists. + + Example config XML: + + component.localhost + ssshh + localhost + 8888 + + + + + + """ + + name = "config" + namespace = "sleekxmpp:config" + interfaces = set(('jid', 'secret', 'server', 'port')) + sub_interfaces = interfaces + + +registerStanzaPlugin(Config, Roster) + + +class ConfigComponent(ComponentXMPP): + + """ + A simple SleekXMPP component that uses an external XML + file to store its configuration data. To make testing + that the component works, it will also echo messages sent + to it. + """ + + def __init__(self, config): + """ + Create a ConfigComponent. + + Arguments: + config -- The XML contents of the config file. + config_file -- The XML config file object itself. + """ + ComponentXMPP.__init__(self, config['jid'], + config['secret'], + config['server'], + config['port']) + + # Store the roster information. + self.roster = config['roster']['items'] + + # The session_start event will be triggered when + # the component establishes its connection with the + # server and the XML streams are ready for use. We + # want to listen for this event so that we we can + # broadcast any needed initial presence stanzas. + self.add_event_handler("session_start", self.start) + + # The message event is triggered whenever a message + # stanza is received. Be aware that that includes + # MUC messages and error messages. + self.add_event_handler("message", self.message) + + def start(self, event): + """ + Process the session_start event. + + The typical action for the session_start event in a component + is to broadcast presence stanzas to all subscribers to the + component. Note that the component does not have a roster + provided by the XMPP server. In this case, we have possibly + saved a roster in the component's configuration file. + + Since the component may use any number of JIDs, you should + also include the JID that is sending the presence. + + Arguments: + event -- An empty dictionary. The session_start + event does not provide any additional + data. + """ + for jid in self.roster: + if self.roster[jid]['subscription'] != 'none': + self.sendPresence(pfrom=self.jid, pto=jid) + + def message(self, msg): + """ + Process incoming message stanzas. Be aware that this also + includes MUC messages and error messages. It is usually + a good idea to check the messages's type before processing + or sending replies. + + Since a component may send messages from any number of JIDs, + it is best to always include a from JID. + + Arguments: + msg -- The received message stanza. See the documentation + for stanza objects and the Message stanza to see + how it may be used. + """ + # The reply method will use the messages 'to' JID as the + # outgoing reply's 'from' JID. + msg.reply("Thanks for sending\n%(body)s" % msg).send() + + +if __name__ == '__main__': + # Setup the command line arguments. + optp = OptionParser() + + # Output verbosity options. + 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) + + # Component name and secret options. + optp.add_option("-c", "--config", help="path to config file", + dest="config", default="config.xml") + + opts, args = optp.parse_args() + + # Setup logging. + logging.basicConfig(level=opts.loglevel, + format='%(levelname)-8s %(message)s') + + # Load configuration data. + config_file = open(opts.config, 'r+') + config_data = "\n".join([line for line in config_file]) + config = Config(xml=ET.fromstring(config_data)) + config_file.close() + + # Setup the ConfigComponent and register plugins. Note that while plugins may + # have interdependencies, the order in which you register them does + # not matter. + xmpp = ConfigComponent(config) + xmpp.registerPlugin('xep_0030') # Service Discovery + xmpp.registerPlugin('xep_0004') # Data Forms + xmpp.registerPlugin('xep_0060') # PubSub + xmpp.registerPlugin('xep_0199') # XMPP Ping + + # Connect to the XMPP server and start processing XMPP stanzas. + if xmpp.connect(): + xmpp.process(threaded=False) + print("Done") + else: + print("Unable to connect.") diff --git a/examples/echo_client.py b/examples/echo_client.py old mode 100644 new mode 100755 index 5e6314f..e6266ec --- a/examples/echo_client.py +++ b/examples/echo_client.py @@ -1,54 +1,129 @@ #!/usr/bin/env python -# coding=utf8 +# -*- coding: utf-8 -*- -import sleekxmpp -import logging -from optparse import OptionParser -import time +""" + 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 logging +import time +from optparse import OptionParser +import sleekxmpp + +# Python versions before 3.0 do not use UTF-8 encoding +# by default. To ensure that Unicode is handled properly +# throughout SleekXMPP, we will set the default encoding +# ourselves to UTF-8. if sys.version_info < (3,0): reload(sys) sys.setdefaultencoding('utf8') -class Example(sleekxmpp.ClientXMPP): +class EchoBot(sleekxmpp.ClientXMPP): + + """ + A simple SleekXMPP bot that will echo messages it + receives, along with a short thank you message. + """ def __init__(self, jid, password): sleekxmpp.ClientXMPP.__init__(self, jid, password) + + # The session_start event will be triggered when + # the bot establishes its connection with the server + # and the XML streams are ready for use. We want to + # listen for this event so that we we can intialize + # our roster. self.add_event_handler("session_start", self.start) + + # The message event is triggered whenever a message + # stanza is received. Be aware that that includes + # MUC messages and error messages. self.add_event_handler("message", self.message) def start(self, event): + """ + Process the session_start event. + + Typical actions for the session_start event are + requesting the roster and broadcasting an intial + presence stanza. + + Arguments: + event -- An empty dictionary. The session_start + event does not provide any additional + data. + """ self.getRoster() self.sendPresence() def message(self, msg): + """ + Process incoming message stanzas. Be aware that this also + includes MUC messages and error messages. It is usually + a good idea to check the messages's type before processing + or sending replies. + + Arguments: + msg -- The received message stanza. See the documentation + for stanza objects and the Message stanza to see + how it may be used. + """ msg.reply("Thanks for sending\n%(body)s" % msg).send() + if __name__ == '__main__': - #parse command line arguements + # Setup the command line arguments. 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') + # Output verbosity options. + 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) - # use this if you don't have pydns, and want to - # talk to GoogleTalk (e.g.) - if xmpp.connect(('talk.google.com', 5222)): - #khif xmpp.connect(): + # JID and password options. + 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() + + # Setup logging. + logging.basicConfig(level=opts.loglevel, + format='%(levelname)-8s %(message)s') + + # Setup the EchoBot and register plugins. Note that while plugins may + # have interdependencies, the order in which you register them does + # not matter. + xmpp = EchoBot(opts.jid, opts.password) + xmpp.registerPlugin('xep_0030') # Service Discovery + xmpp.registerPlugin('xep_0004') # Data Forms + xmpp.registerPlugin('xep_0060') # PubSub + xmpp.registerPlugin('xep_0199') # XMPP Ping + + # Connect to the XMPP server and start processing XMPP stanzas. + if xmpp.connect(): + # If you do not have the pydns library installed, you will need + # to manually specify the name of the server if it does not match + # the one in the JID. For example, to use Google Talk you would + # need to use: + # + # if xmpp.connect(('talk.google.com', 5222)): + # ... xmpp.process(threaded=False) - print("done") + print("Done") else: print("Unable to connect.") From 4a2e7c5393da945359edc2648a2ec124481acf7d Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Wed, 1 Sep 2010 18:21:09 -0400 Subject: [PATCH 17/17] Fixed linespacing and whitespace issues in examples to make them PEP8 compliant. --- examples/config_component.py | 12 ++++++------ examples/echo_client.py | 8 ++++---- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/examples/config_component.py b/examples/config_component.py index 1850b46..cbb8e62 100755 --- a/examples/config_component.py +++ b/examples/config_component.py @@ -24,7 +24,7 @@ from sleekxmpp.xmlstream.stanzabase import ET, registerStanzaPlugin # by default. To ensure that Unicode is handled properly # throughout SleekXMPP, we will set the default encoding # ourselves to UTF-8. -if sys.version_info < (3,0): +if sys.version_info < (3, 0): reload(sys) sys.setdefaultencoding('utf8') @@ -147,13 +147,13 @@ if __name__ == '__main__': optp = OptionParser() # Output verbosity options. - optp.add_option('-q','--quiet', help='set logging to ERROR', + 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', + 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', + optp.add_option('-v', '--verbose', help='set logging to COMM', action='store_const', dest='loglevel', const=5, default=logging.INFO) @@ -173,8 +173,8 @@ if __name__ == '__main__': config = Config(xml=ET.fromstring(config_data)) config_file.close() - # Setup the ConfigComponent and register plugins. Note that while plugins may - # have interdependencies, the order in which you register them does + # Setup the ConfigComponent and register plugins. Note that while plugins + # may have interdependencies, the order in which you register them does # not matter. xmpp = ConfigComponent(config) xmpp.registerPlugin('xep_0030') # Service Discovery diff --git a/examples/echo_client.py b/examples/echo_client.py index e6266ec..99967d5 100755 --- a/examples/echo_client.py +++ b/examples/echo_client.py @@ -20,7 +20,7 @@ import sleekxmpp # by default. To ensure that Unicode is handled properly # throughout SleekXMPP, we will set the default encoding # ourselves to UTF-8. -if sys.version_info < (3,0): +if sys.version_info < (3, 0): reload(sys) sys.setdefaultencoding('utf8') @@ -83,13 +83,13 @@ if __name__ == '__main__': optp = OptionParser() # Output verbosity options. - optp.add_option('-q','--quiet', help='set logging to ERROR', + 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', + 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', + optp.add_option('-v', '--verbose', help='set logging to COMM', action='store_const', dest='loglevel', const=5, default=logging.INFO)