summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rwxr-xr-xexamples/echo_client.py4
-rwxr-xr-xexamples/muc.py186
-rw-r--r--sleekxmpp/clientxmpp.py2
-rw-r--r--sleekxmpp/stanza/iq.py6
-rw-r--r--sleekxmpp/test/livesocket.py21
-rw-r--r--sleekxmpp/test/sleektest.py37
-rw-r--r--sleekxmpp/xmlstream/handler/waiter.py7
-rw-r--r--sleekxmpp/xmlstream/jid.py3
-rw-r--r--sleekxmpp/xmlstream/xmlstream.py15
-rw-r--r--tests/live_multiple_streams.py57
-rw-r--r--tests/live_test.py1
11 files changed, 318 insertions, 21 deletions
diff --git a/examples/echo_client.py b/examples/echo_client.py
index 99967d5f..f449ce4e 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.
diff --git a/examples/muc.py b/examples/muc.py
new file mode 100755
index 00000000..8296cb6d
--- /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.")
diff --git a/sleekxmpp/clientxmpp.py b/sleekxmpp/clientxmpp.py
index dbf87647..1a5e0886 100644
--- a/sleekxmpp/clientxmpp.py
+++ b/sleekxmpp/clientxmpp.py
@@ -383,7 +383,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")
diff --git a/sleekxmpp/stanza/iq.py b/sleekxmpp/stanza/iq.py
index 614d14f5..150baa00 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 <iq> 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/test/livesocket.py b/sleekxmpp/test/livesocket.py
index 5e8c5471..3e0f2135 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 17a23f8e..3afd9a62 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
@@ -219,7 +223,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.
@@ -305,6 +312,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())
@@ -319,6 +330,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.")
@@ -326,10 +341,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='',
@@ -599,11 +617,12 @@ 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
+ 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/sleekxmpp/xmlstream/handler/waiter.py b/sleekxmpp/xmlstream/handler/waiter.py
index a4bc3545..341c01fe 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/jid.py b/sleekxmpp/xmlstream/jid.py
index 33d845a0..d8f45b92 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)
diff --git a/sleekxmpp/xmlstream/xmlstream.py b/sleekxmpp/xmlstream/xmlstream.py
index 30b76ce7..9ae31a20 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):
diff --git a/tests/live_multiple_streams.py b/tests/live_multiple_streams.py
new file mode 100644
index 00000000..69ee74c4
--- /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("<tests xmlns='%s' %s %s %s %s />" % (
+ 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 16b6f1cc..b71930af 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):