mirror of
https://github.com/correl/SleekXMPP.git
synced 2024-11-27 19:19:54 +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:
|
||||
Python 3.1 or 2.6
|
||||
- Python 3.1 or 2.6
|
||||
|
||||
Install:
|
||||
python3 setup.py install
|
||||
> python3 setup.py install
|
||||
|
||||
Root install:
|
||||
sudo python3 setup.py install
|
||||
> sudo python3 setup.py install
|
||||
|
||||
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):
|
||||
#
|
||||
#
|
||||
# def fromXML(self, xml):
|
||||
# self.ptype = xml.get('type')
|
||||
|
||||
|
@ -69,24 +69,24 @@ class ClientXMPP(basexmpp, XMLStream):
|
|||
self.bound = False
|
||||
self.bindfail = False
|
||||
self.is_component = False
|
||||
self.registerHandler(Callback('Stream Features', MatchXPath('{http://etherx.jabber.org/streams}features'), self._handleStreamFeatures, thread=True))
|
||||
self.registerHandler(Callback('Roster Update', MatchXPath('{%s}iq/{jabber:iq:roster}query' % self.default_ns), self._handleRoster, 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))
|
||||
#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("<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("<session xmlns='urn:ietf:params:xml:ns:xmpp-session' />", self.handler_start_session)
|
||||
|
||||
|
||||
#self.registerStanzaExtension('PresenceStanza', PresenceStanzaType)
|
||||
#self.register_plugins()
|
||||
|
||||
|
||||
def __getitem__(self, key):
|
||||
if key in self.plugin:
|
||||
return self.plugin[key]
|
||||
else:
|
||||
logging.warning("""Plugin "%s" is not loaded.""" % key)
|
||||
return False
|
||||
|
||||
|
||||
def get(self, 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.")
|
||||
else:
|
||||
# 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
|
||||
addresses = {}
|
||||
intmax = 0
|
||||
|
@ -128,18 +128,18 @@ class ClientXMPP(basexmpp, XMLStream):
|
|||
logging.warning("Failed to connect")
|
||||
self.event("disconnected")
|
||||
return result
|
||||
|
||||
|
||||
# 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
|
||||
def reconnect(self):
|
||||
logging.info("Reconnecting")
|
||||
self.event("disconnected")
|
||||
XMLStream.reconnect(self)
|
||||
|
||||
|
||||
def disconnect(self, init=True, close=False, reconnect=False):
|
||||
self.event("disconnected")
|
||||
XMLStream.disconnect(self, reconnect)
|
||||
|
||||
|
||||
def registerFeature(self, mask, pointer, breaker = False):
|
||||
"""Register a stream feature."""
|
||||
self.registered_features.append((MatchXMLMask(mask), pointer, breaker))
|
||||
|
@ -157,12 +157,12 @@ class ClientXMPP(basexmpp, XMLStream):
|
|||
iq['type'] = 'set'
|
||||
iq['roster']['items'] = {jid: {'subscription': 'remove'}}
|
||||
return iq.send()['type'] == 'result'
|
||||
|
||||
|
||||
def getRoster(self):
|
||||
"""Request the roster be sent."""
|
||||
iq = self.Iq().setStanzaValues({'type': 'get'}).enable('roster').send()
|
||||
self._handleRoster(iq, request=True)
|
||||
|
||||
|
||||
def _handleStreamFeatures(self, features):
|
||||
self.features = []
|
||||
for sub in features.xml:
|
||||
|
@ -173,7 +173,7 @@ class ClientXMPP(basexmpp, XMLStream):
|
|||
#if self.maskcmp(subelement, feature[0], True):
|
||||
if feature[1](subelement) and feature[2]: #if breaker, don't continue
|
||||
return True
|
||||
|
||||
|
||||
def handler_starttls(self, xml):
|
||||
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)
|
||||
|
@ -187,7 +187,7 @@ class ClientXMPP(basexmpp, XMLStream):
|
|||
logging.debug("Starting TLS")
|
||||
if self.startTLS():
|
||||
raise RestartStream()
|
||||
|
||||
|
||||
def handler_sasl_auth(self, xml):
|
||||
if '{urn:ietf:params:xml:ns:xmpp-tls}starttls' in self.features:
|
||||
return False
|
||||
|
@ -209,7 +209,7 @@ class ClientXMPP(basexmpp, XMLStream):
|
|||
#if 'sasl:DIGEST-MD5' in self.features:
|
||||
# self._auth_digestmd5()
|
||||
return True
|
||||
|
||||
|
||||
def handler_auth_success(self, xml):
|
||||
self.authenticated = True
|
||||
self.features = []
|
||||
|
@ -219,7 +219,7 @@ class ClientXMPP(basexmpp, XMLStream):
|
|||
logging.info("Authentication failed.")
|
||||
self.disconnect()
|
||||
self.event("failed_auth")
|
||||
|
||||
|
||||
def handler_bind_resource(self, xml):
|
||||
logging.debug("Requesting resource: %s" % self.resource)
|
||||
xml.clear()
|
||||
|
@ -238,7 +238,7 @@ class ClientXMPP(basexmpp, XMLStream):
|
|||
logging.debug("Established Session")
|
||||
self.sessionstarted = True
|
||||
self.event("session_start")
|
||||
|
||||
|
||||
def handler_start_session(self, xml):
|
||||
if self.authenticated and self.bound:
|
||||
iq = self.makeIqSet(xml)
|
||||
|
@ -249,7 +249,7 @@ class ClientXMPP(basexmpp, XMLStream):
|
|||
else:
|
||||
#bind probably hasn't happened yet
|
||||
self.bindfail = True
|
||||
|
||||
|
||||
def _handleRoster(self, iq, request=False):
|
||||
if iq['type'] == 'set' or (iq['type'] == 'result' and request):
|
||||
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
|
||||
if name is None:
|
||||
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):
|
||||
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.
|
||||
"""
|
||||
|
||||
from socket import _fileobject
|
||||
import socket
|
||||
|
||||
class filesocket(_fileobject):
|
||||
|
||||
def read(self, size=4096):
|
||||
data = self._sock.recv(size)
|
||||
if data is not None:
|
||||
return data
|
||||
class FileSocket(_fileobject):
|
||||
|
||||
"""
|
||||
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):
|
||||
|
||||
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)
|
||||
"""
|
||||
A custom socket implementation that uses our own FileSocket class
|
||||
to work around issues in Python 2.6 when using sockets as files.
|
||||
"""
|
||||
|
||||
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.
|
||||
"""
|
||||
|
||||
|
||||
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):
|
||||
self.name = name
|
||||
self._destroy = False
|
||||
self._payload = None
|
||||
self._matcher = matcher
|
||||
|
||||
def match(self, xml):
|
||||
return self._matcher.match(xml)
|
||||
|
||||
def prerun(self, payload):
|
||||
self._payload = payload
|
||||
Handler execution may take place in two phases. The first is during
|
||||
the stream processing itself. The second is after stream processing
|
||||
and during SleekXMPP's main event loop. The prerun method is used
|
||||
for execution during stream processing, and the run method is used
|
||||
during the main event loop.
|
||||
|
||||
def run(self, payload):
|
||||
self._payload = payload
|
||||
|
||||
def checkDelete(self):
|
||||
return self._destroy
|
||||
Attributes:
|
||||
name -- The name of the handler.
|
||||
stream -- The stream this handler is assigned to.
|
||||
|
||||
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.
|
||||
"""
|
||||
from . import base
|
||||
import logging
|
||||
|
||||
class Callback(base.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
|
||||
from sleekxmpp.xmlstream.handler.base import BaseHandler
|
||||
|
||||
def prerun(self, payload):
|
||||
base.BaseHandler.prerun(self, payload)
|
||||
if self._instream:
|
||||
self.run(payload, True)
|
||||
|
||||
def run(self, payload, instream=False):
|
||||
if not self._instream or instream:
|
||||
base.BaseHandler.run(self, payload)
|
||||
#if self._thread:
|
||||
# x = threading.Thread(name="Callback_%s" % self.name, target=self._pointer, args=(payload,))
|
||||
# x.start()
|
||||
#else:
|
||||
self._pointer(payload)
|
||||
if self._once:
|
||||
self._destroy = True
|
||||
|
||||
class Callback(BaseHandler):
|
||||
|
||||
"""
|
||||
The Callback handler will execute a callback function with
|
||||
matched stanzas.
|
||||
|
||||
The handler may execute the callback either during stream
|
||||
processing or during the main event loop.
|
||||
|
||||
Callback functions are all executed in the same thread, so be
|
||||
aware if you are executing functions that will block for extended
|
||||
periods of time. Typically, you should signal your own events using the
|
||||
SleekXMPP object's event() method to pass the stanza off to a threaded
|
||||
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.
|
||||
"""
|
||||
from . import base
|
||||
try:
|
||||
import queue
|
||||
except ImportError:
|
||||
import Queue as queue
|
||||
|
||||
import logging
|
||||
from .. stanzabase import StanzaBase
|
||||
try:
|
||||
import queue
|
||||
except ImportError:
|
||||
import Queue as queue
|
||||
|
||||
class Waiter(base.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
|
||||
from sleekxmpp.xmlstream import StanzaBase, RESPONSE_TIMEOUT
|
||||
from sleekxmpp.xmlstream.handler.base import BaseHandler
|
||||
|
||||
def wait(self, timeout=60):
|
||||
try:
|
||||
return self._payload.get(True, timeout)
|
||||
except queue.Empty:
|
||||
logging.warning("Timed out waiting for %s" % self.name)
|
||||
return False
|
||||
|
||||
def checkDelete(self):
|
||||
return True
|
||||
|
||||
class Waiter(BaseHandler):
|
||||
|
||||
"""
|
||||
The Waiter handler allows an event handler to block
|
||||
until a particular stanza has been received. The handler
|
||||
will either be given the matched stanza, or False if the
|
||||
waiter has timed out.
|
||||
|
||||
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.
|
||||
"""
|
||||
import threading
|
||||
from . callback import Callback
|
||||
|
||||
from sleekxmpp.xmlstream.handler import 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.
|
||||
"""
|
||||
from . waiter import Waiter
|
||||
|
||||
from sleekxmpp.xmlstream.handler import 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.
|
||||
"""
|
||||
|
||||
|
||||
class MatcherBase(object):
|
||||
|
||||
def __init__(self, criteria):
|
||||
self._criteria = criteria
|
||||
|
||||
def match(self, xml):
|
||||
return False
|
||||
"""
|
||||
Base class for stanza matchers. Stanza matchers are used to pick
|
||||
stanzas out of the XML stream and pass them to the appropriate
|
||||
stream handlers.
|
||||
"""
|
||||
|
||||
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.
|
||||
"""
|
||||
from . import base
|
||||
|
||||
class MatcherId(base.MatcherBase):
|
||||
|
||||
def match(self, xml):
|
||||
return xml['id'] == self._criteria
|
||||
from sleekxmpp.xmlstream.matcher.base import MatcherBase
|
||||
|
||||
|
||||
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.
|
||||
"""
|
||||
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:
|
||||
if m.match(xml):
|
||||
return True
|
||||
return False
|
||||
|
||||
class MatchMany(MatcherBase):
|
||||
|
||||
"""
|
||||
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.
|
||||
"""
|
||||
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.
|
||||
"""
|
||||
from . import base
|
||||
from xml.etree import cElementTree
|
||||
|
||||
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):
|
||||
base.MatcherBase.__init__(self, criteria)
|
||||
if type(criteria) == type(''):
|
||||
self._criteria = cElementTree.fromstring(self._criteria)
|
||||
self.default_ns = 'jabber:client'
|
||||
|
||||
def setDefaultNS(self, ns):
|
||||
self.default_ns = ns
|
||||
# Flag indicating if the builtin XPath matcher should be used, which
|
||||
# uses namespaces, or a custom matcher that ignores namespaces.
|
||||
# Changing this will affect ALL XMLMask matchers.
|
||||
IGNORE_NS = False
|
||||
|
||||
def match(self, xml):
|
||||
if hasattr(xml, 'xml'):
|
||||
xml = xml.xml
|
||||
return self.maskcmp(xml, self._criteria, True)
|
||||
|
||||
def maskcmp(self, source, maskobj, use_ns=False, default_ns='__no_ns__'):
|
||||
"""maskcmp(xmlobj, maskobj):
|
||||
Compare etree xml object to etree xml object mask"""
|
||||
use_ns = not ignore_ns
|
||||
#TODO require namespaces
|
||||
if source == None: #if element not found (happens during recursive check below)
|
||||
return False
|
||||
if not hasattr(maskobj, 'attrib'): #if the mask is a string, make it an xml obj
|
||||
try:
|
||||
maskobj = cElementTree.fromstring(maskobj)
|
||||
except ExpatError:
|
||||
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
|
||||
return False
|
||||
if use_ns and (source.tag != maskobj.tag and "{%s}%s" % (self.default_ns, maskobj.tag) != source.tag ):
|
||||
return False
|
||||
if maskobj.text and source.text != maskobj.text:
|
||||
return False
|
||||
for attr_name in maskobj.attrib: #compare attributes
|
||||
if source.attrib.get(attr_name, "__None__") != maskobj.attrib[attr_name]:
|
||||
return False
|
||||
#for subelement in maskobj.getiterator()[1:]: #recursively compare subelements
|
||||
for subelement in maskobj: #recursively compare subelements
|
||||
if use_ns:
|
||||
if not self.maskcmp(source.find(subelement.tag), subelement, use_ns):
|
||||
return False
|
||||
else:
|
||||
if not self.maskcmp(self.getChildIgnoreNS(source, subelement.tag), subelement, use_ns):
|
||||
return False
|
||||
return True
|
||||
|
||||
def getChildIgnoreNS(self, xml, tag):
|
||||
tag = tag.split('}')[-1]
|
||||
try:
|
||||
idx = [c.tag.split('}')[-1] for c in xml.getchildren()].index(tag)
|
||||
except ValueError:
|
||||
return None
|
||||
return xml.getchildren()[idx]
|
||||
|
||||
class MatchXMLMask(MatcherBase):
|
||||
|
||||
"""
|
||||
The XMLMask matcher selects stanzas whose XML matches a given
|
||||
XML pattern, or mask. For example, message stanzas with body elements
|
||||
could be matched using the mask:
|
||||
|
||||
<message xmlns="jabber:client"><body /></message>
|
||||
|
||||
Use of XMLMask is discouraged, and XPath or StanzaPath should be used
|
||||
instead.
|
||||
|
||||
The use of namespaces in the mask comparison is controlled by
|
||||
IGNORE_NS. Setting IGNORE_NS to True will disable namespace based matching
|
||||
for ALL XMLMask matchers.
|
||||
|
||||
Methods:
|
||||
match -- Overrides MatcherBase.match.
|
||||
setDefaultNS -- Set the default namespace for the mask.
|
||||
"""
|
||||
|
||||
def __init__(self, criteria):
|
||||
"""
|
||||
Create a new XMLMask matcher.
|
||||
|
||||
Arguments:
|
||||
criteria -- Either an XML object or XML string to use as a mask.
|
||||
"""
|
||||
MatcherBase.__init__(self, criteria)
|
||||
if isinstance(criteria, str):
|
||||
self._criteria = ET.fromstring(self._criteria)
|
||||
self.default_ns = 'jabber:client'
|
||||
|
||||
def setDefaultNS(self, ns):
|
||||
"""
|
||||
Set the default namespace to use during comparisons.
|
||||
|
||||
Arguments:
|
||||
ns -- The new namespace to use as the default.
|
||||
"""
|
||||
self.default_ns = ns
|
||||
|
||||
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.
|
||||
"""
|
||||
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):
|
||||
if hasattr(xml, 'xml'):
|
||||
xml = xml.xml
|
||||
x = cElementTree.Element('x')
|
||||
x.append(xml)
|
||||
if not ignore_ns:
|
||||
if x.find(self._criteria) is not None:
|
||||
return True
|
||||
return False
|
||||
else:
|
||||
criteria = [c.split('}')[-1] for c in self._criteria.split('/')]
|
||||
xml = x
|
||||
for tag in criteria:
|
||||
children = [c.tag.split('}')[-1] for c in xml.getchildren()]
|
||||
try:
|
||||
idx = children.index(tag)
|
||||
except ValueError:
|
||||
return False
|
||||
xml = xml.getchildren()[idx]
|
||||
return True
|
||||
# Flag indicating if the builtin XPath matcher should be used, which
|
||||
# uses namespaces, or a custom matcher that ignores namespaces.
|
||||
# Changing this will affect ALL XPath matchers.
|
||||
IGNORE_NS = False
|
||||
|
||||
|
||||
class MatchXPath(MatcherBase):
|
||||
|
||||
"""
|
||||
The XPath matcher selects stanzas whose XML contents matches a given
|
||||
XPath expression.
|
||||
|
||||
Note that using this matcher may not produce expected behavior when using
|
||||
attribute selectors. For Python 2.6 and 3.1, the ElementTree find method
|
||||
does not support the use of attribute selectors. If you need to support
|
||||
Python 2.6 or 3.1, it might be more useful to use a StanzaPath matcher.
|
||||
|
||||
If the value of IGNORE_NS is set to true, then XPath expressions will
|
||||
be matched without using namespaces.
|
||||
|
||||
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.
|
||||
"""
|
||||
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.
|
||||
components = xpath[0].split('@')
|
||||
tag = components[0]
|
||||
attributes = components[1:]
|
||||
|
||||
if tag not in (self.name, "{%s}%s" % (self.namespace, self.name),
|
||||
self.plugins, self.plugin_attrib):
|
||||
if tag not in (self.name, "{%s}%s" % (self.namespace, self.name)) and \
|
||||
tag not in self.plugins and tag not in self.plugin_attrib:
|
||||
# The requested tag is not in this stanza, so no match.
|
||||
return False
|
||||
|
||||
|
@ -613,6 +614,12 @@ class ElementBase(object):
|
|||
if self[name] != value:
|
||||
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.
|
||||
if not matched_substanzas and len(xpath) > 1:
|
||||
# Convert {namespace}tag@attribs to just tag
|
||||
|
@ -754,30 +761,45 @@ class ElementBase(object):
|
|||
"""
|
||||
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.
|
||||
|
||||
Arguments:
|
||||
xpath -- The XPath expression to fix with namespaces.
|
||||
split -- Indicates if the fixed XPath should be left as a
|
||||
list of element names with namespaces. Defaults to
|
||||
False, which returns a flat string path.
|
||||
xpath -- The XPath expression to fix with namespaces.
|
||||
split -- Indicates if the fixed XPath should be left as a
|
||||
list of element names with namespaces. Defaults to
|
||||
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 = []
|
||||
# Split the XPath into a series of blocks, where a block
|
||||
# is started by an element with a namespace.
|
||||
ns_blocks = xpath.split('{')
|
||||
for ns_block in ns_blocks:
|
||||
if '}' in ns_block:
|
||||
# Apply the found namespace to following elements
|
||||
# that do not have namespaces.
|
||||
namespace = ns_block.split('}')[0]
|
||||
elements = ns_block.split('}')[1].split('/')
|
||||
else:
|
||||
# Apply the stanza's namespace to the following
|
||||
# elements since no namespace was provided.
|
||||
namespace = self.namespace
|
||||
elements = ns_block.split('/')
|
||||
|
||||
for element in elements:
|
||||
if element:
|
||||
fixed.append('{%s}%s' % (namespace,
|
||||
element))
|
||||
# Skip empty entry artifacts from splitting.
|
||||
if propagate_ns:
|
||||
tag = '{%s}%s' % (namespace, element)
|
||||
else:
|
||||
tag = element
|
||||
fixed.append(tag)
|
||||
if split:
|
||||
return fixed
|
||||
return '/'.join(fixed)
|
||||
|
@ -886,6 +908,48 @@ class ElementBase(object):
|
|||
|
||||
|
||||
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'
|
||||
namespace = 'jabber:client'
|
||||
interfaces = set(('type', 'to', 'from', 'id', 'payload'))
|
||||
|
@ -894,6 +958,17 @@ class StanzaBase(ElementBase):
|
|||
|
||||
def __init__(self, stream=None, xml=None, stype=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
|
||||
if stream is not None:
|
||||
self.namespace = stream.default_ns
|
||||
|
@ -907,22 +982,73 @@ class StanzaBase(ElementBase):
|
|||
self.tag = "{%s}%s" % (self.namespace, self.name)
|
||||
|
||||
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:
|
||||
self.xml.attrib['type'] = value
|
||||
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):
|
||||
"""Return a list of XML objects contained in the stanza."""
|
||||
return self.xml.getchildren()
|
||||
|
||||
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
|
||||
|
||||
def delPayload(self):
|
||||
"""Remove the XML contents of the stanza."""
|
||||
self.clear()
|
||||
return self
|
||||
|
||||
def clear(self):
|
||||
"""
|
||||
Remove all XML element contents and plugins.
|
||||
|
||||
Any attribute values will be preserved.
|
||||
"""
|
||||
for child in self.xml.getchildren():
|
||||
self.xml.remove(child)
|
||||
for plugin in list(self.plugins.keys()):
|
||||
|
@ -930,6 +1056,12 @@ class StanzaBase(ElementBase):
|
|||
return 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 self.stream and hasattr(self.stream, "is_component") and \
|
||||
self.stream.is_component:
|
||||
|
@ -941,35 +1073,42 @@ class StanzaBase(ElementBase):
|
|||
return self
|
||||
|
||||
def error(self):
|
||||
"""Set the stanza's type to 'error'."""
|
||||
self['type'] = 'error'
|
||||
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):
|
||||
"""
|
||||
Called when no handlers have been registered to process this
|
||||
stanza.
|
||||
|
||||
Meant to be overridden.
|
||||
"""
|
||||
pass
|
||||
|
||||
def exception(self, e):
|
||||
"""
|
||||
Handle exceptions raised during stanza processing.
|
||||
|
||||
Meant to be overridden.
|
||||
"""
|
||||
logging.exception('Error handling {%s}%s stanza' % (self.namespace,
|
||||
self.name))
|
||||
|
||||
def send(self):
|
||||
"""Queue the stanza to be sent on the XML stream."""
|
||||
self.stream.sendRaw(self.__str__())
|
||||
|
||||
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):
|
||||
"""Serialize the stanza's XML to a string."""
|
||||
return tostring(self.xml, xmlns='',
|
||||
stanza_ns=self.namespace,
|
||||
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.do_handshake()
|
||||
if sys.version_info < (3,0):
|
||||
from . filesocket import filesocket
|
||||
self.filesocket = filesocket(self.socket)
|
||||
self.filesocket = filesocket.FileSocket(self.socket)
|
||||
else:
|
||||
self.filesocket = self.socket.makefile('rb', 0)
|
||||
return True
|
||||
|
@ -358,8 +357,10 @@ class XMLStream(object):
|
|||
return False
|
||||
|
||||
def registerHandler(self, handler, before=None, after=None):
|
||||
"Add handler with matcher class and parameters."
|
||||
self.__handlers.append(handler)
|
||||
"Add handler with matcher class and parameters."
|
||||
if handler.stream is None:
|
||||
self.__handlers.append(handler)
|
||||
handler.stream = self
|
||||
|
||||
def removeHandler(self, name):
|
||||
"Removes the handler."
|
||||
|
@ -367,8 +368,10 @@ class XMLStream(object):
|
|||
for handler in self.__handlers:
|
||||
if handler.name == name:
|
||||
self.__handlers.pop(idx)
|
||||
return
|
||||
return True
|
||||
idx += 1
|
||||
return False
|
||||
|
||||
|
||||
def registerStanza(self, stanza_class):
|
||||
"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:
|
||||
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:
|
||||
child2s = other.findall("%s" % child.tag)
|
||||
if child2s is None:
|
||||
|
|
|
@ -3,6 +3,21 @@ from sleekxmpp.xmlstream.stanzabase import ElementBase
|
|||
|
||||
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):
|
||||
"""Test element names of the form tag1/tag2/tag3."""
|
||||
|
||||
|
@ -332,7 +347,7 @@ class TestElementBase(SleekTest):
|
|||
</wrapper>
|
||||
</foo>
|
||||
""")
|
||||
stanza._setSubText('bar', text='', keep=True)
|
||||
stanza._setSubText('wrapper/bar', text='', keep=True)
|
||||
self.checkStanza(TestStanza, stanza, """
|
||||
<foo xmlns="foo">
|
||||
<wrapper>
|
||||
|
@ -343,7 +358,7 @@ class TestElementBase(SleekTest):
|
|||
""", use_values=False)
|
||||
|
||||
stanza['bar'] = 'a'
|
||||
stanza._setSubText('bar', text='')
|
||||
stanza._setSubText('wrapper/bar', text='')
|
||||
self.checkStanza(TestStanza, stanza, """
|
||||
<foo xmlns="foo">
|
||||
<wrapper>
|
||||
|
@ -439,12 +454,19 @@ class TestElementBase(SleekTest):
|
|||
class TestStanza(ElementBase):
|
||||
name = "foo"
|
||||
namespace = "foo"
|
||||
interfaces = set(('bar','baz'))
|
||||
interfaces = set(('bar','baz', 'qux'))
|
||||
sub_interfaces = set(('qux',))
|
||||
subitem = (TestSubStanza,)
|
||||
|
||||
def setQux(self, value):
|
||||
self._setSubText('qux', text=value)
|
||||
|
||||
def getQux(self):
|
||||
return self._getSubText('qux')
|
||||
|
||||
class TestStanzaPlugin(ElementBase):
|
||||
name = "plugin"
|
||||
namespace = "bar"
|
||||
namespace = "http://test/slash/bar"
|
||||
interfaces = set(('attrib',))
|
||||
|
||||
registerStanzaPlugin(TestStanza, TestStanzaPlugin)
|
||||
|
@ -464,11 +486,22 @@ class TestElementBase(SleekTest):
|
|||
self.failUnless(stanza.match("foo@bar=a@baz=b"),
|
||||
"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'
|
||||
self.failUnless(stanza.match("foo/plugin@attrib=c"),
|
||||
"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.")
|
||||
|
||||
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