Merge branch 'develop' of github.com:fritzy/SleekXMPP into roster
This commit is contained in:
commit
58b95e4ae4
11 changed files with 318 additions and 21 deletions
|
@ -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.
|
||||
|
|
186
examples/muc.py
Executable file
186
examples/muc.py
Executable file
|
@ -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.")
|
|
@ -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")
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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):
|
||||
|
|
57
tests/live_multiple_streams.py
Normal file
57
tests/live_multiple_streams.py
Normal file
|
@ -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()))
|
|
@ -1,7 +1,6 @@
|
|||
import logging
|
||||
|
||||
from sleekxmpp.test import *
|
||||
import sleekxmpp.plugins.xep_0033 as xep_0033
|
||||
|
||||
|
||||
class TestLiveStream(SleekTest):
|
||||
|
|
Loading…
Reference in a new issue