Merge branch 'develop' of git@github.com:fritzy/SleekXMPP into develop

This commit is contained in:
fritzy 2010-09-02 20:01:28 +00:00
commit d576e32f7a
26 changed files with 1401 additions and 341 deletions

View file

@ -1,11 +1,12 @@
Pre-requisites: Pre-requisites:
Python 3.1 or 2.6 - Python 3.1 or 2.6
Install: Install:
python3 setup.py install > python3 setup.py install
Root install: Root install:
sudo python3 setup.py install > sudo python3 setup.py install
To test: 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]

View file

@ -1,54 +0,0 @@
#!/usr/bin/env python
# coding=utf8
import sleekxmpp
import logging
from optparse import OptionParser
import time
import sys
if sys.version_info < (3,0):
reload(sys)
sys.setdefaultencoding('utf8')
class Example(sleekxmpp.ClientXMPP):
def __init__(self, jid, password):
sleekxmpp.ClientXMPP.__init__(self, jid, password)
self.add_event_handler("session_start", self.start)
self.add_event_handler("message", self.message)
def start(self, event):
self.getRoster()
self.sendPresence()
def message(self, msg):
msg.reply("Thanks for sending\n%(body)s" % msg).send()
if __name__ == '__main__':
#parse command line arguements
optp = OptionParser()
optp.add_option('-q','--quiet', help='set logging to ERROR', action='store_const', dest='loglevel', const=logging.ERROR, default=logging.INFO)
optp.add_option('-d','--debug', help='set logging to DEBUG', action='store_const', dest='loglevel', const=logging.DEBUG, default=logging.INFO)
optp.add_option('-v','--verbose', help='set logging to COMM', action='store_const', dest='loglevel', const=5, default=logging.INFO)
optp.add_option("-j","--jid", dest="jid", help="JID to use")
optp.add_option("-p","--password", dest="password", help="password to use")
opts,args = optp.parse_args()
logging.basicConfig(level=opts.loglevel, format='%(levelname)-8s %(message)s')
xmpp = Example(opts.jid, opts.password)
xmpp.registerPlugin('xep_0030')
xmpp.registerPlugin('xep_0004')
xmpp.registerPlugin('xep_0060')
xmpp.registerPlugin('xep_0199')
# use this if you don't have pydns, and want to
# talk to GoogleTalk (e.g.)
# if xmpp.connect(('talk.google.com', 5222)):
if xmpp.connect():
xmpp.process(threaded=False)
print("done")
else:
print("Unable to connect.")

10
examples/config.xml Normal file
View file

@ -0,0 +1,10 @@
<config xmlns="sleekxmpp:config">
<jid>component.localhost</jid>
<secret>ssshh</secret>
<server>localhost</server>
<port>8888</port>
<query xmlns="jabber:iq:roster">
<item jid="user@example.com" subscription="both" />
</query>
</config>

190
examples/config_component.py Executable file
View file

