From 6ee8a2980c2a7c9a8c65453b1d2c551717069ce5 Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Wed, 17 Nov 2010 15:13:09 -0500 Subject: [PATCH 1/7] Fix RESPONSE_TIMEOUT dependency loops. --- sleekxmpp/stanza/iq.py | 6 ++++-- sleekxmpp/xmlstream/handler/waiter.py | 7 +++++-- sleekxmpp/xmlstream/xmlstream.py | 15 +++++++++++---- 3 files changed, 20 insertions(+), 8 deletions(-) diff --git a/sleekxmpp/stanza/iq.py b/sleekxmpp/stanza/iq.py index 614d14f..150baa0 100644 --- a/sleekxmpp/stanza/iq.py +++ b/sleekxmpp/stanza/iq.py @@ -8,7 +8,7 @@ from sleekxmpp.stanza import Error from sleekxmpp.stanza.rootstanza import RootStanza -from sleekxmpp.xmlstream import RESPONSE_TIMEOUT, StanzaBase, ET +from sleekxmpp.xmlstream import StanzaBase, ET from sleekxmpp.xmlstream.handler import Waiter from sleekxmpp.xmlstream.matcher import MatcherId @@ -157,7 +157,7 @@ class Iq(RootStanza): StanzaBase.reply(self) return self - def send(self, block=True, timeout=RESPONSE_TIMEOUT): + def send(self, block=True, timeout=None): """ Send an stanza over the XML stream. @@ -174,6 +174,8 @@ class Iq(RootStanza): before exiting the send call if blocking is used. Defaults to sleekxmpp.xmlstream.RESPONSE_TIMEOUT """ + if timeout is None: + timeout = self.stream.response_timeout if block and self['type'] in ('get', 'set'): waitfor = Waiter('IqWait_%s' % self['id'], MatcherId(self['id'])) self.stream.registerHandler(waitfor) diff --git a/sleekxmpp/xmlstream/handler/waiter.py b/sleekxmpp/xmlstream/handler/waiter.py index a4bc354..341c01f 100644 --- a/sleekxmpp/xmlstream/handler/waiter.py +++ b/sleekxmpp/xmlstream/handler/waiter.py @@ -12,7 +12,7 @@ try: except ImportError: import Queue as queue -from sleekxmpp.xmlstream import StanzaBase, RESPONSE_TIMEOUT +from sleekxmpp.xmlstream import StanzaBase from sleekxmpp.xmlstream.handler.base import BaseHandler @@ -69,7 +69,7 @@ class Waiter(BaseHandler): """ pass - def wait(self, timeout=RESPONSE_TIMEOUT): + def wait(self, timeout=None): """ Block an event handler while waiting for a stanza to arrive. @@ -84,6 +84,9 @@ class Waiter(BaseHandler): arrive. Defaults to the global default timeout value sleekxmpp.xmlstream.RESPONSE_TIMEOUT. """ + if timeout is None: + timeout = self.stream.response_timeout + try: stanza = self._payload.get(True, timeout) except queue.Empty: diff --git a/sleekxmpp/xmlstream/xmlstream.py b/sleekxmpp/xmlstream/xmlstream.py index 30b76ce..9ae31a2 100644 --- a/sleekxmpp/xmlstream/xmlstream.py +++ b/sleekxmpp/xmlstream/xmlstream.py @@ -25,6 +25,8 @@ except ImportError: from sleekxmpp.thirdparty.statemachine import StateMachine from sleekxmpp.xmlstream import Scheduler, tostring from sleekxmpp.xmlstream.stanzabase import StanzaBase, ET +from sleekxmpp.xmlstream.handler import Waiter, XMLCallback +from sleekxmpp.xmlstream.matcher import MatchXMLMask # In Python 2.x, file socket objects are broken. A patched socket # wrapper is provided for this case in filesocket.py. @@ -162,6 +164,8 @@ class XMLStream(object): self.ssl_support = SSL_SUPPORT self.ssl_version = ssl.PROTOCOL_TLSv1 + self.response_timeout = RESPONSE_TIMEOUT + self.state = StateMachine(('disconnected', 'connected')) self.state._set_state('disconnected') @@ -458,8 +462,6 @@ class XMLStream(object): """ # To prevent circular dependencies, we must load the matcher # and handler classes here. - from sleekxmpp.xmlstream.matcher import MatchXMLMask - from sleekxmpp.xmlstream.handler import XMLCallback if name is None: name = 'add_handler_%s' % self.getNewId() @@ -606,7 +608,7 @@ class XMLStream(object): """ return xml - def send(self, data, mask=None, timeout=RESPONSE_TIMEOUT): + def send(self, data, mask=None, timeout=None): """ A wrapper for send_raw for sending stanza objects. @@ -621,6 +623,9 @@ class XMLStream(object): timeout -- Time in seconds to wait for a response before continuing. Defaults to RESPONSE_TIMEOUT. """ + if timeout is None: + timeout = self.response_timeout + if hasattr(mask, 'xml'): mask = mask.xml data = str(data) @@ -643,7 +648,7 @@ class XMLStream(object): self.send_queue.put(data) return True - def send_xml(self, data, mask=None, timeout=RESPONSE_TIMEOUT): + def send_xml(self, data, mask=None, timeout=None): """ Send an XML object on the stream, and optionally wait for a response. @@ -657,6 +662,8 @@ class XMLStream(object): timeout -- Time in seconds to wait for a response before continuing. Defaults to RESPONSE_TIMEOUT. """ + if timeout is None: + timeout = self.response_timeout return self.send(tostring(data), mask, timeout) def process(self, threaded=True): From ea48bb5ac58aa186c18c42c83e21a6a636bd22a9 Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Wed, 17 Nov 2010 15:45:16 -0500 Subject: [PATCH 2/7] Fixed some live stream test errors. Added test demonstrating using multiple stream clients in a single test. --- sleekxmpp/test/livesocket.py | 21 +++++++++++-- sleekxmpp/test/sleektest.py | 33 +++++++++++++++----- tests/live_multiple_streams.py | 57 ++++++++++++++++++++++++++++++++++ tests/live_test.py | 1 - 4 files changed, 101 insertions(+), 11 deletions(-) create mode 100644 tests/live_multiple_streams.py diff --git a/sleekxmpp/test/livesocket.py b/sleekxmpp/test/livesocket.py index 5e8c547..3e0f213 100644 --- a/sleekxmpp/test/livesocket.py +++ b/sleekxmpp/test/livesocket.py @@ -7,6 +7,7 @@ """ import socket +import threading try: import queue except ImportError: @@ -40,6 +41,8 @@ class TestLiveSocket(object): self.recv_buffer = [] self.recv_queue = queue.Queue() self.send_queue = queue.Queue() + self.send_queue_lock = threading.Lock() + self.recv_queue_lock = threading.Lock() self.is_live = True def __getattr__(self, name): @@ -108,7 +111,8 @@ class TestLiveSocket(object): Placeholders. Same as for socket.recv. """ data = self.socket.recv(*args, **kwargs) - self.recv_queue.put(data) + with self.recv_queue_lock: + self.recv_queue.put(data) return data def send(self, data): @@ -120,7 +124,8 @@ class TestLiveSocket(object): Arguments: data -- String value to write. """ - self.send_queue.put(data) + with self.send_queue_lock: + self.send_queue.put(data) self.socket.send(data) # ------------------------------------------------------------------ @@ -143,3 +148,15 @@ class TestLiveSocket(object): Placeholders, same as socket.recv() """ return self.recv(*args, **kwargs) + + def clear(self): + """ + Empty the send queue, typically done once the session has started to + remove the feature negotiation and log in stanzas. + """ + with self.send_queue_lock: + for i in range(0, self.send_queue.qsize()): + self.send_queue.get(block=False) + with self.recv_queue_lock: + for i in range(0, self.recv_queue.qsize()): + self.recv_queue.get(block=False) diff --git a/sleekxmpp/test/sleektest.py b/sleekxmpp/test/sleektest.py index d7a6147..27a7556 100644 --- a/sleekxmpp/test/sleektest.py +++ b/sleekxmpp/test/sleektest.py @@ -7,6 +7,10 @@ """ import unittest +try: + import Queue as queue +except: + import queue import sleekxmpp from sleekxmpp import ClientXMPP, ComponentXMPP @@ -279,6 +283,10 @@ class SleekTest(unittest.TestCase): else: raise ValueError("Unknown XMPP connection mode.") + # We will use this to wait for the session_start event + # for live connections. + skip_queue = queue.Queue() + if socket == 'mock': self.xmpp.set_socket(TestSocket()) @@ -293,6 +301,10 @@ class SleekTest(unittest.TestCase): self.xmpp.socket.recv_data(header) elif socket == 'live': self.xmpp.socket_class = TestLiveSocket + def wait_for_session(x): + self.xmpp.socket.clear() + skip_queue.put('started') + self.xmpp.add_event_handler('session_start', wait_for_session) self.xmpp.connect() else: raise ValueError("Unknown socket type.") @@ -300,10 +312,13 @@ class SleekTest(unittest.TestCase): self.xmpp.register_plugins() self.xmpp.process(threaded=True) if skip: - # Clear startup stanzas - self.xmpp.socket.next_sent(timeout=1) - if mode == 'component': + if socket != 'live': + # Clear startup stanzas self.xmpp.socket.next_sent(timeout=1) + if mode == 'component': + self.xmpp.socket.next_sent(timeout=1) + else: + skip_queue.get(block=True, timeout=10) def make_header(self, sto='', sfrom='', @@ -573,11 +588,13 @@ class SleekTest(unittest.TestCase): Defaults to the value of self.match_method. """ sent = self.xmpp.socket.next_sent(timeout) - if isinstance(data, str): - xml = self.parse_xml(data) - self.fix_namespaces(xml, 'jabber:client') - data = self.xmpp._build_stanza(xml, 'jabber:client') - self.check(data, sent, + if sent is None: + return False + print sent + xml = self.parse_xml(sent) + self.fix_namespaces(xml, 'jabber:client') + sent = self.xmpp._build_stanza(xml, 'jabber:client') + self.check(sent, data, method=method, defaults=defaults, use_values=use_values) diff --git a/tests/live_multiple_streams.py b/tests/live_multiple_streams.py new file mode 100644 index 0000000..69ee74c --- /dev/null +++ b/tests/live_multiple_streams.py @@ -0,0 +1,57 @@ +import logging + +from sleekxmpp.test import * + + +class TestMultipleStreams(SleekTest): + """ + Test that we can test a live stanza stream. + """ + + def setUp(self): + self.client1 = SleekTest() + self.client2 = SleekTest() + + def tearDown(self): + self.client1.stream_close() + self.client2.stream_close() + + def testMultipleStreams(self): + """Test that we can interact with multiple live ClientXMPP instance.""" + + client1 = self.client1 + client2 = self.client2 + + client1.stream_start(mode='client', + socket='live', + skip=True, + jid='user@localhost/test1', + password='user') + client2.stream_start(mode='client', + socket='live', + skip=True, + jid='user@localhost/test2', + password='user') + + client1.xmpp.send_message(mto='user@localhost/test2', + mbody='test') + + client1.send('message@body=test', method='stanzapath') + client2.recv('message@body=test', method='stanzapath') + + +suite = unittest.TestLoader().loadTestsFromTestCase(TestMultipleStreams) + +if __name__ == '__main__': + logging.basicConfig(level=logging.DEBUG, + format='%(levelname)-8s %(message)s') + + tests = unittest.TestSuite([suite]) + result = unittest.TextTestRunner(verbosity=2).run(tests) + test_ns = 'http://andyet.net/protocol/tests' + print("" % ( + test_ns, + 'ran="%s"' % result.testsRun, + 'errors="%s"' % len(result.errors), + 'fails="%s"' % len(result.failures), + 'success="%s"' % result.wasSuccessful())) diff --git a/tests/live_test.py b/tests/live_test.py index 16b6f1c..b71930a 100644 --- a/tests/live_test.py +++ b/tests/live_test.py @@ -1,7 +1,6 @@ import logging from sleekxmpp.test import * -import sleekxmpp.plugins.xep_0033 as xep_0033 class TestLiveStream(SleekTest): From 7ba6d5e02daced34960a11506b4600ba096a9570 Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Wed, 17 Nov 2010 16:01:27 -0500 Subject: [PATCH 3/7] Fix Node set to None error. --- sleekxmpp/clientxmpp.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sleekxmpp/clientxmpp.py b/sleekxmpp/clientxmpp.py index 1c60081..32795e4 100644 --- a/sleekxmpp/clientxmpp.py +++ b/sleekxmpp/clientxmpp.py @@ -384,7 +384,7 @@ class ClientXMPP(BaseXMPP): self.set_jid(response.xml.find('{%s}bind/{%s}jid' % (bind_ns, bind_ns)).text) self.bound = True - log.info("Node set to: %s" % self.boundjid.fulljid) + log.info("Node set to: %s" % self.boundjid.full) session_ns = 'urn:ietf:params:xml:ns:xmpp-session' if "{%s}session" % session_ns not in self.features or self.bindfail: log.debug("Established Session") From e648f08badce77bce3842108340a4c257b619bc1 Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Wed, 17 Nov 2010 16:07:27 -0500 Subject: [PATCH 4/7] Fix stream test errors. --- sleekxmpp/test/sleektest.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/sleekxmpp/test/sleektest.py b/sleekxmpp/test/sleektest.py index 27a7556..5e61cec 100644 --- a/sleekxmpp/test/sleektest.py +++ b/sleekxmpp/test/sleektest.py @@ -197,7 +197,10 @@ class SleekTest(unittest.TestCase): "Stanza:\n%s" % str(stanza)) else: stanza_class = stanza.__class__ - xml = self.parse_xml(criteria) + if isinstance(criteria, str): + xml = self.parse_xml(criteria) + else: + xml = criteria.xml # Ensure that top level namespaces are used, even if they # were not provided. @@ -590,7 +593,6 @@ class SleekTest(unittest.TestCase): sent = self.xmpp.socket.next_sent(timeout) if sent is None: return False - print sent xml = self.parse_xml(sent) self.fix_namespaces(xml, 'jabber:client') sent = self.xmpp._build_stanza(xml, 'jabber:client') From cdbc0570cac6f672a63df2c0368cb045417d942e Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Wed, 17 Nov 2010 17:28:04 -0500 Subject: [PATCH 5/7] Added a basic example for using MUC. --- examples/muc.py | 186 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 186 insertions(+) create mode 100755 examples/muc.py diff --git a/examples/muc.py b/examples/muc.py new file mode 100755 index 0000000..8296cb6 --- /dev/null +++ b/examples/muc.py @@ -0,0 +1,186 @@ +#!/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 + +# 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 MUCBot(sleekxmpp.ClientXMPP): + + """ + A simple SleekXMPP bot that will greets those + who enter the room, and acknowledge any messages + that mentions the bot's nickname. + """ + + def __init__(self, jid, password, room, nick): + sleekxmpp.ClientXMPP.__init__(self, jid, password) + + self.room = room + self.nick = nick + + # 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 groupchat_message event is triggered whenever a message + # stanza is received from any chat room. If you also also + # register a handler for the 'message' event, MUC messages + # will be processed by both handlers. + self.add_event_handler("groupchat_message", self.muc_message) + + # The groupchat_presence event is triggered whenever a + # presence stanza is received from any chat room, including + # any presences you send yourself. To limit event handling + # to a single room, use the events muc::room@server::presence, + # muc::room@server::got_online, or muc::room@server::got_offline. + self.add_event_handler("muc::%s::got_online" % self.room, + self.muc_online) + + + 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() + self.plugin['xep_0045'].joinMUC(self.room, + self.nick, + # If a room password is needed, use: + # password=the_room_password, + wait=True) + + def muc_message(self, msg): + """ + Process incoming message stanzas from any chat room. Be aware + that if you also have any handlers for the 'message' event, + message stanzas may be processed by both handlers, so check + the 'type' attribute when using a 'message' event handler. + + Whenever the bot's nickname is mentioned, respond to + the message. + + IMPORTANT: Always check that a message is not from yourself, + otherwise you will create an infinite loop responding + to your own messages. + + This handler will reply to messages that mention + the bot's nickname. + + Arguments: + msg -- The received message stanza. See the documentation + for stanza objects and the Message stanza to see + how it may be used. + """ + if msg['mucnick'] != self.nick and self.nick in msg['body']: + self.send_message(mto=msg['from'].bare, + mbody="I heard that, %s." % msg['mucnick'], + mtype='groupchat') + + def muc_online(self, presence): + """ + Process a presence stanza from a chat room. In this case, + presences from users that have just come online are + handled by sending a welcome message that includes + the user's nickname and role in the room. + + Arguments: + presence -- The received presence stanza. See the + documentation for the Presence stanza + to see how else it may be used. + """ + if presence['muc']['nick'] != self.nick: + self.send_message(mto=presence['from'].bare, + mbody="Hello, %s %s" % (presence['muc']['role'], + presence['muc']['nick']), + mtype='groupchat') + + +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) + + # 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") + optp.add_option("-r", "--room", dest="room", + help="MUC room to join") + optp.add_option("-n", "--nick", dest="nick", + help="MUC nickname") + + opts, args = optp.parse_args() + + # Setup logging. + logging.basicConfig(level=opts.loglevel, + format='%(levelname)-8s %(message)s') + + if None in [opts.jid, opts.password, opts.room, opts.nick]: + optp.print_help() + sys.exit(1) + + # Setup the MUCBot and register plugins. Note that while plugins may + # have interdependencies, the order in which you register them does + # not matter. + xmpp = MUCBot(opts.jid, opts.password, opts.room, opts.nick) + xmpp.register_plugin('xep_0030') # Service Discovery + xmpp.register_plugin('xep_0045') # Multi-User Chat + xmpp.register_plugin('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") + else: + print("Unable to connect.") From afeb8f3f7c1edade3c33003ad776758c77c8706c Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Wed, 17 Nov 2010 17:30:53 -0500 Subject: [PATCH 6/7] Made echo client print help message. If the jid and password are not supplied, the options list will be displayed instead of hanging trying to connect to a nonexistant server. --- examples/echo_client.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/examples/echo_client.py b/examples/echo_client.py index 99967d5..f449ce4 100755 --- a/examples/echo_client.py +++ b/examples/echo_client.py @@ -105,6 +105,10 @@ if __name__ == '__main__': logging.basicConfig(level=opts.loglevel, format='%(levelname)-8s %(message)s') + if None in [opts.jid, opts.password]: + optp.print_help() + sys.exit(1) + # Setup the EchoBot and register plugins. Note that while plugins may # have interdependencies, the order in which you register them does # not matter. From 60d3afe6b6814bb5d30c4d4d355451d3c15364ca Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Thu, 18 Nov 2010 00:03:39 -0500 Subject: [PATCH 7/7] Added __repr__ for JIDs. --- sleekxmpp/xmlstream/jid.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/sleekxmpp/xmlstream/jid.py b/sleekxmpp/xmlstream/jid.py index 33d845a..d8f45b9 100644 --- a/sleekxmpp/xmlstream/jid.py +++ b/sleekxmpp/xmlstream/jid.py @@ -121,3 +121,6 @@ class JID(object): def __str__(self): """Use the full JID as the string value.""" return self.full + + def __repr__(self): + return str(self)