mirror of
https://github.com/correl/SleekXMPP.git
synced 2024-11-30 19:19:55 +00:00
Merge branch 'develop' of git@github.com:fritzy/SleekXMPP into develop
This commit is contained in:
commit
d576e32f7a
26 changed files with 1401 additions and 341 deletions
9
INSTALL
9
INSTALL
|
@ -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]
|
||||||
|
|
54
example.py
54
example.py
|
@ -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
10
examples/config.xml
Normal 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
190
examples/config_component.py
Executable 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
129
examples/echo_client.py
Executable 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.")
|
|
@ -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']:
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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.")
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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]
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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."
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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
112
tests/test_handlers.py
Normal 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
79
tests/test_stanzabase.py
Normal 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)
|
Loading…
Reference in a new issue