@ -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:
<config xmlns="sleekxmpp:config">
<jid>component.localhost</jid>
<secret>ssshh</secret>
<server>localhost</server>
<port>8888</port>
<query xmlns="jabber:iq:roster">
<item jid="user@example.com" subscription="both" />
</query>
</config>
"""
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.")

129
examples/echo_client.py Executable file
View file

@ -0,0 +1,129 @@
#!/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 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__':
# 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")
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")
else:
print("Unable to connect.")

View file

@ -37,7 +37,7 @@ except ImportError:
#class PresenceStanzaType(object): #class PresenceStanzaType(object):
# #
# def fromXML(self, xml): # def fromXML(self, xml):
# self.ptype = xml.get('type') # self.ptype = xml.get('type')
@ -69,24 +69,24 @@ class ClientXMPP(basexmpp, XMLStream):
self.bound = False self.bound = False
self.bindfail = False self.bindfail = False
self.is_component = False self.is_component = False
self.registerHandler(Callback('Stream Features', MatchXPath('{http://etherx.jabber.org/streams}features'), self._handleStreamFeatures, 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, thread=True)) self.registerHandler(Callback('Roster Update', MatchXPath('{%s}iq/{jabber:iq:roster}query' % self.default_ns), self._handleRoster))
#self.registerHandler(Callback('Roster Update', MatchXMLMask("<presence xmlns='%s' type='subscribe' />" % self.default_ns), self._handlePresenceSubscribe, thread=True)) #self.registerHandler(Callback('Roster Update', MatchXMLMask("<presence xmlns='%s' type='subscribe' />" % self.default_ns), self._handlePresenceSubscribe, thread=True))
self.registerFeature("<starttls xmlns='urn:ietf:params:xml:ns:xmpp-tls' />", self.handler_starttls, True) self.registerFeature("<starttls xmlns='urn:ietf:params:xml:ns:xmpp-tls' />", self.handler_starttls, True)
self.registerFeature("<mechanisms xmlns='urn:ietf:params:xml:ns:xmpp-sasl' />", self.handler_sasl_auth, True) self.registerFeature("<mechanisms xmlns='urn:ietf:params:xml:ns:xmpp-sasl' />", self.handler_sasl_auth, True)
self.registerFeature("<bind xmlns='urn:ietf:params:xml:ns:xmpp-bind' />", self.handler_bind_resource) self.registerFeature("<bind xmlns='urn:ietf:params:xml:ns:xmpp-bind' />", self.handler_bind_resource)
self.registerFeature("<session xmlns='urn:ietf:params:xml:ns:xmpp-session' />", self.handler_start_session) self.registerFeature("<session xmlns='urn:ietf:params:xml:ns:xmpp-session' />", self.handler_start_session)
#self.registerStanzaExtension('PresenceStanza', PresenceStanzaType) #self.registerStanzaExtension('PresenceStanza', PresenceStanzaType)
#self.register_plugins() #self.register_plugins()
def __getitem__(self, key): def __getitem__(self, key):
if key in self.plugin: if key in self.plugin:
return self.plugin[key] return self.plugin[key]
else: else:
logging.warning("""Plugin "%s" is not loaded.""" % key) logging.warning("""Plugin "%s" is not loaded.""" % key)
return False return False
def get(self, key, default): def get(self, key, default):
return self.plugin.get(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.") logging.debug("No appropriate SRV record found. Using JID server name.")
else: else:
# pick a random answer, weighted by priority # 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 # suggestions are welcome
addresses = {} addresses = {}
intmax = 0 intmax = 0
@ -128,18 +128,18 @@ class ClientXMPP(basexmpp, XMLStream):
logging.warning("Failed to connect") logging.warning("Failed to connect")
self.event("disconnected") self.event("disconnected")
return result return result
# overriding reconnect and disconnect so that we can get some events # 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 # should events be part of or required by xmlstream? Maybe that would be cleaner
def reconnect(self): def reconnect(self):
logging.info("Reconnecting") logging.info("Reconnecting")
self.event("disconnected") self.event("disconnected")
XMLStream.reconnect(self) XMLStream.reconnect(self)
def disconnect(self, init=True, close=False, reconnect=False): def disconnect(self, init=True, close=False, reconnect=False):
self.event("disconnected") self.event("disconnected")
XMLStream.disconnect(self, reconnect) XMLStream.disconnect(self, reconnect)
def registerFeature(self, mask, pointer, breaker = False): def registerFeature(self, mask, pointer, breaker = False):
"""Register a stream feature.""" """Register a stream feature."""
self.registered_features.append((MatchXMLMask(mask), pointer, breaker)) self.registered_features.append((MatchXMLMask(mask), pointer, breaker))
@ -157,12 +157,12 @@ class ClientXMPP(basexmpp, XMLStream):
iq['type'] = 'set' iq['type'] = 'set'
iq['roster']['items'] = {jid: {'subscription': 'remove'}} iq['roster']['items'] = {jid: {'subscription': 'remove'}}
return iq.send()['type'] == 'result' return iq.send()['type'] == 'result'
def getRoster(self): def getRoster(self):
"""Request the roster be sent.""" """Request the roster be sent."""
iq = self.Iq().setStanzaValues({'type': 'get'}).enable('roster').send() iq = self.Iq().setStanzaValues({'type': 'get'}).enable('roster').send()
self._handleRoster(iq, request=True) self._handleRoster(iq, request=True)
def _handleStreamFeatures(self, features): def _handleStreamFeatures(self, features):
self.features = [] self.features = []
for sub in features.xml: for sub in features.xml:
@ -173,7 +173,7 @@ class ClientXMPP(basexmpp, XMLStream):
#if self.maskcmp(subelement, feature[0], True): #if self.maskcmp(subelement, feature[0], True):
if feature[1](subelement) and feature[2]: #if breaker, don't continue if feature[1](subelement) and feature[2]: #if breaker, don't continue
return True return True
def handler_starttls(self, xml): def handler_starttls(self, xml):
if not self.authenticated and self.ssl_support: if not self.authenticated and self.ssl_support:
self.add_handler("<proceed xmlns='urn:ietf:params:xml:ns:xmpp-tls' />", self.handler_tls_start, name='TLS Proceed', instream=True) self.add_handler("<proceed xmlns='urn:ietf:params:xml:ns:xmpp-tls' />", self.handler_tls_start, name='TLS Proceed', instream=True)
@ -187,7 +187,7 @@ class ClientXMPP(basexmpp, XMLStream):
logging.debug("Starting TLS") logging.debug("Starting TLS")
if self.startTLS(): if self.startTLS():
raise RestartStream() raise RestartStream()
def handler_sasl_auth(self, xml): def handler_sasl_auth(self, xml):
if '{urn:ietf:params:xml:ns:xmpp-tls}starttls' in self.features: if '{urn:ietf:params:xml:ns:xmpp-tls}starttls' in self.features:
return False return False
@ -209,7 +209,7 @@ class ClientXMPP(basexmpp, XMLStream):
#if 'sasl:DIGEST-MD5' in self.features: #if 'sasl:DIGEST-MD5' in self.features:
# self._auth_digestmd5() # self._auth_digestmd5()
return True return True
def handler_auth_success(self, xml): def handler_auth_success(self, xml):
self.authenticated = True self.authenticated = True
self.features = [] self.features = []
@ -219,7 +219,7 @@ class ClientXMPP(basexmpp, XMLStream):
logging.info("Authentication failed.") logging.info("Authentication failed.")
self.disconnect() self.disconnect()
self.event("failed_auth") self.event("failed_auth")
def handler_bind_resource(self, xml): def handler_bind_resource(self, xml):
logging.debug("Requesting resource: %s" % self.resource) logging.debug("Requesting resource: %s" % self.resource)
xml.clear() xml.clear()
@ -238,7 +238,7 @@ class ClientXMPP(basexmpp, XMLStream):
logging.debug("Established Session") logging.debug("Established Session")
self.sessionstarted = True self.sessionstarted = True
self.event("session_start") self.event("session_start")
def handler_start_session(self, xml): def handler_start_session(self, xml):
if self.authenticated and self.bound: if self.authenticated and self.bound:
iq = self.makeIqSet(xml) iq = self.makeIqSet(xml)
@ -249,7 +249,7 @@ class ClientXMPP(basexmpp, XMLStream):
else: else:
#bind probably hasn't happened yet #bind probably hasn't happened yet
self.bindfail = True self.bindfail = True
def _handleRoster(self, iq, request=False): def _handleRoster(self, iq, request=False):
if iq['type'] == 'set' or (iq['type'] == 'result' and request): if iq['type'] == 'set' or (iq['type'] == 'result' and request):
for jid in iq['roster']['items']: for jid in iq['roster']['items']:

View file

@ -123,7 +123,7 @@ class basexmpp(object):
# threaded is no longer needed, but leaving it for backwards compatibility for now # threaded is no longer needed, but leaving it for backwards compatibility for now
if name is None: if name is None:
name = 'add_handler_%s' % self.getNewId() 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): def getId(self):
return "%x".upper() % self.id return "%x".upper() % self.id

View file

@ -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.")

View file

@ -5,21 +5,37 @@
See the file LICENSE for copying permission. See the file LICENSE for copying permission.
""" """
from socket import _fileobject from socket import _fileobject
import socket import socket
class filesocket(_fileobject):
def read(self, size=4096): class FileSocket(_fileobject):
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): class Socket26(socket._socketobject):
def makefile(self, mode='r', bufsize=-1): """
"""makefile([mode[, bufsize]]) -> file object A custom socket implementation that uses our own FileSocket class
Return a regular file object corresponding to the socket. The mode to work around issues in Python 2.6 when using sockets as files.
and bufsize arguments are as for the built-in open() function.""" """
return filesocket(self._sock, mode, bufsize)
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)

View file

@ -6,23 +6,82 @@
See the file LICENSE for copying permission. See the file LICENSE for copying permission.
""" """
class BaseHandler(object): 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): Handler execution may take place in two phases. The first is during
self.name = name the stream processing itself. The second is after stream processing
self._destroy = False and during SleekXMPP's main event loop. The prerun method is used
self._payload = None for execution during stream processing, and the run method is used
self._matcher = matcher during the main event loop.
def match(self, xml):
return self._matcher.match(xml)
def prerun(self, payload):
self._payload = payload
def run(self, payload): Attributes:
self._payload = payload name -- The name of the handler.
stream -- The stream this handler is assigned to.
def checkDelete(self):
return self._destroy 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

View file

@ -5,30 +5,80 @@
See the file LICENSE for copying permission. See the file LICENSE for copying permission.
""" """
from . import base
import logging
class Callback(base.BaseHandler): from sleekxmpp.xmlstream.handler.base import 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
def prerun(self, payload):
base.BaseHandler.prerun(self, payload) class Callback(BaseHandler):
if self._instream:
self.run(payload, True) """
The Callback handler will execute a callback function with
def run(self, payload, instream=False): matched stanzas.
if not self._instream or instream:
base.BaseHandler.run(self, payload) The handler may execute the callback either during stream
#if self._thread: processing or during the main event loop.
# x = threading.Thread(name="Callback_%s" % self.name, target=self._pointer, args=(payload,))
# x.start() Callback functions are all executed in the same thread, so be
#else: aware if you are executing functions that will block for extended
self._pointer(payload) periods of time. Typically, you should signal your own events using the
if self._once: SleekXMPP object's event() method to pass the stanza off to a threaded
self._destroy = True 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.
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
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

View file

@ -5,32 +5,94 @@
See the file LICENSE for copying permission. See the file LICENSE for copying permission.
""" """
from . import base
try:
import queue
except ImportError:
import Queue as queue
import logging import logging
from .. stanzabase import StanzaBase try:
import queue
except ImportError:
import Queue as queue
class Waiter(base.BaseHandler): from sleekxmpp.xmlstream import StanzaBase, RESPONSE_TIMEOUT
from sleekxmpp.xmlstream.handler.base import 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
def wait(self, timeout=60):
try: class Waiter(BaseHandler):
return self._payload.get(True, timeout)
except queue.Empty: """
logging.warning("Timed out waiting for %s" % self.name) The Waiter handler allows an event handler to block
return False until a particular stanza has been received. The handler
will either be given the matched stanza, or False if the
def checkDelete(self): waiter has timed out.
return True
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):
"""
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, stream=stream)
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

View file

@ -5,10 +5,32 @@
See the file LICENSE for copying permission. See the file LICENSE for copying permission.
""" """
import threading
from . callback import Callback from sleekxmpp.xmlstream.handler import Callback
class XMLCallback(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)

View file

@ -5,9 +5,29 @@
See the file LICENSE for copying permission. See the file LICENSE for copying permission.
""" """
from . waiter import Waiter
from sleekxmpp.xmlstream.handler import Waiter
class XMLWaiter(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)

View file

@ -5,10 +5,30 @@
See the file LICENSE for copying permission. See the file LICENSE for copying permission.
""" """
class MatcherBase(object): class MatcherBase(object):
def __init__(self, criteria): """
self._criteria = criteria Base class for stanza matchers. Stanza matchers are used to pick
stanzas out of the XML stream and pass them to the appropriate
def match(self, xml): stream handlers.
return False """
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

View file

@ -5,9 +5,28 @@
See the file LICENSE for copying permission. See the file LICENSE for copying permission.
""" """
from . import base
class MatcherId(base.MatcherBase): from sleekxmpp.xmlstream.matcher.base import MatcherBase
def match(self, xml):
return xml['id'] == self._criteria 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

View file

@ -5,13 +5,36 @@
See the file LICENSE for copying permission. 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: class MatchMany(MatcherBase):
if m.match(xml):
return True """
return False 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

View file

@ -5,10 +5,34 @@
See the file LICENSE for copying permission. 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)

View file

@ -5,63 +5,151 @@
See the file LICENSE for copying permission. See the file LICENSE for copying permission.
""" """
from . import base
from xml.etree import cElementTree
from xml.parsers.expat import ExpatError 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): # Flag indicating if the builtin XPath matcher should be used, which
base.MatcherBase.__init__(self, criteria) # uses namespaces, or a custom matcher that ignores namespaces.
if type(criteria) == type(''): # Changing this will affect ALL XMLMask matchers.
self._criteria = cElementTree.fromstring(self._criteria) IGNORE_NS = False
self.default_ns = 'jabber:client'
def setDefaultNS(self, ns):
self.default_ns = ns
def match(self, xml):
if hasattr(xml, 'xml'): class MatchXMLMask(MatcherBase):
xml = xml.xml
return self.maskcmp(xml, self._criteria, True) """
The XMLMask matcher selects stanzas whose XML matches a given
def maskcmp(self, source, maskobj, use_ns=False, default_ns='__no_ns__'): XML pattern, or mask. For example, message stanzas with body elements
"""maskcmp(xmlobj, maskobj): could be matched using the mask:
Compare etree xml object to etree xml object mask"""
use_ns = not ignore_ns <message xmlns="jabber:client"><body /></message>
#TODO require namespaces
if source == None: #if element not found (happens during recursive check below) Use of XMLMask is discouraged, and XPath or StanzaPath should be used
return False instead.
if not hasattr(maskobj, 'attrib'): #if the mask is a string, make it an xml obj
try: The use of namespaces in the mask comparison is controlled by
maskobj = cElementTree.fromstring(maskobj) IGNORE_NS. Setting IGNORE_NS to True will disable namespace based matching
except ExpatError: for ALL XMLMask matchers.
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 Methods:
return False match -- Overrides MatcherBase.match.
if use_ns and (source.tag != maskobj.tag and "{%s}%s" % (self.default_ns, maskobj.tag) != source.tag ): setDefaultNS -- Set the default namespace for the mask.
return False """
if maskobj.text and source.text != maskobj.text:
return False def __init__(self, criteria):
for attr_name in maskobj.attrib: #compare attributes """
if source.attrib.get(attr_name, "__None__") != maskobj.attrib[attr_name]: Create a new XMLMask matcher.
return False
#for subelement in maskobj.getiterator()[1:]: #recursively compare subelements Arguments:
for subelement in maskobj: #recursively compare subelements criteria -- Either an XML object or XML string to use as a mask.
if use_ns: """
if not self.maskcmp(source.find(subelement.tag), subelement, use_ns): MatcherBase.__init__(self, criteria)
return False if isinstance(criteria, str):
else: self._criteria = ET.fromstring(self._criteria)
if not self.maskcmp(self.getChildIgnoreNS(source, subelement.tag), subelement, use_ns): self.default_ns = 'jabber:client'
return False
return True def setDefaultNS(self, ns):
"""
def getChildIgnoreNS(self, xml, tag): Set the default namespace to use during comparisons.
tag = tag.split('}')[-1]
try: Arguments:
idx = [c.tag.split('}')[-1] for c in xml.getchildren()].index(tag) ns -- The new namespace to use as the default.
except ValueError: """
return None self.default_ns = ns
return xml.getchildren()[idx]
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]

View file

@ -5,30 +5,75 @@
See the file LICENSE for copying permission. 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): # Flag indicating if the builtin XPath matcher should be used, which
if hasattr(xml, 'xml'): # uses namespaces, or a custom matcher that ignores namespaces.
xml = xml.xml # Changing this will affect ALL XPath matchers.
x = cElementTree.Element('x') IGNORE_NS = False
x.append(xml)
if not ignore_ns:
if x.find(self._criteria) is not None: class MatchXPath(MatcherBase):
return True
return False """
else: The XPath matcher selects stanzas whose XML contents matches a given
criteria = [c.split('}')[-1] for c in self._criteria.split('/')] XPath expression.
xml = x
for tag in criteria: Note that using this matcher may not produce expected behavior when using
children = [c.tag.split('}')[-1] for c in xml.getchildren()] attribute selectors. For Python 2.6 and 3.1, the ElementTree find method
try: does not support the use of attribute selectors. If you need to support
idx = children.index(tag) Python 2.6 or 3.1, it might be more useful to use a StanzaPath matcher.
except ValueError:
return False If the value of IGNORE_NS is set to true, then XPath expressions will
xml = xml.getchildren()[idx] be matched without using namespaces.
return True
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

View file

@ -586,15 +586,16 @@ class ElementBase(object):
string or a list of element names with attribute checks. string or a list of element names with attribute checks.
""" """
if isinstance(xpath, str): 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. # Extract the tag name and attribute checks for the first XPath node.
components = xpath[0].split('@') components = xpath[0].split('@')
tag = components[0] tag = components[0]
attributes = components[1:] attributes = components[1:]
if tag not in (self.name, "{%s}%s" % (self.namespace, self.name), if tag not in (self.name, "{%s}%s" % (self.namespace, self.name)) and \
self.plugins, self.plugin_attrib): tag not in self.plugins and tag not in self.plugin_attrib:
# The requested tag is not in this stanza, so no match. # The requested tag is not in this stanza, so no match.
return False return False
@ -613,6 +614,12 @@ class ElementBase(object):
if self[name] != value: if self[name] != value:
return False 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. # Attempt to continue matching the XPath using the stanza's plugins.
if not matched_substanzas and len(xpath) > 1: if not matched_substanzas and len(xpath) > 1:
# Convert {namespace}tag@attribs to just tag # Convert {namespace}tag@attribs to just tag
@ -754,30 +761,45 @@ class ElementBase(object):
""" """
return self 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. Apply the stanza's namespace to elements in an XPath expression.
Arguments: Arguments:
xpath -- The XPath expression to fix with namespaces. xpath -- The XPath expression to fix with namespaces.
split -- Indicates if the fixed XPath should be left as a split -- Indicates if the fixed XPath should be left as a
list of element names with namespaces. Defaults to list of element names with namespaces. Defaults to
False, which returns a flat string path. 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 = [] fixed = []
# Split the XPath into a series of blocks, where a block
# is started by an element with a namespace.
ns_blocks = xpath.split('{') ns_blocks = xpath.split('{')
for ns_block in ns_blocks: for ns_block in ns_blocks:
if '}' in ns_block: if '}' in ns_block:
# Apply the found namespace to following elements
# that do not have namespaces.
namespace = ns_block.split('}')[0] namespace = ns_block.split('}')[0]
elements = ns_block.split('}')[1].split('/') elements = ns_block.split('}')[1].split('/')
else: else:
# Apply the stanza's namespace to the following
# elements since no namespace was provided.
namespace = self.namespace namespace = self.namespace
elements = ns_block.split('/') elements = ns_block.split('/')
for element in elements: for element in elements:
if element: if element:
fixed.append('{%s}%s' % (namespace, # Skip empty entry artifacts from splitting.
element)) if propagate_ns:
tag = '{%s}%s' % (namespace, element)
else:
tag = element
fixed.append(tag)
if split: if split:
return fixed return fixed
return '/'.join(fixed) return '/'.join(fixed)
@ -886,6 +908,48 @@ class ElementBase(object):
class StanzaBase(ElementBase): 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' name = 'stanza'
namespace = 'jabber:client' namespace = 'jabber:client'
interfaces = set(('type', 'to', 'from', 'id', 'payload')) interfaces = set(('type', 'to', 'from', 'id', 'payload'))
@ -894,6 +958,17 @@ class StanzaBase(ElementBase):
def __init__(self, stream=None, xml=None, stype=None, def __init__(self, stream=None, xml=None, stype=None,
sto=None, sfrom=None, sid=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 self.stream = stream
if stream is not None: if stream is not None:
self.namespace = stream.default_ns self.namespace = stream.default_ns
@ -907,22 +982,73 @@ class StanzaBase(ElementBase):
self.tag = "{%s}%s" % (self.namespace, self.name) self.tag = "{%s}%s" % (self.namespace, self.name)
def setType(self, value): 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: if value in self.types:
self.xml.attrib['type'] = value self.xml.attrib['type'] = value
return self 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): def getPayload(self):
"""Return a list of XML objects contained in the stanza."""
return self.xml.getchildren() return self.xml.getchildren()
def setPayload(self, value): def setPayload(self, value):
self.xml.append(value) """
Add XML content to the stanza.
Arguments:
value -- Either an XML or a stanza object, or a list
of XML or stanza objects.
"""
if not isinstance(value, list):
value = [value]
for val in value:
self.append(val)
return self return self
def delPayload(self): def delPayload(self):
"""Remove the XML contents of the stanza."""
self.clear() self.clear()
return self return self
def clear(self): def clear(self):
"""
Remove all XML element contents and plugins.
Any attribute values will be preserved.
"""
for child in self.xml.getchildren(): for child in self.xml.getchildren():
self.xml.remove(child) self.xml.remove(child)
for plugin in list(self.plugins.keys()): for plugin in list(self.plugins.keys()):
@ -930,6 +1056,12 @@ class StanzaBase(ElementBase):
return self return self
def reply(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 it's a component, use from
if self.stream and hasattr(self.stream, "is_component") and \ if self.stream and hasattr(self.stream, "is_component") and \
self.stream.is_component: self.stream.is_component:
@ -941,35 +1073,42 @@ class StanzaBase(ElementBase):
return self return self
def error(self): def error(self):
"""Set the stanza's type to 'error'."""
self['type'] = 'error' self['type'] = 'error'
return self 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): def unhandled(self):
"""
Called when no handlers have been registered to process this
stanza.
Meant to be overridden.
"""
pass pass
def exception(self, e): def exception(self, e):
"""
Handle exceptions raised during stanza processing.
Meant to be overridden.
"""
logging.exception('Error handling {%s}%s stanza' % (self.namespace, logging.exception('Error handling {%s}%s stanza' % (self.namespace,
self.name)) self.name))
def send(self): def send(self):
"""Queue the stanza to be sent on the XML stream."""
self.stream.sendRaw(self.__str__()) self.stream.sendRaw(self.__str__())
def __copy__(self): 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): def __str__(self):
"""Serialize the stanza's XML to a string."""
return tostring(self.xml, xmlns='', return tostring(self.xml, xmlns='',
stanza_ns=self.namespace, stanza_ns=self.namespace,
stream=self.stream) stream=self.stream)

View file

@ -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 = ssl.wrap_socket(self.socket, ssl_version=ssl.PROTOCOL_TLSv1, do_handshake_on_connect=False)
self.socket.do_handshake() self.socket.do_handshake()
if sys.version_info < (3,0): if sys.version_info < (3,0):
from . filesocket import filesocket self.filesocket = filesocket.FileSocket(self.socket)
self.filesocket = filesocket(self.socket)
else: else:
self.filesocket = self.socket.makefile('rb', 0) self.filesocket = self.socket.makefile('rb', 0)
return True return True
@ -358,8 +357,10 @@ class XMLStream(object):
return False return False
def registerHandler(self, handler, before=None, after=None): def registerHandler(self, handler, before=None, after=None):
"Add handler with matcher class and parameters." "Add handler with matcher class and parameters."
self.__handlers.append(handler) if handler.stream is None:
self.__handlers.append(handler)
handler.stream = self
def removeHandler(self, name): def removeHandler(self, name):
"Removes the handler." "Removes the handler."
@ -367,8 +368,10 @@ class XMLStream(object):
for handler in self.__handlers: for handler in self.__handlers:
if handler.name == name: if handler.name == name:
self.__handlers.pop(idx) self.__handlers.pop(idx)
return return True
idx += 1 idx += 1
return False
def registerStanza(self, stanza_class): def registerStanza(self, stanza_class):
"Adds stanza. If root stanzas build stanzas sent in events while non-root stanzas build substanza objects." "Adds stanza. If root stanzas build stanzas sent in events while non-root stanzas build substanza objects."

View file

@ -504,7 +504,18 @@ class SleekTest(unittest.TestCase):
if xml.attrib != other.attrib: if xml.attrib != other.attrib:
return False 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: for child in xml:
child2s = other.findall("%s" % child.tag) child2s = other.findall("%s" % child.tag)
if child2s is None: if child2s is None:

View file

@ -3,6 +3,21 @@ from sleekxmpp.xmlstream.stanzabase import ElementBase
class TestElementBase(SleekTest): 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): def testExtendedName(self):
"""Test element names of the form tag1/tag2/tag3.""" """Test element names of the form tag1/tag2/tag3."""
@ -332,7 +347,7 @@ class TestElementBase(SleekTest):
</wrapper> </wrapper>
</foo> </foo>
""") """)
stanza._setSubText('bar', text='', keep=True) stanza._setSubText('wrapper/bar', text='', keep=True)
self.checkStanza(TestStanza, stanza, """ self.checkStanza(TestStanza, stanza, """
<foo xmlns="foo"> <foo xmlns="foo">
<wrapper> <wrapper>
@ -343,7 +358,7 @@ class TestElementBase(SleekTest):
""", use_values=False) """, use_values=False)
stanza['bar'] = 'a' stanza['bar'] = 'a'
stanza._setSubText('bar', text='') stanza._setSubText('wrapper/bar', text='')
self.checkStanza(TestStanza, stanza, """ self.checkStanza(TestStanza, stanza, """
<foo xmlns="foo"> <foo xmlns="foo">
<wrapper> <wrapper>
@ -439,12 +454,19 @@ class TestElementBase(SleekTest):
class TestStanza(ElementBase): class TestStanza(ElementBase):
name = "foo" name = "foo"
namespace = "foo" namespace = "foo"
interfaces = set(('bar','baz')) interfaces = set(('bar','baz', 'qux'))
sub_interfaces = set(('qux',))
subitem = (TestSubStanza,) subitem = (TestSubStanza,)
def setQux(self, value):
self._setSubText('qux', text=value)
def getQux(self):
return self._getSubText('qux')
class TestStanzaPlugin(ElementBase): class TestStanzaPlugin(ElementBase):
name = "plugin" name = "plugin"
namespace = "bar" namespace = "http://test/slash/bar"
interfaces = set(('attrib',)) interfaces = set(('attrib',))
registerStanzaPlugin(TestStanza, TestStanzaPlugin) registerStanzaPlugin(TestStanza, TestStanzaPlugin)
@ -464,11 +486,22 @@ class TestElementBase(SleekTest):
self.failUnless(stanza.match("foo@bar=a@baz=b"), self.failUnless(stanza.match("foo@bar=a@baz=b"),
"Stanza did not match its own name with multiple attributes.") "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' stanza['plugin']['attrib'] = 'c'
self.failUnless(stanza.match("foo/plugin@attrib=c"), self.failUnless(stanza.match("foo/plugin@attrib=c"),
"Stanza did not match with plugin and attribute.") "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.") "Stanza did not match with namespaced plugin.")
substanza = TestSubStanza() substanza = TestSubStanza()

112
tests/test_handlers.py Normal file
View file

@ -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("""
<message>
<body>Success!</body>
</message>
""")
callback = Callback('Test Callback',
MatchXPath('{test}tester'),
callback_handler)
self.xmpp.registerHandler(callback)
self.streamRecv("""<tester xmlns="test" />""")
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("""
<message>
<body>Successful: %s</body>
</message>
""" % reply['query'])
self.xmpp.add_event_handler('message', waiter_handler, threaded=True)
# Send message to trigger waiter_handler
self.streamRecv("""
<message>
<body>Testing</body>
</message>
""")
# 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("""
<iq id="test" type="result">
<query xmlns="test" />
</iq>
""")
# 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("""<message><body>Start Test</body></message>""")
# 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)

79
tests/test_stanzabase.py Normal file
View file

@ -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)