summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore2
-rw-r--r--LICENSE121
-rw-r--r--README3
-rwxr-xr-xexamples/proxy_echo_client.py167
-rw-r--r--setup.py39
-rw-r--r--sleekxmpp/__init__.py4
-rw-r--r--sleekxmpp/basexmpp.py70
-rw-r--r--sleekxmpp/clientxmpp.py278
-rw-r--r--sleekxmpp/componentxmpp.py4
-rw-r--r--sleekxmpp/features/__init__.py9
-rw-r--r--sleekxmpp/features/feature_bind/__init__.py10
-rw-r--r--sleekxmpp/features/feature_bind/bind.py64
-rw-r--r--sleekxmpp/features/feature_bind/stanza.py22
-rw-r--r--sleekxmpp/features/feature_mechanisms/__init__.py13
-rw-r--r--sleekxmpp/features/feature_mechanisms/mechanisms.py129
-rw-r--r--sleekxmpp/features/feature_mechanisms/stanza/__init__.py15
-rw-r--r--sleekxmpp/features/feature_mechanisms/stanza/auth.py39
-rw-r--r--sleekxmpp/features/feature_mechanisms/stanza/challenge.py39
-rw-r--r--sleekxmpp/features/feature_mechanisms/stanza/failure.py78
-rw-r--r--sleekxmpp/features/feature_mechanisms/stanza/mechanisms.py55
-rw-r--r--sleekxmpp/features/feature_mechanisms/stanza/response.py39
-rw-r--r--sleekxmpp/features/feature_mechanisms/stanza/success.py26
-rw-r--r--sleekxmpp/features/feature_session/__init__.py10
-rw-r--r--sleekxmpp/features/feature_session/session.py56
-rw-r--r--sleekxmpp/features/feature_session/stanza.py21
-rw-r--r--sleekxmpp/features/feature_starttls/__init__.py10
-rw-r--r--sleekxmpp/features/feature_starttls/stanza.py47
-rw-r--r--sleekxmpp/features/feature_starttls/starttls.py70
-rw-r--r--sleekxmpp/plugins/__init__.py7
-rw-r--r--sleekxmpp/plugins/base.py3
-rw-r--r--sleekxmpp/plugins/old_0060.py (renamed from sleekxmpp/plugins/xep_0060.py)0
-rw-r--r--sleekxmpp/plugins/stanza_pubsub.py557
-rw-r--r--sleekxmpp/plugins/xep_0009/remote.py2
-rw-r--r--sleekxmpp/plugins/xep_0050/adhoc.py2
-rw-r--r--sleekxmpp/plugins/xep_0060/__init__.py2
-rw-r--r--sleekxmpp/plugins/xep_0060/pubsub.py313
-rw-r--r--sleekxmpp/plugins/xep_0060/stanza/__init__.py3
-rw-r--r--sleekxmpp/plugins/xep_0060/stanza/base.py24
-rw-r--r--sleekxmpp/plugins/xep_0060/stanza/pubsub.py277
-rw-r--r--sleekxmpp/plugins/xep_0060/stanza/pubsub_event.py124
-rw-r--r--sleekxmpp/plugins/xep_0060/stanza/pubsub_owner.py152
-rw-r--r--sleekxmpp/plugins/xep_0066/__init__.py11
-rw-r--r--sleekxmpp/plugins/xep_0066/oob.py154
-rw-r--r--sleekxmpp/plugins/xep_0066/stanza.py33
-rw-r--r--sleekxmpp/plugins/xep_0078.py72
-rw-r--r--sleekxmpp/plugins/xep_0078/__init__.py12
-rw-r--r--sleekxmpp/plugins/xep_0078/legacyauth.py108
-rw-r--r--sleekxmpp/plugins/xep_0078/stanza.py43
-rw-r--r--sleekxmpp/plugins/xep_0082.py206
-rw-r--r--sleekxmpp/plugins/xep_0092/version.py2
-rw-r--r--sleekxmpp/plugins/xep_0199/ping.py2
-rw-r--r--sleekxmpp/plugins/xep_0202.py117
-rw-r--r--sleekxmpp/plugins/xep_0202/__init__.py12
-rw-r--r--sleekxmpp/plugins/xep_0202/stanza.py127
-rw-r--r--sleekxmpp/plugins/xep_0202/time.py92
-rw-r--r--sleekxmpp/plugins/xep_0203/__init__.py12
-rw-r--r--sleekxmpp/plugins/xep_0203/delay.py36
-rw-r--r--sleekxmpp/plugins/xep_0203/stanza.py41
-rw-r--r--sleekxmpp/plugins/xep_0224/__init__.py11
-rw-r--r--sleekxmpp/plugins/xep_0224/attention.py72
-rw-r--r--sleekxmpp/plugins/xep_0224/stanza.py40
-rw-r--r--sleekxmpp/stanza/__init__.py3
-rw-r--r--sleekxmpp/stanza/error.py4
-rw-r--r--sleekxmpp/stanza/message.py2
-rw-r--r--sleekxmpp/stanza/rootstanza.py5
-rw-r--r--sleekxmpp/stanza/stream_features.py54
-rw-r--r--sleekxmpp/test/sleektest.py2
-rw-r--r--sleekxmpp/thirdparty/__init__.py3
-rw-r--r--sleekxmpp/thirdparty/mini_dateutil.py267
-rw-r--r--sleekxmpp/thirdparty/suelta/LICENSE21
-rw-r--r--sleekxmpp/thirdparty/suelta/PLAYING-NICELY27
-rw-r--r--sleekxmpp/thirdparty/suelta/README8
-rw-r--r--sleekxmpp/thirdparty/suelta/__init__.py26
-rw-r--r--sleekxmpp/thirdparty/suelta/exceptions.py31
-rw-r--r--sleekxmpp/thirdparty/suelta/mechanisms/__init__.py5
-rw-r--r--sleekxmpp/thirdparty/suelta/mechanisms/anonymous.py36
-rw-r--r--sleekxmpp/thirdparty/suelta/mechanisms/cram_md5.py63
-rw-r--r--sleekxmpp/thirdparty/suelta/mechanisms/digest_md5.py273
-rw-r--r--sleekxmpp/thirdparty/suelta/mechanisms/plain.py61
-rw-r--r--sleekxmpp/thirdparty/suelta/mechanisms/scram_hmac.py176
-rw-r--r--sleekxmpp/thirdparty/suelta/sasl.py402
-rw-r--r--sleekxmpp/thirdparty/suelta/saslprep.py78
-rw-r--r--sleekxmpp/thirdparty/suelta/util.py118
-rw-r--r--sleekxmpp/xmlstream/stanzabase.py10
-rw-r--r--sleekxmpp/xmlstream/tostring.py (renamed from sleekxmpp/xmlstream/tostring/tostring.py)31
-rw-r--r--sleekxmpp/xmlstream/tostring/__init__.py19
-rw-r--r--sleekxmpp/xmlstream/tostring/tostring26.py110
-rw-r--r--sleekxmpp/xmlstream/xmlstream.py157
-rw-r--r--tests/test_stanza_xep_0060.py2
-rw-r--r--tests/test_stream_exceptions.py126
-rw-r--r--tests/test_stream_presence.py51
-rw-r--r--tests/test_stream_xep_0030.py14
-rw-r--r--tests/test_stream_xep_0066.py44
-rw-r--r--tests/test_stream_xep_0128.py1
-rw-r--r--tests/test_stream_xep_0249.py1
-rw-r--r--tests/test_tostring.py4
-rw-r--r--todo1.010
97 files changed, 5157 insertions, 1197 deletions
diff --git a/.gitignore b/.gitignore
index 0fe2c40e..ff75f768 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,4 @@
*.pyc
build/
+dist/
+MANIFEST
diff --git a/LICENSE b/LICENSE
index fb9f977c..df302d00 100644
--- a/LICENSE
+++ b/LICENSE
@@ -17,3 +17,124 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
+
+
+
+
+Licences of Bundled Third Pary Code
+-----------------------------------
+
+dateutil - Extensions to the standard python 2.3+ datetime module.
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Copyright (c) 2003-2011 - Gustavo Niemeyer <gustavo@niemeyer.net>
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+ * Redistributions of source code must retain the above copyright notice,
+ this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+ * Neither the name of the copyright holder nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+fixed_datetime
+~~~~~~~~~~~~~~
+
+Copyright (c) 2008, Red Innovation Ltd., Finland
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+ * Neither the name of Red Innovation nor the names of its contributors
+ may be used to endorse or promote products derived from this software
+ without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY RED INNOVATION ``AS IS'' AND ANY
+EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL RED INNOVATION BE LIABLE FOR ANY
+DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+
+OrderedDict - A port of the Python 2.7+ OrderedDict to Python 2.6
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Copyright (c) 2009 Raymond Hettinger
+
+Permission is hereby granted, free of charge, to any person
+obtaining a copy of this software and associated documentation files
+(the "Software"), to deal in the Software without restriction,
+including without limitation the rights to use, copy, modify, merge,
+publish, distribute, sublicense, and/or sell copies of the Software,
+and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be
+ included in all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+ OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+ HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+ WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+ OTHER DEALINGS IN THE SOFTWARE.
+
+
+
+
+SUELTA – A PURE-PYTHON SASL CLIENT LIBRARY
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+This software is subject to "The MIT License"
+
+Copyright 2007-2010 David Alan Cridland
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/README b/README
index da9fe8c8..8a85365c 100644
--- a/README
+++ b/README
@@ -42,6 +42,9 @@ Main Author: Nathan Fritz fritz@netflint.net
Contributors: Kevin Smith & Lance Stout
Patches: Remko Tronçon
+Dave Cridland, for his Suelta SASL library.
+
+
Feel free to add fritzy@netflint.net to your roster for direct support and comments.
Join sleekxmpp-discussion@googlegroups.com / http://groups.google.com/group/sleekxmpp-discussion for email discussion.
Join sleek@conference.jabber.org for groupchat discussion.
diff --git a/examples/proxy_echo_client.py b/examples/proxy_echo_client.py
new file mode 100755
index 00000000..4db9a552
--- /dev/null
+++ b/examples/proxy_echo_client.py
@@ -0,0 +1,167 @@
+#!/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
+import getpass
+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.send_presence()
+ self.get_roster()
+
+ 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")
+ optp.add_option("--phost", dest="proxy_host",
+ help="Proxy hostname")
+ optp.add_option("--pport", dest="proxy_port",
+ help="Proxy port")
+ optp.add_option("--puser", dest="proxy_user",
+ help="Proxy username")
+ optp.add_option("--ppass", dest="proxy_pass",
+ help="Proxy password")
+
+
+
+ opts, args = optp.parse_args()
+
+ # Setup logging.
+ logging.basicConfig(level=opts.loglevel,
+ format='%(levelname)-8s %(message)s')
+
+ if opts.jid is None:
+ opts.jid = raw_input("Username: ")
+ if opts.password is None:
+ opts.password = getpass.getpass("Password: ")
+ if opts.proxy_host is None:
+ opts.proxy_host = raw_input("Proxy host: ")
+ if opts.proxy_port is None:
+ opts.proxy_port = raw_input("Proxy port: ")
+ if opts.proxy_user is None:
+ opts.proxy_user = raw_input("Proxy username: ")
+ if opts.proxy_pass is None and opts.proxy_user:
+ opts.proxy_pass = getpass.getpass("Proxy password: ")
+
+ # 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.register_plugin('xep_0030') # Service Discovery
+ xmpp.register_plugin('xep_0004') # Data Forms
+ xmpp.register_plugin('xep_0060') # PubSub
+ xmpp.register_plugin('xep_0199') # XMPP Ping
+
+ # If you are working with an OpenFire server, you may need
+ # to adjust the SSL version used:
+ # xmpp.ssl_version = ssl.PROTOCOL_SSLv3
+
+ # If you want to verify the SSL certificates offered by a server:
+ # xmpp.ca_certs = "path/to/ca/cert"
+
+ xmpp.use_proxy = True
+ xmpp.proxy_config = {
+ 'host': opts.proxy_host,
+ 'port': int(opts.proxy_port),
+ 'username': opts.proxy_user,
+ 'password': opts.proxy_pass}
+
+ # Connect to the XMPP server and start processing XMPP stanzas.
+ if xmpp.connect():
+ # If you do not have the pydns library installed, you will need
+ # to manually specify the name of the server if it does not match
+ # the one in the JID. For example, to use Google Talk you would
+ # need to use:
+ #
+ # if xmpp.connect(('talk.google.com', 5222)):
+ # ...
+ xmpp.process(threaded=False)
+ print("Done")
+ else:
+ print("Unable to connect.")
diff --git a/setup.py b/setup.py
index 19047925..e3b3aa9a 100644
--- a/setup.py
+++ b/setup.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
-# Copyright (C) 2007-2008 Nathanael C. Fritz
+# Copyright (C) 2007-2011 Nathanael C. Fritz
# All Rights Reserved
#
# This software is licensed as described in the README file,
@@ -29,13 +29,16 @@ import sleekxmpp
VERSION = sleekxmpp.__version__
DESCRIPTION = 'SleekXMPP is an elegant Python library for XMPP (aka Jabber, Google Talk, etc).'
-LONG_DESCRIPTION = """
-SleekXMPP is an elegant Python library for XMPP (aka Jabber, Google Talk, etc).
-"""
+with open('README') as readme:
+ LONG_DESCRIPTION = '\n'.join(readme)
CLASSIFIERS = [ 'Intended Audience :: Developers',
- 'License :: OSI Approved :: MIT',
+ 'License :: OSI Approved :: MIT License',
'Programming Language :: Python',
+ 'Programming Language :: Python 2.6',
+ 'Programming Language :: Python 2.7',
+ 'Programming Language :: Python 3.1',
+ 'Programming Language :: Python 3.2',
'Topic :: Software Development :: Libraries :: Python Modules',
]
@@ -45,7 +48,6 @@ packages = [ 'sleekxmpp',
'sleekxmpp/xmlstream',
'sleekxmpp/xmlstream/matcher',
'sleekxmpp/xmlstream/handler',
- 'sleekxmpp/thirdparty',
'sleekxmpp/plugins',
'sleekxmpp/plugins/xep_0009',
'sleekxmpp/plugins/xep_0009/stanza',
@@ -53,18 +55,30 @@ packages = [ 'sleekxmpp',
'sleekxmpp/plugins/xep_0030/stanza',
'sleekxmpp/plugins/xep_0050',
'sleekxmpp/plugins/xep_0059',
+ 'sleekxmpp/plugins/xep_0060',
+ 'sleekxmpp/plugins/xep_0060/stanza',
+ 'sleekxmpp/plugins/xep_0066',
+ 'sleekxmpp/plugins/xep_0078',
'sleekxmpp/plugins/xep_0085',
'sleekxmpp/plugins/xep_0086',
'sleekxmpp/plugins/xep_0092',
'sleekxmpp/plugins/xep_0128',
'sleekxmpp/plugins/xep_0199',
+ 'sleekxmpp/plugins/xep_0202',
+ 'sleekxmpp/plugins/xep_0203',
+ 'sleekxmpp/plugins/xep_0224',
+ 'sleekxmpp/plugins/xep_0249',
+ 'sleekxmpp/features',
+ 'sleekxmpp/features/feature_mechanisms',
+ 'sleekxmpp/features/feature_mechanisms/stanza',
+ 'sleekxmpp/features/feature_starttls',
+ 'sleekxmpp/features/feature_bind',
+ 'sleekxmpp/features/feature_session',
+ 'sleekxmpp/thirdparty',
+ 'sleekxmpp/thirdparty/suelta',
+ 'sleekxmpp/thirdparty/suelta/mechanisms',
]
-if sys.version_info < (3, 0):
- py_modules = ['sleekxmpp.xmlstream.tostring.tostring26']
-else:
- py_modules = ['sleekxmpp.xmlstream.tostring.tostring']
-
setup(
name = "sleekxmpp",
version = VERSION,
@@ -72,11 +86,10 @@ setup(
long_description = LONG_DESCRIPTION,
author = 'Nathanael Fritz',
author_email = 'fritzy [at] netflint.net',
- url = 'http://code.google.com/p/sleekxmpp',
+ url = 'http://github.com/fritzy/SleekXMPP',
license = 'MIT',
platforms = [ 'any' ],
packages = packages,
- py_modules = py_modules,
requires = [ 'tlslite', 'pythondns' ],
)
diff --git a/sleekxmpp/__init__.py b/sleekxmpp/__init__.py
index 5ad11742..d2c014d3 100644
--- a/sleekxmpp/__init__.py
+++ b/sleekxmpp/__init__.py
@@ -15,5 +15,5 @@ from sleekxmpp.xmlstream import XMLStream, RestartStream
from sleekxmpp.xmlstream.matcher import *
from sleekxmpp.xmlstream.stanzabase import StanzaBase, ET
-__version__ = '1.0beta5'
-__version_info__ = (1, 0, 0, 'beta5', 0)
+__version__ = '1.0rc1'
+__version_info__ = (1, 0, 0, 'rc1', 0)
diff --git a/sleekxmpp/basexmpp.py b/sleekxmpp/basexmpp.py
index 3992a4f9..7c131250 100644
--- a/sleekxmpp/basexmpp.py
+++ b/sleekxmpp/basexmpp.py
@@ -92,6 +92,7 @@ class BaseXMPP(XMLStream):
# Deprecated method names are re-mapped for backwards compatibility.
self.default_ns = default_ns
self.stream_ns = 'http://etherx.jabber.org/streams'
+ self.namespace_map[self.stream_ns] = 'stream'
self.boundjid = JID("")
@@ -105,6 +106,8 @@ class BaseXMPP(XMLStream):
self.sentpresence = False
+ self.stanza = sleekxmpp.stanza
+
self.register_handler(
Callback('IM',
MatchXPath('{%s}message/{%s}body' % (self.default_ns,
@@ -135,12 +138,41 @@ class BaseXMPP(XMLStream):
register_stanza_plugin(Message, Nick)
register_stanza_plugin(Message, HTMLIM)
- def process(self, *args, **kwargs):
+ def start_stream_handler(self, xml):
+ """
+ Save the stream ID once the streams have been established.
+
+ Overrides XMLStream.start_stream_handler.
+
+ Arguments:
+ xml -- The incoming stream's root element.
"""
- Ensure that plugin inter-dependencies are handled before starting
- event processing.
+ self.stream_id = xml.get('id', '')
+ def process(self, *args, **kwargs):
+ """
Overrides XMLStream.process.
+
+ Initialize the XML streams and begin processing events.
+
+ The number of threads used for processing stream events is determined
+ by HANDLER_THREADS.
+
+ Arguments:
+ block -- If block=False then event dispatcher will run
+ in a separate thread, allowing for the stream to be
+ used in the background for another application.
+ Otherwise, process(block=True) blocks the current thread.
+ Defaults to False.
+
+ **threaded is deprecated and included for API compatibility**
+ threaded -- If threaded=True then event dispatcher will run
+ in a separate thread, allowing for the stream to be
+ used in the background for another application.
+ Defaults to True.
+
+ Event handlers and the send queue will be threaded
+ regardless of these parameters.
"""
for name in self.plugin:
if not self.plugin[name].post_inited:
@@ -162,23 +194,36 @@ class BaseXMPP(XMLStream):
try:
# Import the given module that contains the plugin.
if not module:
- module = sleekxmpp.plugins
- module = __import__("%s.%s" % (module.__name__, plugin),
- globals(), locals(), [plugin])
+ try:
+ module = sleekxmpp.plugins
+ module = __import__(
+ str("%s.%s" % (module.__name__, plugin)),
+ globals(), locals(), [str(plugin)])
+ except ImportError:
+ module = sleekxmpp.features
+ module = __import__(
+ str("%s.%s" % (module.__name__, plugin)),
+ globals(), locals(), [str(plugin)])
if isinstance(module, str):
# We probably want to load a module from outside
# the sleekxmpp package, so leave out the globals().
module = __import__(module, fromlist=[plugin])
+ # Use the global plugin config cache, if applicable
+ if not pconfig:
+ pconfig = self.plugin_config.get(plugin, {})
+
# Load the plugin class from the module.
self.plugin[plugin] = getattr(module, plugin)(self, pconfig)
- # Let XEP implementing plugins have some extra logging info.
- xep = ''
- if hasattr(self.plugin[plugin], 'xep'):
- xep = "(XEP-%s) " % self.plugin[plugin].xep
+ # Let XEP/RFC implementing plugins have some extra logging info.
+ spec = '(CUSTOM) '
+ if self.plugin[plugin].xep:
+ spec = "(XEP-%s) " % self.plugin[plugin].xep
+ elif self.plugin[plugin].rfc:
+ spec = "(RFC-%s) " % self.plugin[plugin].rfc
- desc = (xep, self.plugin[plugin].description)
+ desc = (spec, self.plugin[plugin].description)
log.debug("Loaded Plugin %s%s" % desc)
except:
log.exception("Unable to load plugin: %s", plugin)
@@ -640,7 +685,8 @@ class BaseXMPP(XMLStream):
log.debug("%s %s got offline" % (jid, resource))
del connections[resource]
- if not connections and not self.roster[jid]['in_roster']:
+ if not connections and \
+ not self.roster[jid].get('in_roster', False):
del self.roster[jid]
if not was_offline:
self.event("got_offline", presence)
diff --git a/sleekxmpp/clientxmpp.py b/sleekxmpp/clientxmpp.py
index fb5b2087..ad127726 100644
--- a/sleekxmpp/clientxmpp.py
+++ b/sleekxmpp/clientxmpp.py
@@ -15,12 +15,14 @@ import hashlib
import random
import threading
+import sleekxmpp
from sleekxmpp import plugins
from sleekxmpp import stanza
+from sleekxmpp import features
from sleekxmpp.basexmpp import BaseXMPP
-from sleekxmpp.stanza import Message, Presence, Iq
+from sleekxmpp.stanza import *
from sleekxmpp.xmlstream import XMLStream, RestartStream
-from sleekxmpp.xmlstream import StanzaBase, ET
+from sleekxmpp.xmlstream import StanzaBase, ET, register_stanza_plugin
from sleekxmpp.xmlstream.matcher import *
from sleekxmpp.xmlstream.handler import *
@@ -38,9 +40,12 @@ log = logging.getLogger(__name__)
class ClientXMPP(BaseXMPP):
"""
- SleekXMPP's client class.
+ SleekXMPP's client class. ( Use only for good, not for evil.)
- Use only for good, not for evil.
+ Typical Use:
+ xmpp = ClientXMPP('user@server.tld/resource', 'password')
+ xmpp.process(block=False) // when block is True, it blocks the current
+ // thread. False by default.
Attributes:
@@ -81,15 +86,19 @@ class ClientXMPP(BaseXMPP):
"xmlns='%s'" % self.default_ns)
self.stream_footer = "</stream:stream>"
- self.features = []
- self.registered_features = []
+ self.features = set()
+ self._stream_feature_handlers = {}
+ self._stream_feature_order = []
#TODO: Use stream state here
self.authenticated = False
self.sessionstarted = False
self.bound = False
self.bindfail = False
- self.add_event_handler('connected', self.handle_connected)
+
+ self.add_event_handler('connected', self._handle_connected)
+
+ self.register_stanza(StreamFeatures)
self.register_handler(
Callback('Stream Features',
@@ -102,32 +111,11 @@ class ClientXMPP(BaseXMPP):
'jabber:iq:roster')),
self._handle_roster))
- self.register_feature(
- "<starttls xmlns='urn:ietf:params:xml:ns:xmpp-tls' />",
- self._handle_starttls, True)
- self.register_feature(
- "<mechanisms xmlns='urn:ietf:params:xml:ns:xmpp-sasl' />",
- self._handle_sasl_auth, True)
- self.register_feature(
- "<bind xmlns='urn:ietf:params:xml:ns:xmpp-bind' />",
- self._handle_bind_resource)
- self.register_feature(
- "<session xmlns='urn:ietf:params:xml:ns:xmpp-session' />",
- self._handle_start_session)
-
- def handle_connected(self, event=None):
- #TODO: Use stream state here
- self.authenticated = False
- self.sessionstarted = False
- self.bound = False
- self.bindfail = False
- self.schedule("session timeout checker", 15,
- self._session_timeout_check)
-
- def _session_timeout_check(self):
- if not self.session_started_event.isSet():
- log.debug("Session start has taken more than 15 seconds")
- self.disconnect(reconnect=self.auto_reconnect)
+ # Setup default stream features
+ self.register_plugin('feature_starttls')
+ self.register_plugin('feature_mechanisms')
+ self.register_plugin('feature_bind')
+ self.register_plugin('feature_session')
def connect(self, address=tuple(), reattempt=True, use_tls=True):
"""
@@ -168,18 +156,23 @@ class ClientXMPP(BaseXMPP):
addresses = {}
intmax = 0
+ topprio = 65535
+ for answer in answers:
+ topprio = min(topprio, answer.priority)
for answer in answers:
- intmax += answer.priority
- addresses[intmax] = (answer.target.to_text()[:-1],
+ if answer.priority == topprio:
+ intmax += answer.weight
+ addresses[intmax] = (answer.target.to_text()[:-1],
answer.port)
+
#python3 returns a generator for dictionary keys
- priorities = [x for x in addresses.keys()]
- priorities.sort()
+ items = [x for x in addresses.keys()]
+ items.sort()
picked = random.randint(0, intmax)
- for priority in priorities:
- if picked <= priority:
- address = addresses[priority]
+ for item in items:
+ if picked <= item:
+ address = addresses[item]
break
if not address:
@@ -189,19 +182,22 @@ class ClientXMPP(BaseXMPP):
return XMLStream.connect(self, address[0], address[1],
use_tls=use_tls, reattempt=reattempt)
- def register_feature(self, mask, pointer, breaker=False):
+ def register_feature(self, name, handler, restart=False, order=5000):
"""
Register a stream feature.
Arguments:
- mask -- An XML string matching the feature's element.
- pointer -- The function to execute if the feature is received.
- breaker -- Indicates if feature processing should halt with
+ name -- The name of the stream feature.
+ handler -- The function to execute if the feature is received.
+ restart -- Indicates if feature processing should halt with
this feature. Defaults to False.
+ order -- The relative ordering in which the feature should
+ be negotiated. Lower values will be attempted
+ earlier when available.
"""
- self.registered_features.append((MatchXMLMask(mask),
- pointer,
- breaker))
+ self._stream_feature_handlers[name] = (handler, restart)
+ self._stream_feature_order.append((order, name))
+ self._stream_feature_order.sort()
def update_roster(self, jid, name=None, subscription=None, groups=[],
block=True, timeout=None, callback=None):
@@ -273,179 +269,35 @@ class ClientXMPP(BaseXMPP):
else:
return self._handle_roster(response, request=True)
- def _handle_stream_features(self, features):
- """
- Process the received stream features.
-
- Arguments:
- features -- The features stanza.
- """
- # Record all of the features.
- self.features = []
- for sub in features.xml:
- self.features.append(sub.tag)
-
- # Process the features.
- for sub in features.xml:
- for feature in self.registered_features:
- mask, handler, halt = feature
- if mask.match(sub):
- if handler(sub) and halt:
- # Don't continue if the feature was
- # marked as a breaker.
- return True
-
- def _handle_starttls(self, xml):
- """
- Handle notification that the server supports TLS.
-
- Arguments:
- xml -- The STARTLS proceed element.
- """
- if not self.use_tls:
- return False
- elif not self.authenticated and self.ssl_support:
- tls_ns = 'urn:ietf:params:xml:ns:xmpp-tls'
- self.add_handler("<proceed xmlns='%s' />" % tls_ns,
- self._handle_tls_start,
- name='TLS Proceed',
- instream=True)
- self.send_xml(xml, now=True)
- return True
- else:
- log.warning("The module tlslite is required to log in" +\
- " to some servers, and has not been found.")
- return False
-
- def _handle_tls_start(self, xml):
- """
- Handle encrypting the stream using TLS.
-
- Restarts the stream.
- """
- log.debug("Starting TLS")
- if self.start_tls():
- raise RestartStream()
-
- def _handle_sasl_auth(self, xml):
- """
- Handle authenticating using SASL.
-
- Arguments:
- xml -- The SASL mechanisms stanza.
- """
- if self.use_tls and \
- '{urn:ietf:params:xml:ns:xmpp-tls}starttls' in self.features:
- return False
-
- log.debug("Starting SASL Auth")
- sasl_ns = 'urn:ietf:params:xml:ns:xmpp-sasl'
- self.add_handler("<success xmlns='%s' />" % sasl_ns,
- self._handle_auth_success,
- name='SASL Sucess',
- instream=True)
- self.add_handler("<failure xmlns='%s' />" % sasl_ns,
- self._handle_auth_fail,
- name='SASL Failure',
- instream=True)
-
- sasl_mechs = xml.findall('{%s}mechanism' % sasl_ns)
- if sasl_mechs:
- for sasl_mech in sasl_mechs:
- self.features.append("sasl:%s" % sasl_mech.text)
- if 'sasl:PLAIN' in self.features and self.boundjid.user:
- if sys.version_info < (3, 0):
- user = bytes(self.boundjid.user)
- password = bytes(self.password)
- else:
- user = bytes(self.boundjid.user, 'utf-8')
- password = bytes(self.password, 'utf-8')
-
- auth = base64.b64encode(b'\x00' + user + \
- b'\x00' + password).decode('utf-8')
-
- self.send("<auth xmlns='%s' mechanism='PLAIN'>%s</auth>" % (
- sasl_ns,
- auth),
- now=True)
- elif 'sasl:ANONYMOUS' in self.features and not self.boundjid.user:
- self.send("<auth xmlns='%s' mechanism='%s' />" % (
- sasl_ns,
- 'ANONYMOUS'),
- now=True)
- else:
- log.error("No appropriate login method.")
- self.disconnect()
- return True
-
- def _handle_auth_success(self, xml):
- """
- SASL authentication succeeded. Restart the stream.
-
- Arguments:
- xml -- The SASL authentication success element.
- """
- self.authenticated = True
- self.features = []
- raise RestartStream()
-
- def _handle_auth_fail(self, xml):
- """
- SASL authentication failed. Disconnect and shutdown.
+ def _handle_connected(self, event=None):
+ #TODO: Use stream state here
+ self.authenticated = False
+ self.sessionstarted = False
+ self.bound = False
+ self.bindfail = False
+ self.features = set()
- Arguments:
- xml -- The SASL authentication failure element.
- """
- log.info("Authentication failed.")
- self.event("failed_auth", direct=True)
- self.disconnect()
+ def session_timeout():
+ if not self.session_started_event.isSet():
+ log.debug("Session start has taken more than 15 seconds")
+ self.disconnect(reconnect=self.auto_reconnect)
- def _handle_bind_resource(self, xml):
- """
- Handle requesting a specific resource.
+ self.schedule("session timeout checker", 15, session_timeout)
- Arguments:
- xml -- The bind feature element.
- """
- log.debug("Requesting resource: %s" % self.boundjid.resource)
- xml.clear()
- iq = self.Iq(stype='set')
- if self.boundjid.resource:
- res = ET.Element('resource')
- res.text = self.boundjid.resource
- xml.append(res)
- iq.append(xml)
- response = iq.send(now=True)
-
- bind_ns = 'urn:ietf:params:xml:ns:xmpp-bind'
- self.set_jid(response.xml.find('{%s}bind/{%s}jid' % (bind_ns,
- bind_ns)).text)
- self.bound = True
- log.info("Node set to: %s" % self.boundjid.full)
- session_ns = 'urn:ietf:params:xml:ns:xmpp-session'
- if "{%s}session" % session_ns not in self.features or self.bindfail:
- log.debug("Established Session")
- self.sessionstarted = True
- self.session_started_event.set()
- self.event("session_start")
-
- def _handle_start_session(self, xml):
+ def _handle_stream_features(self, features):
"""
- Handle the start of the session.
+ Process the received stream features.
Arguments:
- xml -- The session feature element.
+ features -- The features stanza.
"""
- if self.authenticated and self.bound:
- iq = self.makeIqSet(xml)
- response = iq.send(now=True)
- log.debug("Established Session")
- self.sessionstarted = True
- self.session_started_event.set()
- self.event("session_start")
- else:
- # Bind probably hasn't happened yet.
- self.bindfail = True
+ for order, name in self._stream_feature_order:
+ if name in features['features']:
+ handler, restart = self._stream_feature_handlers[name]
+ if handler(features) and restart:
+ # Don't continue if the feature requires
+ # restarting the XML stream.
+ return True
def _handle_roster(self, iq, request=False):
"""
diff --git a/sleekxmpp/componentxmpp.py b/sleekxmpp/componentxmpp.py
index f9e7da4d..ed96016a 100644
--- a/sleekxmpp/componentxmpp.py
+++ b/sleekxmpp/componentxmpp.py
@@ -115,11 +115,13 @@ class ComponentXMPP(BaseXMPP):
Once the streams are established, attempt to handshake
with the server to be accepted as a component.
- Overrides XMLStream.start_stream_handler.
+ Overrides BaseXMPP.start_stream_handler.
Arguments:
xml -- The incoming stream's root element.
"""
+ BaseXMPP.start_stream_handler(self, xml)
+
# Construct a hash of the stream ID and the component secret.
sid = xml.get('id', '')
pre_hash = '%s%s' % (sid, self.secret)
diff --git a/sleekxmpp/features/__init__.py b/sleekxmpp/features/__init__.py
new file mode 100644
index 00000000..5bfe173d
--- /dev/null
+++ b/sleekxmpp/features/__init__.py
@@ -0,0 +1,9 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2011 Nathanael C. Fritz
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+__all__ = ['feature_starttls', 'feature_mechanisms', 'feature_bind']
diff --git a/sleekxmpp/features/feature_bind/__init__.py b/sleekxmpp/features/feature_bind/__init__.py
new file mode 100644
index 00000000..aa854f87
--- /dev/null
+++ b/sleekxmpp/features/feature_bind/__init__.py
@@ -0,0 +1,10 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2011 Nathanael C. Fritz
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+from sleekxmpp.features.feature_bind.bind import feature_bind
+from sleekxmpp.features.feature_bind.stanza import Bind
diff --git a/sleekxmpp/features/feature_bind/bind.py b/sleekxmpp/features/feature_bind/bind.py
new file mode 100644
index 00000000..de03192c
--- /dev/null
+++ b/sleekxmpp/features/feature_bind/bind.py
@@ -0,0 +1,64 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2011 Nathanael C. Fritz
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+import logging
+
+from sleekxmpp.stanza import Iq, StreamFeatures
+from sleekxmpp.features.feature_bind import stanza
+from sleekxmpp.xmlstream import register_stanza_plugin
+from sleekxmpp.xmlstream.matcher import *
+from sleekxmpp.xmlstream.handler import *
+from sleekxmpp.plugins.base import base_plugin
+
+
+log = logging.getLogger(__name__)
+
+
+class feature_bind(base_plugin):
+
+ def plugin_init(self):
+ self.name = 'Bind Resource'
+ self.rfc = '6120'
+ self.description = 'Resource Binding Stream Feature'
+ self.stanza = stanza
+
+ self.xmpp.register_feature('bind',
+ self._handle_bind_resource,
+ restart=False,
+ order=10000)
+
+ register_stanza_plugin(Iq, stanza.Bind)
+ register_stanza_plugin(StreamFeatures, stanza.Bind)
+
+ def _handle_bind_resource(self, features):
+ """
+ Handle requesting a specific resource.
+
+ Arguments:
+ features -- The stream features stanza.
+ """
+ log.debug("Requesting resource: %s" % self.xmpp.boundjid.resource)
+ iq = self.xmpp.Iq()
+ iq['type'] = 'set'
+ iq.enable('bind')
+ if self.xmpp.boundjid.resource:
+ iq['bind']['resource'] = self.xmpp.boundjid.resource
+ response = iq.send(now=True)
+
+ self.xmpp.set_jid(response['bind']['jid'])
+ self.xmpp.bound = True
+
+ self.xmpp.features.add('bind')
+
+ log.info("Node set to: %s" % self.xmpp.boundjid.full)
+
+ if 'session' not in features['features']:
+ log.debug("Established Session")
+ self.xmpp.sessionstarted = True
+ self.xmpp.session_started_event.set()
+ self.xmpp.event("session_start")
diff --git a/sleekxmpp/features/feature_bind/stanza.py b/sleekxmpp/features/feature_bind/stanza.py
new file mode 100644
index 00000000..2c1484e0
--- /dev/null
+++ b/sleekxmpp/features/feature_bind/stanza.py
@@ -0,0 +1,22 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2011 Nathanael C. Fritz
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+from sleekxmpp.stanza import Iq, StreamFeatures
+from sleekxmpp.xmlstream import ElementBase, ET, register_stanza_plugin
+
+
+class Bind(ElementBase):
+
+ """
+ """
+
+ name = 'bind'
+ namespace = 'urn:ietf:params:xml:ns:xmpp-bind'
+ interfaces = set(('resource', 'jid'))
+ sub_interfaces = interfaces
+ plugin_attrib = 'bind'
diff --git a/sleekxmpp/features/feature_mechanisms/__init__.py b/sleekxmpp/features/feature_mechanisms/__init__.py
new file mode 100644
index 00000000..5379ef4e
--- /dev/null
+++ b/sleekxmpp/features/feature_mechanisms/__init__.py
@@ -0,0 +1,13 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2011 Nathanael C. Fritz
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+from sleekxmpp.features.feature_mechanisms.mechanisms import feature_mechanisms
+from sleekxmpp.features.feature_mechanisms.stanza import Mechanisms
+from sleekxmpp.features.feature_mechanisms.stanza import Auth
+from sleekxmpp.features.feature_mechanisms.stanza import Success
+from sleekxmpp.features.feature_mechanisms.stanza import Failure
diff --git a/sleekxmpp/features/feature_mechanisms/mechanisms.py b/sleekxmpp/features/feature_mechanisms/mechanisms.py
new file mode 100644
index 00000000..a6cff0a0
--- /dev/null
+++ b/sleekxmpp/features/feature_mechanisms/mechanisms.py
@@ -0,0 +1,129 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2011 Nathanael C. Fritz
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+import logging
+
+from sleekxmpp.thirdparty import suelta
+
+from sleekxmpp.stanza import StreamFeatures
+from sleekxmpp.xmlstream import RestartStream, register_stanza_plugin
+from sleekxmpp.xmlstream.matcher import *
+from sleekxmpp.xmlstream.handler import *
+from sleekxmpp.plugins.base import base_plugin
+from sleekxmpp.features.feature_mechanisms import stanza
+
+
+log = logging.getLogger(__name__)
+
+
+class feature_mechanisms(base_plugin):
+
+ def plugin_init(self):
+ self.name = 'SASL Mechanisms'
+ self.rfc = '6120'
+ self.description = "SASL Stream Feature"
+ self.stanza = stanza
+
+ self.use_mech = self.config.get('use_mech', None)
+
+ def tls_active():
+ return 'starttls' in self.xmpp.features
+
+ def basic_callback(mech, values):
+ if 'username' in values:
+ values['username'] = self.xmpp.boundjid.user
+ if 'password' in values:
+ values['password'] = self.xmpp.password
+ mech.fulfill(values)
+
+ sasl_callback = self.config.get('sasl_callback', None)
+ if sasl_callback is None:
+ sasl_callback = basic_callback
+
+ self.mech = None
+ self.sasl = suelta.SASL(self.xmpp.boundjid.domain, 'xmpp',
+ username=self.xmpp.boundjid.user,
+ sec_query=suelta.sec_query_allow,
+ request_values=sasl_callback,
+ tls_active=tls_active,
+ mech=self.use_mech)
+
+ register_stanza_plugin(StreamFeatures, stanza.Mechanisms)
+
+ self.xmpp.register_stanza(stanza.Success)
+ self.xmpp.register_stanza(stanza.Failure)
+ self.xmpp.register_stanza(stanza.Auth)
+ self.xmpp.register_stanza(stanza.Challenge)
+ self.xmpp.register_stanza(stanza.Response)
+
+ self.xmpp.register_handler(
+ Callback('SASL Success',
+ MatchXPath(stanza.Success.tag_name()),
+ self._handle_success,
+ instream=True,
+ once=True))
+ self.xmpp.register_handler(
+ Callback('SASL Failure',
+ MatchXPath(stanza.Failure.tag_name()),
+ self._handle_fail,
+ instream=True,
+ once=True))
+ self.xmpp.register_handler(
+ Callback('SASL Challenge',
+ MatchXPath(stanza.Challenge.tag_name()),
+ self._handle_challenge))
+
+ self.xmpp.register_feature('mechanisms',
+ self._handle_sasl_auth,
+ restart=True,
+ order=self.config.get('order', 100))
+
+ def _handle_sasl_auth(self, features):
+ """
+ Handle authenticating using SASL.
+
+ Arguments:
+ features -- The stream features stanza.
+ """
+ if 'mechanisms' in self.xmpp.features:
+ # SASL authentication has already succeeded, but the
+ # server has incorrectly offered it again.
+ return False
+
+ mech_list = features['mechanisms']
+ self.mech = self.sasl.choose_mechanism(mech_list)
+
+ if self.mech is not None:
+ resp = stanza.Auth(self.xmpp)
+ resp['mechanism'] = self.mech.name
+ resp['value'] = self.mech.process()
+ resp.send(now=True)
+ else:
+ log.error("No appropriate login method.")
+ self.xmpp.event("no_auth", direct=True)
+ self.xmpp.disconnect()
+ return True
+
+ def _handle_challenge(self, stanza):
+ """SASL challenge received. Process and send response."""
+ resp = self.stanza.Response(self.xmpp)
+ resp['value'] = self.mech.process(stanza['value'])
+ resp.send(now=True)
+
+ def _handle_success(self, stanza):
+ """SASL authentication succeeded. Restart the stream."""
+ self.xmpp.authenticated = True
+ self.xmpp.features.add('mechanisms')
+ raise RestartStream()
+
+ def _handle_fail(self, stanza):
+ """SASL authentication failed. Disconnect and shutdown."""
+ log.info("Authentication failed: %s" % stanza['condition'])
+ self.xmpp.event("failed_auth", stanza, direct=True)
+ self.xmpp.disconnect()
+ return True
diff --git a/sleekxmpp/features/feature_mechanisms/stanza/__init__.py b/sleekxmpp/features/feature_mechanisms/stanza/__init__.py
new file mode 100644
index 00000000..8b80f358
--- /dev/null
+++ b/sleekxmpp/features/feature_mechanisms/stanza/__init__.py
@@ -0,0 +1,15 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2011 Nathanael C. Fritz
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+
+from sleekxmpp.features.feature_mechanisms.stanza.mechanisms import Mechanisms
+from sleekxmpp.features.feature_mechanisms.stanza.auth import Auth
+from sleekxmpp.features.feature_mechanisms.stanza.success import Success
+from sleekxmpp.features.feature_mechanisms.stanza.failure import Failure
+from sleekxmpp.features.feature_mechanisms.stanza.challenge import Challenge
+from sleekxmpp.features.feature_mechanisms.stanza.response import Response
diff --git a/sleekxmpp/features/feature_mechanisms/stanza/auth.py b/sleekxmpp/features/feature_mechanisms/stanza/auth.py
new file mode 100644
index 00000000..e069b57f
--- /dev/null
+++ b/sleekxmpp/features/feature_mechanisms/stanza/auth.py
@@ -0,0 +1,39 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2011 Nathanael C. Fritz
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+import base64
+
+from sleekxmpp.thirdparty.suelta.util import bytes
+
+from sleekxmpp.stanza import StreamFeatures
+from sleekxmpp.xmlstream import ElementBase, StanzaBase, ET
+from sleekxmpp.xmlstream import register_stanza_plugin
+
+
+class Auth(StanzaBase):
+
+ """
+ """
+
+ name = 'auth'
+ namespace = 'urn:ietf:params:xml:ns:xmpp-sasl'
+ interfaces = set(('mechanism', 'value'))
+ plugin_attrib = name
+
+ def setup(self, xml):
+ StanzaBase.setup(self, xml)
+ self.xml.tag = self.tag_name()
+
+ def get_value(self):
+ return base64.b64decode(bytes(self.xml.text))
+
+ def set_value(self, values):
+ self.xml.text = bytes(base64.b64encode(values)).decode('utf-8')
+
+ def del_value(self):
+ self.xml.text = ''
diff --git a/sleekxmpp/features/feature_mechanisms/stanza/challenge.py b/sleekxmpp/features/feature_mechanisms/stanza/challenge.py
new file mode 100644
index 00000000..82af869f
--- /dev/null
+++ b/sleekxmpp/features/feature_mechanisms/stanza/challenge.py
@@ -0,0 +1,39 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2011 Nathanael C. Fritz
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+import base64
+
+from sleekxmpp.thirdparty.suelta.util import bytes
+
+from sleekxmpp.stanza import StreamFeatures
+from sleekxmpp.xmlstream import ElementBase, StanzaBase, ET
+from sleekxmpp.xmlstream import register_stanza_plugin
+
+
+class Challenge(StanzaBase):
+
+ """
+ """
+
+ name = 'challenge'
+ namespace = 'urn:ietf:params:xml:ns:xmpp-sasl'
+ interfaces = set(('value',))
+ plugin_attrib = name
+
+ def setup(self, xml):
+ StanzaBase.setup(self, xml)
+ self.xml.tag = self.tag_name()
+
+ def get_value(self):
+ return base64.b64decode(bytes(self.xml.text))
+
+ def set_value(self, values):
+ self.xml.text = bytes(base64.b64encode(values)).decode('utf-8')
+
+ def del_value(self):
+ self.xml.text = ''
diff --git a/sleekxmpp/features/feature_mechanisms/stanza/failure.py b/sleekxmpp/features/feature_mechanisms/stanza/failure.py
new file mode 100644
index 00000000..027cc5af
--- /dev/null
+++ b/sleekxmpp/features/feature_mechanisms/stanza/failure.py
@@ -0,0 +1,78 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2011 Nathanael C. Fritz
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+from sleekxmpp.stanza import StreamFeatures
+from sleekxmpp.xmlstream import ElementBase, StanzaBase, ET
+from sleekxmpp.xmlstream import register_stanza_plugin
+
+
+class Failure(StanzaBase):
+
+ """
+ """
+
+ name = 'failure'
+ namespace = 'urn:ietf:params:xml:ns:xmpp-sasl'
+ interfaces = set(('condition', 'text'))
+ plugin_attrib = name
+ sub_interfaces = set(('text',))
+ conditions = set(('aborted', 'account-disabled', 'credentials-expired',
+ 'encryption-required', 'incorrect-encoding', 'invalid-authzid',
+ 'invalid-mechanism', 'malformed-request', 'mechansism-too-weak',
+ 'not-authorized', 'temporary-auth-failure'))
+
+ def setup(self, xml=None):
+ """
+ Populate the stanza object using an optional XML object.
+
+ Overrides ElementBase.setup.
+
+ Sets a default error type and condition, and changes the
+ parent stanza's type to 'error'.
+
+ Arguments:
+ xml -- Use an existing XML object for the stanza's values.
+ """
+ # StanzaBase overrides self.namespace
+ self.namespace = Failure.namespace
+
+ if StanzaBase.setup(self, xml):
+ #If we had to generate XML then set default values.
+ self['condition'] = 'not-authorized'
+
+ self.xml.tag = self.tag_name()
+
+ def get_condition(self):
+ """Return the condition element's name."""
+ for child in self.xml.getchildren():
+ if "{%s}" % self.namespace in child.tag:
+ cond = child.tag.split('}', 1)[-1]
+ if cond in self.conditions:
+ return cond
+ return 'not-authorized'
+
+ def set_condition(self, value):
+ """
+ Set the tag name of the condition element.
+
+ Arguments:
+ value -- The tag name of the condition element.
+ """
+ if value in self.conditions:
+ del self['condition']
+ self.xml.append(ET.Element("{%s}%s" % (self.namespace, value)))
+ return self
+
+ def del_condition(self):
+ """Remove the condition element."""
+ for child in self.xml.getchildren():
+ if "{%s}" % self.condition_ns in child.tag:
+ tag = child.tag.split('}', 1)[-1]
+ if tag in self.conditions:
+ self.xml.remove(child)
+ return self
diff --git a/sleekxmpp/features/feature_mechanisms/stanza/mechanisms.py b/sleekxmpp/features/feature_mechanisms/stanza/mechanisms.py
new file mode 100644
index 00000000..c09cafbd
--- /dev/null
+++ b/sleekxmpp/features/feature_mechanisms/stanza/mechanisms.py
@@ -0,0 +1,55 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2011 Nathanael C. Fritz
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+from sleekxmpp.stanza import StreamFeatures
+from sleekxmpp.xmlstream import ElementBase, StanzaBase, ET
+from sleekxmpp.xmlstream import register_stanza_plugin
+
+
+class Mechanisms(ElementBase):
+
+ """
+ """
+
+ name = 'mechanisms'
+ namespace = 'urn:ietf:params:xml:ns:xmpp-sasl'
+ interfaces = set(('mechanisms', 'required'))
+ plugin_attrib = name
+ is_extension = True
+
+ def get_required(self):
+ """
+ """
+ return True
+
+ def get_mechanisms(self):
+ """
+ """
+ results = []
+ mechs = self.findall('{%s}mechanism' % self.namespace)
+ if mechs:
+ for mech in mechs:
+ results.append(mech.text)
+ return results
+
+ def set_mechanisms(self, values):
+ """
+ """
+ self.del_mechanisms()
+ for val in values:
+ mech = ET.Element('{%s}mechanism' % self.namespace)
+ mech.text = val
+ self.append(mech)
+
+ def del_mechanisms(self):
+ """
+ """
+ mechs = self.findall('{%s}mechanism' % self.namespace)
+ if mechs:
+ for mech in mechs:
+ self.xml.remove(mech)
diff --git a/sleekxmpp/features/feature_mechanisms/stanza/response.py b/sleekxmpp/features/feature_mechanisms/stanza/response.py
new file mode 100644
index 00000000..45bb8207
--- /dev/null
+++ b/sleekxmpp/features/feature_mechanisms/stanza/response.py
@@ -0,0 +1,39 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2011 Nathanael C. Fritz
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+import base64
+
+from sleekxmpp.thirdparty.suelta.util import bytes
+
+from sleekxmpp.stanza import StreamFeatures
+from sleekxmpp.xmlstream import ElementBase, StanzaBase, ET
+from sleekxmpp.xmlstream import register_stanza_plugin
+
+
+class Response(StanzaBase):
+
+ """
+ """
+
+ name = 'response'
+ namespace = 'urn:ietf:params:xml:ns:xmpp-sasl'
+ interfaces = set(('value',))
+ plugin_attrib = name
+
+ def setup(self, xml):
+ StanzaBase.setup(self, xml)
+ self.xml.tag = self.tag_name()
+
+ def get_value(self):
+ return base64.b64decode(bytes(self.xml.text))
+
+ def set_value(self, values):
+ self.xml.text = bytes(base64.b64encode(values)).decode('utf-8')
+
+ def del_value(self):
+ self.xml.text = ''
diff --git a/sleekxmpp/features/feature_mechanisms/stanza/success.py b/sleekxmpp/features/feature_mechanisms/stanza/success.py
new file mode 100644
index 00000000..028e28a3
--- /dev/null
+++ b/sleekxmpp/features/feature_mechanisms/stanza/success.py
@@ -0,0 +1,26 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2011 Nathanael C. Fritz
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+from sleekxmpp.stanza import StreamFeatures
+from sleekxmpp.xmlstream import ElementBase, StanzaBase, ET
+from sleekxmpp.xmlstream import register_stanza_plugin
+
+
+class Success(StanzaBase):
+
+ """
+ """
+
+ name = 'success'
+ namespace = 'urn:ietf:params:xml:ns:xmpp-sasl'
+ interfaces = set()
+ plugin_attrib = name
+
+ def setup(self, xml):
+ StanzaBase.setup(self, xml)
+ self.xml.tag = self.tag_name()
diff --git a/sleekxmpp/features/feature_session/__init__.py b/sleekxmpp/features/feature_session/__init__.py
new file mode 100644
index 00000000..3c84baed
--- /dev/null
+++ b/sleekxmpp/features/feature_session/__init__.py
@@ -0,0 +1,10 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2011 Nathanael C. Fritz
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+from sleekxmpp.features.feature_session.session import feature_session
+from sleekxmpp.features.feature_session.stanza import Session
diff --git a/sleekxmpp/features/feature_session/session.py b/sleekxmpp/features/feature_session/session.py
new file mode 100644
index 00000000..0daec5da
--- /dev/null
+++ b/sleekxmpp/features/feature_session/session.py
@@ -0,0 +1,56 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2011 Nathanael C. Fritz
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+import logging
+
+from sleekxmpp.stanza import Iq, StreamFeatures
+from sleekxmpp.xmlstream import register_stanza_plugin
+from sleekxmpp.xmlstream.matcher import *
+from sleekxmpp.xmlstream.handler import *
+from sleekxmpp.plugins.base import base_plugin
+
+from sleekxmpp.features.feature_session import stanza
+
+
+log = logging.getLogger(__name__)
+
+
+class feature_session(base_plugin):
+
+ def plugin_init(self):
+ self.name = 'Start Session'
+ self.rfc = '3920'
+ self.description = 'Start Session Stream Feature'
+ self.stanza = stanza
+
+ self.xmpp.register_feature('session',
+ self._handle_start_session,
+ restart=False,
+ order=10001)
+
+ register_stanza_plugin(Iq, stanza.Session)
+ register_stanza_plugin(StreamFeatures, stanza.Session)
+
+ def _handle_start_session(self, features):
+ """
+ Handle the start of the session.
+
+ Arguments:
+ feature -- The stream features element.
+ """
+ iq = self.xmpp.Iq()
+ iq['type'] = 'set'
+ iq.enable('session')
+ response = iq.send(now=True)
+
+ self.xmpp.features.add('session')
+
+ log.debug("Established Session")
+ self.xmpp.sessionstarted = True
+ self.xmpp.session_started_event.set()
+ self.xmpp.event("session_start")
diff --git a/sleekxmpp/features/feature_session/stanza.py b/sleekxmpp/features/feature_session/stanza.py
new file mode 100644
index 00000000..40ea583d
--- /dev/null
+++ b/sleekxmpp/features/feature_session/stanza.py
@@ -0,0 +1,21 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2011 Nathanael C. Fritz
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+from sleekxmpp.stanza import Iq, StreamFeatures
+from sleekxmpp.xmlstream import ElementBase, ET, register_stanza_plugin
+
+
+class Session(ElementBase):
+
+ """
+ """
+
+ name = 'session'
+ namespace = 'urn:ietf:params:xml:ns:xmpp-session'
+ interfaces = set()
+ plugin_attrib = 'session'
diff --git a/sleekxmpp/features/feature_starttls/__init__.py b/sleekxmpp/features/feature_starttls/__init__.py
new file mode 100644
index 00000000..4ae89433
--- /dev/null
+++ b/sleekxmpp/features/feature_starttls/__init__.py
@@ -0,0 +1,10 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2011 Nathanael C. Fritz
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+from sleekxmpp.features.feature_starttls.starttls import feature_starttls
+from sleekxmpp.features.feature_starttls.stanza import *
diff --git a/sleekxmpp/features/feature_starttls/stanza.py b/sleekxmpp/features/feature_starttls/stanza.py
new file mode 100644
index 00000000..8b09ad94
--- /dev/null
+++ b/sleekxmpp/features/feature_starttls/stanza.py
@@ -0,0 +1,47 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2011 Nathanael C. Fritz
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+from sleekxmpp.stanza import StreamFeatures
+from sleekxmpp.xmlstream import StanzaBase, ElementBase
+from sleekxmpp.xmlstream import register_stanza_plugin
+
+
+class STARTTLS(ElementBase):
+
+ """
+ """
+
+ name = 'starttls'
+ namespace = 'urn:ietf:params:xml:ns:xmpp-tls'
+ interfaces = set(('required',))
+ plugin_attrib = name
+
+ def get_required(self):
+ """
+ """
+ return True
+
+
+class Proceed(StanzaBase):
+
+ """
+ """
+
+ name = 'proceed'
+ namespace = 'urn:ietf:params:xml:ns:xmpp-tls'
+ interfaces = set()
+
+
+class Failure(StanzaBase):
+
+ """
+ """
+
+ name = 'failure'
+ namespace = 'urn:ietf:params:xml:ns:xmpp-tls'
+ interfaces = set()
diff --git a/sleekxmpp/features/feature_starttls/starttls.py b/sleekxmpp/features/feature_starttls/starttls.py
new file mode 100644
index 00000000..639788a0
--- /dev/null
+++ b/sleekxmpp/features/feature_starttls/starttls.py
@@ -0,0 +1,70 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2011 Nathanael C. Fritz
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+import logging
+
+from sleekxmpp.stanza import StreamFeatures
+from sleekxmpp.xmlstream import RestartStream, register_stanza_plugin
+from sleekxmpp.xmlstream.matcher import *
+from sleekxmpp.xmlstream.handler import *
+from sleekxmpp.plugins.base import base_plugin
+from sleekxmpp.features.feature_starttls import stanza
+
+
+log = logging.getLogger(__name__)
+
+
+class feature_starttls(base_plugin):
+
+ def plugin_init(self):
+ self.name = "STARTTLS"
+ self.rfc = '6120'
+ self.description = "STARTTLS Stream Feature"
+ self.stanza = stanza
+
+ self.xmpp.register_handler(
+ Callback('STARTTLS Proceed',
+ MatchXPath(stanza.Proceed.tag_name()),
+ self._handle_starttls_proceed,
+ instream=True))
+ self.xmpp.register_feature('starttls',
+ self._handle_starttls,
+ restart=True,
+ order=self.config.get('order', 0))
+
+ self.xmpp.register_stanza(stanza.Proceed)
+ self.xmpp.register_stanza(stanza.Failure)
+ register_stanza_plugin(StreamFeatures, stanza.STARTTLS)
+
+ def _handle_starttls(self, features):
+ """
+ Handle notification that the server supports TLS.
+
+ Arguments:
+ features -- The stream:features element.
+ """
+ if 'starttls' in self.xmpp.features:
+ # We have already negotiated TLS, but the server is
+ # offering it again, against spec.
+ return False
+ elif not self.xmpp.use_tls:
+ return False
+ elif self.xmpp.ssl_support:
+ self.xmpp.send(features['starttls'], now=True)
+ return True
+ else:
+ log.warning("The module tlslite is required to log in" +\
+ " to some servers, and has not been found.")
+ return False
+
+ def _handle_starttls_proceed(self, proceed):
+ """Restart the XML stream when TLS is accepted."""
+ log.debug("Starting TLS")
+ if self.xmpp.start_tls():
+ self.xmpp.features.add('starttls')
+ raise RestartStream()
diff --git a/sleekxmpp/plugins/__init__.py b/sleekxmpp/plugins/__init__.py
index d27937ae..c0b1121b 100644
--- a/sleekxmpp/plugins/__init__.py
+++ b/sleekxmpp/plugins/__init__.py
@@ -6,5 +6,8 @@
See the file LICENSE for copying permission.
"""
__all__ = ['xep_0004', 'xep_0009', 'xep_0012', 'xep_0030', 'xep_0033',
- 'xep_0045', 'xep_0050', 'xep_0060', 'xep_0085', 'xep_0086',
- 'xep_0092', 'xep_0128', 'xep_0199', 'xep_0202', 'gmail_notify']
+ 'xep_0045', 'xep_0050', 'xep_0060', 'xep_0066', 'xep_0082',
+ 'xep_0085', 'xep_0086', 'xep_0092', 'xep_0128', 'xep_0199',
+ 'xep_0203', 'xep_0224', 'xep_0249', 'gmail_notify']
+
+# Don't automatically load xep_0078
diff --git a/sleekxmpp/plugins/base.py b/sleekxmpp/plugins/base.py
index 2dd68c8d..561421d8 100644
--- a/sleekxmpp/plugins/base.py
+++ b/sleekxmpp/plugins/base.py
@@ -66,7 +66,8 @@ class base_plugin(object):
"""
if config is None:
config = {}
- self.xep = 'base'
+ self.xep = None
+ self.rfc = None
self.description = 'Base Plugin'
self.xmpp = xmpp
self.config = config
diff --git a/sleekxmpp/plugins/xep_0060.py b/sleekxmpp/plugins/old_0060.py
index 93124fca..93124fca 100644
--- a/sleekxmpp/plugins/xep_0060.py
+++ b/sleekxmpp/plugins/old_0060.py
diff --git a/sleekxmpp/plugins/stanza_pubsub.py b/sleekxmpp/plugins/stanza_pubsub.py
deleted file mode 100644
index b5964537..00000000
--- a/sleekxmpp/plugins/stanza_pubsub.py
+++ /dev/null
@@ -1,557 +0,0 @@
-from .. xmlstream.stanzabase import registerStanzaPlugin, ElementBase, ET, JID
-from .. stanza.iq import Iq
-from .. stanza.message import Message
-from .. basexmpp import basexmpp
-from .. xmlstream.xmlstream import XMLStream
-import logging
-from . import xep_0004
-
-
-class PubsubState(ElementBase):
- namespace = 'http://jabber.org/protocol/psstate'
- name = 'state'
- plugin_attrib = 'psstate'
- interfaces = set(('node', 'item', 'payload'))
- plugin_attrib_map = {}
- plugin_tag_map = {}
-
- def setPayload(self, value):
- self.xml.append(value)
-
- def getPayload(self):
- childs = self.xml.getchildren()
- if len(childs) > 0:
- return childs[0]
-
- def delPayload(self):
- for child in self.xml.getchildren():
- self.xml.remove(child)
-
-registerStanzaPlugin(Iq, PubsubState)
-
-class PubsubStateEvent(ElementBase):
- namespace = 'http://jabber.org/protocol/psstate#event'
- name = 'event'
- plugin_attrib = 'psstate_event'
- intefaces = set(tuple())
- plugin_attrib_map = {}
- plugin_tag_map = {}
-
-registerStanzaPlugin(Message, PubsubStateEvent)
-registerStanzaPlugin(PubsubStateEvent, PubsubState)
-
-class Pubsub(ElementBase):
- namespace = 'http://jabber.org/protocol/pubsub'
- name = 'pubsub'
- plugin_attrib = 'pubsub'
- interfaces = set(tuple())
- plugin_attrib_map = {}
- plugin_tag_map = {}
-
-registerStanzaPlugin(Iq, Pubsub)
-
-class PubsubOwner(ElementBase):
- namespace = 'http://jabber.org/protocol/pubsub#owner'
- name = 'pubsub'
- plugin_attrib = 'pubsub_owner'
- interfaces = set(tuple())
- plugin_attrib_map = {}
- plugin_tag_map = {}
-
-registerStanzaPlugin(Iq, PubsubOwner)
-
-class Affiliation(ElementBase):
- namespace = 'http://jabber.org/protocol/pubsub'
- name = 'affiliation'
- plugin_attrib = name
- interfaces = set(('node', 'affiliation'))
- plugin_attrib_map = {}
- plugin_tag_map = {}
-
-class Affiliations(ElementBase):
- namespace = 'http://jabber.org/protocol/pubsub'
- name = 'affiliations'
- plugin_attrib = 'affiliations'
- interfaces = set(tuple())
- plugin_attrib_map = {}
- plugin_tag_map = {}
- subitem = (Affiliation,)
-
- def append(self, affiliation):
- if not isinstance(affiliation, Affiliation):
- raise TypeError
- self.xml.append(affiliation.xml)
- return self.iterables.append(affiliation)
-
-registerStanzaPlugin(Pubsub, Affiliations)
-
-
-class Subscription(ElementBase):
- namespace = 'http://jabber.org/protocol/pubsub'
- name = 'subscription'
- plugin_attrib = name
- interfaces = set(('jid', 'node', 'subscription', 'subid'))
- plugin_attrib_map = {}
- plugin_tag_map = {}
-
- def setjid(self, value):
- self._setattr('jid', str(value))
-
- def getjid(self):
- return jid(self._getattr('jid'))
-
-registerStanzaPlugin(Pubsub, Subscription)
-
-class Subscriptions(ElementBase):
- namespace = 'http://jabber.org/protocol/pubsub'
- name = 'subscriptions'
- plugin_attrib = 'subscriptions'
- interfaces = set(tuple())
- plugin_attrib_map = {}
- plugin_tag_map = {}
- subitem = (Subscription,)
-
-registerStanzaPlugin(Pubsub, Subscriptions)
-
-class OptionalSetting(object):
- interfaces = set(('required',))
-
- def setRequired(self, value):
- value = bool(value)
- if value and not self['required']:
- self.xml.append(ET.Element("{%s}required" % self.namespace))
- elif not value and self['required']:
- self.delRequired()
-
- def getRequired(self):
- required = self.xml.find("{%s}required" % self.namespace)
- if required is not None:
- return True
- else:
- return False
-
- def delRequired(self):
- required = self.xml.find("{%s}required" % self.namespace)
- if required is not None:
- self.xml.remove(required)
-
-
-class SubscribeOptions(ElementBase, OptionalSetting):
- namespace = 'http://jabber.org/protocol/pubsub'
- name = 'subscribe-options'
- plugin_attrib = 'suboptions'
- plugin_attrib_map = {}
- plugin_tag_map = {}
- interfaces = set(('required',))
-
-registerStanzaPlugin(Subscription, SubscribeOptions)
-
-class Item(ElementBase):
- namespace = 'http://jabber.org/protocol/pubsub'
- name = 'item'
- plugin_attrib = name
- interfaces = set(('id', 'payload'))
- plugin_attrib_map = {}
- plugin_tag_map = {}
-
- def setPayload(self, value):
- self.xml.append(value)
-
- def getPayload(self):
- childs = self.xml.getchildren()
- if len(childs) > 0:
- return childs[0]
-
- def delPayload(self):
- for child in self.xml.getchildren():
- self.xml.remove(child)
-
-class Items(ElementBase):
- namespace = 'http://jabber.org/protocol/pubsub'
- name = 'items'
- plugin_attrib = 'items'
- interfaces = set(('node',))
- plugin_attrib_map = {}
- plugin_tag_map = {}
- subitem = (Item,)
-
-registerStanzaPlugin(Pubsub, Items)
-
-class Create(ElementBase):
- namespace = 'http://jabber.org/protocol/pubsub'
- name = 'create'
- plugin_attrib = name
- interfaces = set(('node',))
- plugin_attrib_map = {}
- plugin_tag_map = {}
-
-registerStanzaPlugin(Pubsub, Create)
-
-#class Default(ElementBase):
-# namespace = 'http://jabber.org/protocol/pubsub'
-# name = 'default'
-# plugin_attrib = name
-# interfaces = set(('node', 'type'))
-# plugin_attrib_map = {}
-# plugin_tag_map = {}
-#
-# def getType(self):
-# t = self._getAttr('type')
-# if not t: t == 'leaf'
-# return t
-#
-#registerStanzaPlugin(Pubsub, Default)
-
-class Publish(Items):
- namespace = 'http://jabber.org/protocol/pubsub'
- name = 'publish'
- plugin_attrib = name
- interfaces = set(('node',))
- plugin_attrib_map = {}
- plugin_tag_map = {}
- subitem = (Item,)
-
-registerStanzaPlugin(Pubsub, Publish)
-
-class Retract(Items):
- namespace = 'http://jabber.org/protocol/pubsub'
- name = 'retract'
- plugin_attrib = name
- interfaces = set(('node', 'notify'))
- plugin_attrib_map = {}
- plugin_tag_map = {}
-
-registerStanzaPlugin(Pubsub, Retract)
-
-class Unsubscribe(ElementBase):
- namespace = 'http://jabber.org/protocol/pubsub'
- name = 'unsubscribe'
- plugin_attrib = name
- interfaces = set(('node', 'jid'))
- plugin_attrib_map = {}
- plugin_tag_map = {}
-
- def setJid(self, value):
- self._setAttr('jid', str(value))
-
- def getJid(self):
- return JID(self._getAttr('jid'))
-
-registerStanzaPlugin(Pubsub, Unsubscribe)
-
-class Subscribe(ElementBase):
- namespace = 'http://jabber.org/protocol/pubsub'
- name = 'subscribe'
- plugin_attrib = name
- interfaces = set(('node', 'jid'))
- plugin_attrib_map = {}
- plugin_tag_map = {}
-
- def setJid(self, value):
- self._setAttr('jid', str(value))
-
- def getJid(self):
- return JID(self._getAttr('jid'))
-
-registerStanzaPlugin(Pubsub, Subscribe)
-
-class Configure(ElementBase):
- namespace = 'http://jabber.org/protocol/pubsub'
- name = 'configure'
- plugin_attrib = name
- interfaces = set(('node', 'type'))
- plugin_attrib_map = {}
- plugin_tag_map = {}
-
- def getType(self):
- t = self._getAttr('type')
- if not t: t == 'leaf'
- return t
-
-registerStanzaPlugin(Pubsub, Configure)
-registerStanzaPlugin(Configure, xep_0004.Form)
-
-class DefaultConfig(ElementBase):
- namespace = 'http://jabber.org/protocol/pubsub#owner'
- name = 'default'
- plugin_attrib = 'default'
- interfaces = set(('node', 'type', 'config'))
- plugin_attrib_map = {}
- plugin_tag_map = {}
-
- def __init__(self, *args, **kwargs):
- ElementBase.__init__(self, *args, **kwargs)
-
- def getType(self):
- t = self._getAttr('type')
- if not t: t = 'leaf'
- return t
-
- def getConfig(self):
- return self['form']
-
- def setConfig(self, value):
- self['form'].setStanzaValues(value.getStanzaValues())
- return self
-
-registerStanzaPlugin(PubsubOwner, DefaultConfig)
-registerStanzaPlugin(DefaultConfig, xep_0004.Form)
-
-class Options(ElementBase):
- namespace = 'http://jabber.org/protocol/pubsub'
- name = 'options'
- plugin_attrib = 'options'
- interfaces = set(('jid', 'node', 'options'))
- plugin_attrib_map = {}
- plugin_tag_map = {}
-
- def __init__(self, *args, **kwargs):
- ElementBase.__init__(self, *args, **kwargs)
-
- def getOptions(self):
- config = self.xml.find('{jabber:x:data}x')
- form = xep_0004.Form()
- if config is not None:
- form.fromXML(config)
- return form
-
- def setOptions(self, value):
- self.xml.append(value.getXML())
- return self
-
- def delOptions(self):
- config = self.xml.find('{jabber:x:data}x')
- self.xml.remove(config)
-
- def setJid(self, value):
- self._setAttr('jid', str(value))
-
- def getJid(self):
- return JID(self._getAttr('jid'))
-
-registerStanzaPlugin(Pubsub, Options)
-registerStanzaPlugin(Subscribe, Options)
-
-class OwnerAffiliations(Affiliations):
- namespace = 'http://jabber.org/protocol/pubsub#owner'
- interfaces = set(('node'))
- plugin_attrib_map = {}
- plugin_tag_map = {}
-
- def append(self, affiliation):
- if not isinstance(affiliation, OwnerAffiliation):
- raise TypeError
- self.xml.append(affiliation.xml)
- return self.affiliations.append(affiliation)
-
-registerStanzaPlugin(PubsubOwner, OwnerAffiliations)
-
-class OwnerAffiliation(Affiliation):
- namespace = 'http://jabber.org/protocol/pubsub#owner'
- interfaces = set(('affiliation', 'jid'))
- plugin_attrib_map = {}
- plugin_tag_map = {}
-
-class OwnerConfigure(Configure):
- namespace = 'http://jabber.org/protocol/pubsub#owner'
- interfaces = set(('node', 'config'))
- plugin_attrib_map = {}
- plugin_tag_map = {}
-
-registerStanzaPlugin(PubsubOwner, OwnerConfigure)
-
-class OwnerDefault(OwnerConfigure):
- namespace = 'http://jabber.org/protocol/pubsub#owner'
- interfaces = set(('node', 'config'))
- plugin_attrib_map = {}
- plugin_tag_map = {}
-
- def getConfig(self):
- return self['form']
-
- def setConfig(self, value):
- self['form'].setStanzaValues(value.getStanzaValues())
- return self
-
-registerStanzaPlugin(PubsubOwner, OwnerDefault)
-registerStanzaPlugin(OwnerDefault, xep_0004.Form)
-
-class OwnerDelete(ElementBase, OptionalSetting):
- namespace = 'http://jabber.org/protocol/pubsub#owner'
- name = 'delete'
- plugin_attrib = 'delete'
- plugin_attrib_map = {}
- plugin_tag_map = {}
- interfaces = set(('node',))
-
-registerStanzaPlugin(PubsubOwner, OwnerDelete)
-
-class OwnerPurge(ElementBase, OptionalSetting):
- namespace = 'http://jabber.org/protocol/pubsub#owner'
- name = 'purge'
- plugin_attrib = name
- plugin_attrib_map = {}
- plugin_tag_map = {}
-
-registerStanzaPlugin(PubsubOwner, OwnerPurge)
-
-class OwnerRedirect(ElementBase):
- namespace = 'http://jabber.org/protocol/pubsub#owner'
- name = 'redirect'
- plugin_attrib = name
- interfaces = set(('node', 'jid'))
- plugin_attrib_map = {}
- plugin_tag_map = {}
-
- def setJid(self, value):
- self._setAttr('jid', str(value))
-
- def getJid(self):
- return JID(self._getAttr('jid'))
-
-registerStanzaPlugin(OwnerDelete, OwnerRedirect)
-
-class OwnerSubscriptions(Subscriptions):
- namespace = 'http://jabber.org/protocol/pubsub#owner'
- interfaces = set(('node',))
- plugin_attrib_map = {}
- plugin_tag_map = {}
-
- def append(self, subscription):
- if not isinstance(subscription, OwnerSubscription):
- raise TypeError
- self.xml.append(subscription.xml)
- return self.subscriptions.append(subscription)
-
-registerStanzaPlugin(PubsubOwner, OwnerSubscriptions)
-
-class OwnerSubscription(ElementBase):
- namespace = 'http://jabber.org/protocol/pubsub#owner'
- name = 'subscription'
- plugin_attrib = name
- interfaces = set(('jid', 'subscription'))
- plugin_attrib_map = {}
- plugin_tag_map = {}
-
- def setJid(self, value):
- self._setAttr('jid', str(value))
-
- def getJid(self):
- return JID(self._getAttr('from'))
-
-class Event(ElementBase):
- namespace = 'http://jabber.org/protocol/pubsub#event'
- name = 'event'
- plugin_attrib = 'pubsub_event'
- interfaces = set(('node',))
- plugin_attrib_map = {}
- plugin_tag_map = {}
-
-registerStanzaPlugin(Message, Event)
-
-class EventItem(ElementBase):
- namespace = 'http://jabber.org/protocol/pubsub#event'
- name = 'item'
- plugin_attrib = 'item'
- interfaces = set(('id', 'payload'))
- plugin_attrib_map = {}
- plugin_tag_map = {}
-
- def setPayload(self, value):
- self.xml.append(value)
-
- def getPayload(self):
- childs = self.xml.getchildren()
- if len(childs) > 0:
- return childs[0]
-
- def delPayload(self):
- for child in self.xml.getchildren():
- self.xml.remove(child)
-
-
-class EventRetract(ElementBase):
- namespace = 'http://jabber.org/protocol/pubsub#event'
- name = 'retract'
- plugin_attrib = 'retract'
- interfaces = set(('id',))
- plugin_attrib_map = {}
- plugin_tag_map = {}
-
-class EventItems(ElementBase):
- namespace = 'http://jabber.org/protocol/pubsub#event'
- name = 'items'
- plugin_attrib = 'items'
- interfaces = set(('node',))
- plugin_attrib_map = {}
- plugin_tag_map = {}
- subitem = (EventItem, EventRetract)
-
-registerStanzaPlugin(Event, EventItems)
-
-class EventCollection(ElementBase):
- namespace = 'http://jabber.org/protocol/pubsub#event'
- name = 'collection'
- plugin_attrib = name
- interfaces = set(('node',))
- plugin_attrib_map = {}
- plugin_tag_map = {}
-
-registerStanzaPlugin(Event, EventCollection)
-
-class EventAssociate(ElementBase):
- namespace = 'http://jabber.org/protocol/pubsub#event'
- name = 'associate'
- plugin_attrib = name
- interfaces = set(('node',))
- plugin_attrib_map = {}
- plugin_tag_map = {}
-
-registerStanzaPlugin(EventCollection, EventAssociate)
-
-class EventDisassociate(ElementBase):
- namespace = 'http://jabber.org/protocol/pubsub#event'
- name = 'disassociate'
- plugin_attrib = name
- interfaces = set(('node',))
- plugin_attrib_map = {}
- plugin_tag_map = {}
-
-registerStanzaPlugin(EventCollection, EventDisassociate)
-
-class EventConfiguration(ElementBase):
- namespace = 'http://jabber.org/protocol/pubsub#event'
- name = 'configuration'
- plugin_attrib = name
- interfaces = set(('node', 'config'))
- plugin_attrib_map = {}
- plugin_tag_map = {}
-
-registerStanzaPlugin(Event, EventConfiguration)
-registerStanzaPlugin(EventConfiguration, xep_0004.Form)
-
-class EventPurge(ElementBase):
- namespace = 'http://jabber.org/protocol/pubsub#event'
- name = 'purge'
- plugin_attrib = name
- interfaces = set(('node',))
- plugin_attrib_map = {}
- plugin_tag_map = {}
-
-registerStanzaPlugin(Event, EventPurge)
-
-class EventSubscription(ElementBase):
- namespace = 'http://jabber.org/protocol/pubsub#event'
- name = 'subscription'
- plugin_attrib = name
- interfaces = set(('node','expiry', 'jid', 'subid', 'subscription'))
- plugin_attrib_map = {}
- plugin_tag_map = {}
-
- def setJid(self, value):
- self._setAttr('jid', str(value))
-
- def getJid(self):
- return JID(self._getAttr('jid'))
-
-registerStanzaPlugin(Event, EventSubscription)
diff --git a/sleekxmpp/plugins/xep_0009/remote.py b/sleekxmpp/plugins/xep_0009/remote.py
index 8c534118..b5d10b85 100644
--- a/sleekxmpp/plugins/xep_0009/remote.py
+++ b/sleekxmpp/plugins/xep_0009/remote.py
@@ -463,7 +463,7 @@ class RemoteSession(object):
key = "%s.%s" % (endpoint, name)
log.debug("Registering call handler for %s (%s)." % (key, method))
with self._lock:
- if self._entries.has_key(key):
+ if key in self._entries:
raise KeyError("A handler for %s has already been regisered!" % endpoint)
self._entries[key] = JabberRPCEntry(endpoint, method)
return key
diff --git a/sleekxmpp/plugins/xep_0050/adhoc.py b/sleekxmpp/plugins/xep_0050/adhoc.py
index 72c6c513..dd1c88d6 100644
--- a/sleekxmpp/plugins/xep_0050/adhoc.py
+++ b/sleekxmpp/plugins/xep_0050/adhoc.py
@@ -589,5 +589,5 @@ class xep_0050(base_plugin):
elif iq['type'] == 'error':
self.terminate_command(session)
- if iq['command']['status'] == 'completed':
+ if iq['command']['status'] == 'completed':
self.terminate_command(session)
diff --git a/sleekxmpp/plugins/xep_0060/__init__.py b/sleekxmpp/plugins/xep_0060/__init__.py
new file mode 100644
index 00000000..026f7c2b
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0060/__init__.py
@@ -0,0 +1,2 @@
+from sleekxmpp.plugins.xep_0060.pubsub import xep_0060
+from sleekxmpp.plugins.xep_0060 import stanza
diff --git a/sleekxmpp/plugins/xep_0060/pubsub.py b/sleekxmpp/plugins/xep_0060/pubsub.py
new file mode 100644
index 00000000..e199be07
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0060/pubsub.py
@@ -0,0 +1,313 @@
+from __future__ import with_statement
+from sleekxmpp.plugins import base
+import logging
+#from xml.etree import cElementTree as ET
+from sleekxmpp.xmlstream.stanzabase import registerStanzaPlugin, ElementBase, ET
+from sleekxmpp.plugins.xep_0060 import stanza
+from sleekxmpp.plugins.xep_0004 import Form
+
+
+log = logging.getLogger(__name__)
+
+
+class xep_0060(base.base_plugin):
+ """
+ XEP-0060 Publish Subscribe
+ """
+
+ def plugin_init(self):
+ self.xep = '0060'
+ self.description = 'Publish-Subscribe'
+
+ def create_node(self, jid, node, config=None, collection=False, ntype=None):
+ pubsub = ET.Element('{http://jabber.org/protocol/pubsub}pubsub')
+ create = ET.Element('create')
+ create.set('node', node)
+ pubsub.append(create)
+ configure = ET.Element('configure')
+ if collection:
+ ntype = 'collection'
+ #if config is None:
+ # submitform = self.xmpp.plugin['xep_0004'].makeForm('submit')
+ #else:
+ if config is not None:
+ submitform = config
+ if 'FORM_TYPE' in submitform.field:
+ submitform.field['FORM_TYPE'].setValue('http://jabber.org/protocol/pubsub#node_config')
+ else:
+ submitform.addField('FORM_TYPE', 'hidden', value='http://jabber.org/protocol/pubsub#node_config')
+ if ntype:
+ if 'pubsub#node_type' in submitform.field:
+ submitform.field['pubsub#node_type'].setValue(ntype)
+ else:
+ submitform.addField('pubsub#node_type', value=ntype)
+ else:
+ if 'pubsub#node_type' in submitform.field:
+ submitform.field['pubsub#node_type'].setValue('leaf')
+ else:
+ submitform.addField('pubsub#node_type', value='leaf')
+ submitform['type'] = 'submit'
+ configure.append(submitform.xml)
+ pubsub.append(configure)
+ iq = self.xmpp.makeIqSet(pubsub)
+ iq.attrib['to'] = jid
+ iq.attrib['from'] = self.xmpp.boundjid.full
+ id = iq['id']
+ result = iq.send()
+ if result is False or result is None or result['type'] == 'error': return False
+ return True
+
+ def subscribe(self, jid, node, bare=True, subscribee=None):
+ pubsub = ET.Element('{http://jabber.org/protocol/pubsub}pubsub')
+ subscribe = ET.Element('subscribe')
+ subscribe.attrib['node'] = node
+ if subscribee is None:
+ if bare:
+ subscribe.attrib['jid'] = self.xmpp.boundjid.bare
+ else:
+ subscribe.attrib['jid'] = self.xmpp.boundjid.full
+ else:
+ subscribe.attrib['jid'] = subscribee
+ pubsub.append(subscribe)
+ iq = self.xmpp.makeIqSet(pubsub)
+ iq.attrib['to'] = jid
+ iq.attrib['from'] = self.xmpp.boundjid.full
+ id = iq['id']
+ result = iq.send()
+ if result is False or result is None or result['type'] == 'error': return False
+ return True
+
+ def unsubscribe(self, jid, node, bare=True, subscribee=None):
+ pubsub = ET.Element('{http://jabber.org/protocol/pubsub}pubsub')
+ unsubscribe = ET.Element('unsubscribe')
+ unsubscribe.attrib['node'] = node
+ if subscribee is None:
+ if bare:
+ unsubscribe.attrib['jid'] = self.xmpp.boundjid.bare
+ else:
+ unsubscribe.attrib['jid'] = self.xmpp.boundjid.full
+ else:
+ unsubscribe.attrib['jid'] = subscribee
+ pubsub.append(unsubscribe)
+ iq = self.xmpp.makeIqSet(pubsub)
+ iq.attrib['to'] = jid
+ iq.attrib['from'] = self.xmpp.boundjid.full
+ id = iq['id']
+ result = iq.send()
+ if result is False or result is None or result['type'] == 'error': return False
+ return True
+
+ def getNodeConfig(self, jid, node=None): # if no node, then grab default
+ pubsub = ET.Element('{http://jabber.org/protocol/pubsub#owner}pubsub')
+ if node is not None:
+ configure = ET.Element('configure')
+ configure.attrib['node'] = node
+ else:
+ configure = ET.Element('default')
+ pubsub.append(configure)
+ #TODO: Add configure support.
+ iq = self.xmpp.makeIqGet()
+ iq.append(pubsub)
+ iq.attrib['to'] = jid
+ iq.attrib['from'] = self.xmpp.boundjid.full
+ id = iq['id']
+ #self.xmpp.add_handler("<iq id='%s'/>" % id, self.handlerCreateNodeResponse)
+ result = iq.send()
+ if result is None or result == False or result['type'] == 'error':
+ log.warning("got error instead of config")
+ return False
+ if node is not None:
+ form = result.find('{http://jabber.org/protocol/pubsub#owner}pubsub/{http://jabber.org/protocol/pubsub#owner}configure/{jabber:x:data}x')
+ else:
+ form = result.find('{http://jabber.org/protocol/pubsub#owner}pubsub/{http://jabber.org/protocol/pubsub#owner}default/{jabber:x:data}x')
+ if not form or form is None:
+ log.error("No form found.")
+ return False
+ return Form(xml=form)
+
+ def getNodeSubscriptions(self, jid, node):
+ pubsub = ET.Element('{http://jabber.org/protocol/pubsub#owner}pubsub')
+ subscriptions = ET.Element('subscriptions')
+ subscriptions.attrib['node'] = node
+ pubsub.append(subscriptions)
+ iq = self.xmpp.makeIqGet()
+ iq.append(pubsub)
+ iq.attrib['to'] = jid
+ iq.attrib['from'] = self.xmpp.boundjid.full
+ id = iq['id']
+ result = iq.send()
+ if result is None or result == False or result['type'] == 'error':
+ log.warning("got error instead of config")
+ return False
+ else:
+ results = result.findall('{http://jabber.org/protocol/pubsub#owner}pubsub/{http://jabber.org/protocol/pubsub#owner}subscriptions/{http://jabber.org/protocol/pubsub#owner}subscription')
+ if results is None:
+ return False
+ subs = {}
+ for sub in results:
+ subs[sub.get('jid')] = sub.get('subscription')
+ return subs
+
+ def getNodeAffiliations(self, jid, node):
+ pubsub = ET.Element('{http://jabber.org/protocol/pubsub#owner}pubsub')
+ affiliations = ET.Element('affiliations')
+ affiliations.attrib['node'] = node
+ pubsub.append(affiliations)
+ iq = self.xmpp.makeIqGet()
+ iq.append(pubsub)
+ iq.attrib['to'] = jid
+ iq.attrib['from'] = self.xmpp.boundjid.full
+ id = iq['id']
+ result = iq.send()
+ if result is None or result == False or result['type'] == 'error':
+ log.warning("got error instead of config")
+ return False
+ else:
+ results = result.findall('{http://jabber.org/protocol/pubsub#owner}pubsub/{http://jabber.org/protocol/pubsub#owner}affiliations/{http://jabber.org/protocol/pubsub#owner}affiliation')
+ if results is None:
+ return False
+ subs = {}
+ for sub in results:
+ subs[sub.get('jid')] = sub.get('affiliation')
+ return subs
+
+ def deleteNode(self, jid, node):
+ pubsub = ET.Element('{http://jabber.org/protocol/pubsub#owner}pubsub')
+ iq = self.xmpp.makeIqSet()
+ delete = ET.Element('delete')
+ delete.attrib['node'] = node
+ pubsub.append(delete)
+ iq.append(pubsub)
+ iq.attrib['to'] = jid
+ iq.attrib['from'] = self.xmpp.boundjid.full
+ result = iq.send()
+ if result is not None and result is not False and result['type'] != 'error':
+ return True
+ else:
+ return False
+
+
+ def setNodeConfig(self, jid, node, config):
+ pubsub = ET.Element('{http://jabber.org/protocol/pubsub#owner}pubsub')
+ configure = ET.Element('configure')
+ configure.attrib['node'] = node
+ config = config.getXML('submit')
+ configure.append(config)
+ pubsub.append(configure)
+ iq = self.xmpp.makeIqSet(pubsub)
+ iq.attrib['to'] = jid
+ iq.attrib['from'] = self.xmpp.boundjid.full
+ id = iq['id']
+ result = iq.send()
+ if result is None or result['type'] == 'error':
+ return False
+ return True
+
+ def setItem(self, jid, node, items=[]):
+ pubsub = ET.Element('{http://jabber.org/protocol/pubsub}pubsub')
+ publish = ET.Element('publish')
+ publish.attrib['node'] = node
+ for pub_item in items:
+ id, payload = pub_item
+ item = ET.Element('item')
+ if id is not None:
+ item.attrib['id'] = id
+ item.append(payload)
+ publish.append(item)
+ pubsub.append(publish)
+ iq = self.xmpp.makeIqSet(pubsub)
+ iq.attrib['to'] = jid
+ iq.attrib['from'] = self.xmpp.boundjid.full
+ id = iq['id']
+ result = iq.send()
+ if result is None or result is False or result['type'] == 'error': return False
+ return True
+
+ def addItem(self, jid, node, items=[]):
+ return self.setItem(jid, node, items)
+
+ def deleteItem(self, jid, node, item):
+ pubsub = ET.Element('{http://jabber.org/protocol/pubsub}pubsub')
+ retract = ET.Element('retract')
+ retract.attrib['node'] = node
+ itemn = ET.Element('item')
+ itemn.attrib['id'] = item
+ retract.append(itemn)
+ pubsub.append(retract)
+ iq = self.xmpp.makeIqSet(pubsub)
+ iq.attrib['to'] = jid
+ iq.attrib['from'] = self.xmpp.boundjid.full
+ id = iq['id']
+ result = iq.send()
+ if result is None or result is False or result['type'] == 'error': return False
+ return True
+
+ def getNodes(self, jid):
+ response = self.xmpp.plugin['xep_0030'].getItems(jid)
+ items = response.findall('{http://jabber.org/protocol/disco#items}query/{http://jabber.org/protocol/disco#items}item')
+ nodes = {}
+ if items is not None and items is not False:
+ for item in items:
+ nodes[item.get('node')] = item.get('name')
+ return nodes
+
+ def getItems(self, jid, node):
+ response = self.xmpp.plugin['xep_0030'].getItems(jid, node)
+ items = response.findall('{http://jabber.org/protocol/disco#items}query/{http://jabber.org/protocol/disco#items}item')
+ nodeitems = []
+ if items is not None and items is not False:
+ for item in items:
+ nodeitems.append(item.get('node'))
+ return nodeitems
+
+ def addNodeToCollection(self, jid, child, parent=''):
+ config = self.getNodeConfig(jid, child)
+ if not config or config is None:
+ self.lasterror = "Config Error"
+ return False
+ try:
+ config.field['pubsub#collection'].setValue(parent)
+ except KeyError:
+ log.warning("pubsub#collection doesn't exist in config, trying to add it")
+ config.addField('pubsub#collection', value=parent)
+ if not self.setNodeConfig(jid, child, config):
+ return False
+ return True
+
+ def modifyAffiliation(self, ps_jid, node, user_jid, affiliation):
+ if affiliation not in ('owner', 'publisher', 'member', 'none', 'outcast'):
+ raise TypeError
+ pubsub = ET.Element('{http://jabber.org/protocol/pubsub#owner}pubsub')
+ affs = ET.Element('affiliations')
+ affs.attrib['node'] = node
+ aff = ET.Element('affiliation')
+ aff.attrib['jid'] = user_jid
+ aff.attrib['affiliation'] = affiliation
+ affs.append(aff)
+ pubsub.append(affs)
+ iq = self.xmpp.makeIqSet(pubsub)
+ iq.attrib['to'] = ps_jid
+ iq.attrib['from'] = self.xmpp.boundjid.full
+ id = iq['id']
+ result = iq.send()
+ if result is None or result is False or result['type'] == 'error':
+ return False
+ return True
+
+ def addNodeToCollection(self, jid, child, parent=''):
+ config = self.getNodeConfig(jid, child)
+ if not config or config is None:
+ self.lasterror = "Config Error"
+ return False
+ try:
+ config.field['pubsub#collection'].setValue(parent)
+ except KeyError:
+ log.warning("pubsub#collection doesn't exist in config, trying to add it")
+ config.addField('pubsub#collection', value=parent)
+ if not self.setNodeConfig(jid, child, config):
+ return False
+ return True
+
+ def removeNodeFromCollection(self, jid, child):
+ self.addNodeToCollection(jid, child, '')
+
diff --git a/sleekxmpp/plugins/xep_0060/stanza/__init__.py b/sleekxmpp/plugins/xep_0060/stanza/__init__.py
new file mode 100644
index 00000000..d7cd91a8
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0060/stanza/__init__.py
@@ -0,0 +1,3 @@
+from sleekxmpp.plugins.xep_0060.stanza.pubsub import Pubsub, Affiliation, Affiliations, Subscription, Subscriptions, SubscribeOptions, Item, Items, Create, Publish, Retract, Unsubscribe, Subscribe, Configure, Options, PubsubState, PubsubStateEvent
+from sleekxmpp.plugins.xep_0060.stanza.pubsub_owner import PubsubOwner, DefaultConfig, OwnerAffiliations, OwnerAffiliation, OwnerConfigure, OwnerDefault, OwnerDelete, OwnerPurge, OwnerRedirect, OwnerSubscriptions, OwnerSubscription
+from sleekxmpp.plugins.xep_0060.stanza.pubsub_event import Event, EventItem, EventRetract, EventItems, EventCollection, EventAssociate, EventDisassociate, EventConfiguration, EventPurge, EventSubscription
diff --git a/sleekxmpp/plugins/xep_0060/stanza/base.py b/sleekxmpp/plugins/xep_0060/stanza/base.py
new file mode 100644
index 00000000..9b1efe1b
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0060/stanza/base.py
@@ -0,0 +1,24 @@
+from xml.etree import cElementTree as ET
+
+class OptionalSetting(object):
+ interfaces = set(('required',))
+
+ def setRequired(self, value):
+ value = bool(value)
+ if value and not self['required']:
+ self.xml.append(ET.Element("{%s}required" % self.namespace))
+ elif not value and self['required']:
+ self.delRequired()
+
+ def getRequired(self):
+ required = self.xml.find("{%s}required" % self.namespace)
+ if required is not None:
+ return True
+ else:
+ return False
+
+ def delRequired(self):
+ required = self.xml.find("{%s}required" % self.namespace)
+ if required is not None:
+ self.xml.remove(required)
+
diff --git a/sleekxmpp/plugins/xep_0060/stanza/pubsub.py b/sleekxmpp/plugins/xep_0060/stanza/pubsub.py
new file mode 100644
index 00000000..96655942
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0060/stanza/pubsub.py
@@ -0,0 +1,277 @@
+from sleekxmpp.xmlstream.stanzabase import registerStanzaPlugin, ElementBase, ET, JID
+from sleekxmpp.stanza.iq import Iq
+from sleekxmpp.stanza.message import Message
+from sleekxmpp.basexmpp import basexmpp
+from sleekxmpp.xmlstream.xmlstream import XMLStream
+import logging
+from sleekxmpp.plugins import xep_0004
+from sleekxmpp.plugins.xep_0060.stanza.base import OptionalSetting
+
+
+class Pubsub(ElementBase):
+ namespace = 'http://jabber.org/protocol/pubsub'
+ name = 'pubsub'
+ plugin_attrib = 'pubsub'
+ interfaces = set(tuple())
+ plugin_attrib_map = {}
+ plugin_tag_map = {}
+
+registerStanzaPlugin(Iq, Pubsub)
+
+
+class Affiliation(ElementBase):
+ namespace = 'http://jabber.org/protocol/pubsub'
+ name = 'affiliation'
+ plugin_attrib = name
+ interfaces = set(('node', 'affiliation'))
+ plugin_attrib_map = {}
+ plugin_tag_map = {}
+
+class Affiliations(ElementBase):
+ namespace = 'http://jabber.org/protocol/pubsub'
+ name = 'affiliations'
+ plugin_attrib = 'affiliations'
+ interfaces = set(tuple())
+ plugin_attrib_map = {}
+ plugin_tag_map = {}
+ subitem = (Affiliation,)
+
+ def append(self, affiliation):
+ if not isinstance(affiliation, Affiliation):
+ raise TypeError
+ self.xml.append(affiliation.xml)
+ return self.iterables.append(affiliation)
+
+registerStanzaPlugin(Pubsub, Affiliations)
+
+
+class Subscription(ElementBase):
+ namespace = 'http://jabber.org/protocol/pubsub'
+ name = 'subscription'
+ plugin_attrib = name
+ interfaces = set(('jid', 'node', 'subscription', 'subid'))
+ plugin_attrib_map = {}
+ plugin_tag_map = {}
+
+ def setjid(self, value):
+ self._setattr('jid', str(value))
+
+ def getjid(self):
+ return jid(self._getattr('jid'))
+
+registerStanzaPlugin(Pubsub, Subscription)
+
+class Subscriptions(ElementBase):
+ namespace = 'http://jabber.org/protocol/pubsub'
+ name = 'subscriptions'
+ plugin_attrib = 'subscriptions'
+ interfaces = set(tuple())
+ plugin_attrib_map = {}
+ plugin_tag_map = {}
+ subitem = (Subscription,)
+
+registerStanzaPlugin(Pubsub, Subscriptions)
+
+
+class SubscribeOptions(ElementBase, OptionalSetting):
+ namespace = 'http://jabber.org/protocol/pubsub'
+ name = 'subscribe-options'
+ plugin_attrib = 'suboptions'
+ plugin_attrib_map = {}
+ plugin_tag_map = {}
+ interfaces = set(('required',))
+
+registerStanzaPlugin(Subscription, SubscribeOptions)
+
+class Item(ElementBase):
+ namespace = 'http://jabber.org/protocol/pubsub'
+ name = 'item'
+ plugin_attrib = name
+ interfaces = set(('id', 'payload'))
+ plugin_attrib_map = {}
+ plugin_tag_map = {}
+
+ def setPayload(self, value):
+ self.xml.append(value)
+
+ def getPayload(self):
+ childs = self.xml.getchildren()
+ if len(childs) > 0:
+ return childs[0]
+
+ def delPayload(self):
+ for child in self.xml.getchildren():
+ self.xml.remove(child)
+
+class Items(ElementBase):
+ namespace = 'http://jabber.org/protocol/pubsub'
+ name = 'items'
+ plugin_attrib = 'items'
+ interfaces = set(('node',))
+ plugin_attrib_map = {}
+ plugin_tag_map = {}
+ subitem = (Item,)
+
+registerStanzaPlugin(Pubsub, Items)
+
+class Create(ElementBase):
+ namespace = 'http://jabber.org/protocol/pubsub'
+ name = 'create'
+ plugin_attrib = name
+ interfaces = set(('node',))
+ plugin_attrib_map = {}
+ plugin_tag_map = {}
+
+registerStanzaPlugin(Pubsub, Create)
+
+#class Default(ElementBase):
+# namespace = 'http://jabber.org/protocol/pubsub'
+# name = 'default'
+# plugin_attrib = name
+# interfaces = set(('node', 'type'))
+# plugin_attrib_map = {}
+# plugin_tag_map = {}
+#
+# def getType(self):
+# t = self._getAttr('type')
+# if not t: t == 'leaf'
+# return t
+#
+#registerStanzaPlugin(Pubsub, Default)
+
+class Publish(Items):
+ namespace = 'http://jabber.org/protocol/pubsub'
+ name = 'publish'
+ plugin_attrib = name
+ interfaces = set(('node',))
+ plugin_attrib_map = {}
+ plugin_tag_map = {}
+ subitem = (Item,)
+
+registerStanzaPlugin(Pubsub, Publish)
+
+class Retract(Items):
+ namespace = 'http://jabber.org/protocol/pubsub'
+ name = 'retract'
+ plugin_attrib = name
+ interfaces = set(('node', 'notify'))
+ plugin_attrib_map = {}
+ plugin_tag_map = {}
+
+registerStanzaPlugin(Pubsub, Retract)
+
+class Unsubscribe(ElementBase):
+ namespace = 'http://jabber.org/protocol/pubsub'
+ name = 'unsubscribe'
+ plugin_attrib = name
+ interfaces = set(('node', 'jid'))
+ plugin_attrib_map = {}
+ plugin_tag_map = {}
+
+ def setJid(self, value):
+ self._setAttr('jid', str(value))
+
+ def getJid(self):
+ return JID(self._getAttr('jid'))
+
+registerStanzaPlugin(Pubsub, Unsubscribe)
+
+class Subscribe(ElementBase):
+ namespace = 'http://jabber.org/protocol/pubsub'
+ name = 'subscribe'
+ plugin_attrib = name
+ interfaces = set(('node', 'jid'))
+ plugin_attrib_map = {}
+ plugin_tag_map = {}
+
+ def setJid(self, value):
+ self._setAttr('jid', str(value))
+
+ def getJid(self):
+ return JID(self._getAttr('jid'))
+
+registerStanzaPlugin(Pubsub, Subscribe)
+
+class Configure(ElementBase):
+ namespace = 'http://jabber.org/protocol/pubsub'
+ name = 'configure'
+ plugin_attrib = name
+ interfaces = set(('node', 'type'))
+ plugin_attrib_map = {}
+ plugin_tag_map = {}
+
+ def getType(self):
+ t = self._getAttr('type')
+ if not t: t == 'leaf'
+ return t
+
+registerStanzaPlugin(Pubsub, Configure)
+registerStanzaPlugin(Configure, xep_0004.Form)
+
+class Options(ElementBase):
+ namespace = 'http://jabber.org/protocol/pubsub'
+ name = 'options'
+ plugin_attrib = 'options'
+ interfaces = set(('jid', 'node', 'options'))
+ plugin_attrib_map = {}
+ plugin_tag_map = {}
+
+ def __init__(self, *args, **kwargs):
+ ElementBase.__init__(self, *args, **kwargs)
+
+ def getOptions(self):
+ config = self.xml.find('{jabber:x:data}x')
+ form = xep_0004.Form()
+ if config is not None:
+ form.fromXML(config)
+ return form
+
+ def setOptions(self, value):
+ self.xml.append(value.getXML())
+ return self
+
+ def delOptions(self):
+ config = self.xml.find('{jabber:x:data}x')
+ self.xml.remove(config)
+
+ def setJid(self, value):
+ self._setAttr('jid', str(value))
+
+ def getJid(self):
+ return JID(self._getAttr('jid'))
+
+registerStanzaPlugin(Pubsub, Options)
+registerStanzaPlugin(Subscribe, Options)
+
+class PubsubState(ElementBase):
+ namespace = 'http://jabber.org/protocol/psstate'
+ name = 'state'
+ plugin_attrib = 'psstate'
+ interfaces = set(('node', 'item', 'payload'))
+ plugin_attrib_map = {}
+ plugin_tag_map = {}
+
+ def setPayload(self, value):
+ self.xml.append(value)
+
+ def getPayload(self):
+ childs = self.xml.getchildren()
+ if len(childs) > 0:
+ return childs[0]
+
+ def delPayload(self):
+ for child in self.xml.getchildren():
+ self.xml.remove(child)
+
+registerStanzaPlugin(Iq, PubsubState)
+
+class PubsubStateEvent(ElementBase):
+ namespace = 'http://jabber.org/protocol/psstate#event'
+ name = 'event'
+ plugin_attrib = 'psstate_event'
+ intefaces = set(tuple())
+ plugin_attrib_map = {}
+ plugin_tag_map = {}
+
+registerStanzaPlugin(Message, PubsubStateEvent)
+registerStanzaPlugin(PubsubStateEvent, PubsubState)
diff --git a/sleekxmpp/plugins/xep_0060/stanza/pubsub_event.py b/sleekxmpp/plugins/xep_0060/stanza/pubsub_event.py
new file mode 100644
index 00000000..2dfe6c4a
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0060/stanza/pubsub_event.py
@@ -0,0 +1,124 @@
+from sleekxmpp.xmlstream.stanzabase import registerStanzaPlugin, ElementBase, ET, JID
+from sleekxmpp.stanza.iq import Iq
+from sleekxmpp.stanza.message import Message
+from sleekxmpp.basexmpp import basexmpp
+from sleekxmpp.xmlstream.xmlstream import XMLStream
+import logging
+from sleekxmpp.plugins import xep_0004
+
+class Event(ElementBase):
+ namespace = 'http://jabber.org/protocol/pubsub#event'
+ name = 'event'
+ plugin_attrib = 'pubsub_event'
+ interfaces = set(('node',))
+ plugin_attrib_map = {}
+ plugin_tag_map = {}
+
+registerStanzaPlugin(Message, Event)
+
+class EventItem(ElementBase):
+ namespace = 'http://jabber.org/protocol/pubsub#event'
+ name = 'item'
+ plugin_attrib = 'item'
+ interfaces = set(('id', 'payload'))
+ plugin_attrib_map = {}
+ plugin_tag_map = {}
+
+ def setPayload(self, value):
+ self.xml.append(value)
+
+ def getPayload(self):
+ childs = self.xml.getchildren()
+ if len(childs) > 0:
+ return childs[0]
+
+ def delPayload(self):
+ for child in self.xml.getchildren():
+ self.xml.remove(child)
+
+
+class EventRetract(ElementBase):
+ namespace = 'http://jabber.org/protocol/pubsub#event'
+ name = 'retract'
+ plugin_attrib = 'retract'
+ interfaces = set(('id',))
+ plugin_attrib_map = {}
+ plugin_tag_map = {}
+
+class EventItems(ElementBase):
+ namespace = 'http://jabber.org/protocol/pubsub#event'
+ name = 'items'
+ plugin_attrib = 'items'
+ interfaces = set(('node',))
+ plugin_attrib_map = {}
+ plugin_tag_map = {}
+ subitem = (EventItem, EventRetract)
+
+registerStanzaPlugin(Event, EventItems)
+
+class EventCollection(ElementBase):
+ namespace = 'http://jabber.org/protocol/pubsub#event'
+ name = 'collection'
+ plugin_attrib = name
+ interfaces = set(('node',))
+ plugin_attrib_map = {}
+ plugin_tag_map = {}
+
+registerStanzaPlugin(Event, EventCollection)
+
+class EventAssociate(ElementBase):
+ namespace = 'http://jabber.org/protocol/pubsub#event'
+ name = 'associate'
+ plugin_attrib = name
+ interfaces = set(('node',))
+ plugin_attrib_map = {}
+ plugin_tag_map = {}
+
+registerStanzaPlugin(EventCollection, EventAssociate)
+
+class EventDisassociate(ElementBase):
+ namespace = 'http://jabber.org/protocol/pubsub#event'
+ name = 'disassociate'
+ plugin_attrib = name
+ interfaces = set(('node',))
+ plugin_attrib_map = {}
+ plugin_tag_map = {}
+
+registerStanzaPlugin(EventCollection, EventDisassociate)
+
+class EventConfiguration(ElementBase):
+ namespace = 'http://jabber.org/protocol/pubsub#event'
+ name = 'configuration'
+ plugin_attrib = name
+ interfaces = set(('node', 'config'))
+ plugin_attrib_map = {}
+ plugin_tag_map = {}
+
+registerStanzaPlugin(Event, EventConfiguration)
+registerStanzaPlugin(EventConfiguration, xep_0004.Form)
+
+class EventPurge(ElementBase):
+ namespace = 'http://jabber.org/protocol/pubsub#event'
+ name = 'purge'
+ plugin_attrib = name
+ interfaces = set(('node',))
+ plugin_attrib_map = {}
+ plugin_tag_map = {}
+
+registerStanzaPlugin(Event, EventPurge)
+
+class EventSubscription(ElementBase):
+ namespace = 'http://jabber.org/protocol/pubsub#event'
+ name = 'subscription'
+ plugin_attrib = name
+ interfaces = set(('node','expiry', 'jid', 'subid', 'subscription'))
+ plugin_attrib_map = {}
+ plugin_tag_map = {}
+
+ def setJid(self, value):
+ self._setAttr('jid', str(value))
+
+ def getJid(self):
+ return JID(self._getAttr('jid'))
+
+registerStanzaPlugin(Event, EventSubscription)
diff --git a/sleekxmpp/plugins/xep_0060/stanza/pubsub_owner.py b/sleekxmpp/plugins/xep_0060/stanza/pubsub_owner.py
new file mode 100644
index 00000000..a90780cc
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0060/stanza/pubsub_owner.py
@@ -0,0 +1,152 @@
+from sleekxmpp.xmlstream.stanzabase import registerStanzaPlugin, ElementBase, ET, JID
+from sleekxmpp.stanza.iq import Iq
+from sleekxmpp.stanza.message import Message
+from sleekxmpp.basexmpp import basexmpp
+from sleekxmpp.xmlstream.xmlstream import XMLStream
+import logging
+from sleekxmpp.plugins import xep_0004
+from sleekxmpp.plugins.xep_0060.stanza.base import OptionalSetting
+from sleekxmpp.plugins.xep_0060.stanza.pubsub import Affiliations, Affiliation, Configure, Subscriptions
+
+class PubsubOwner(ElementBase):
+ namespace = 'http://jabber.org/protocol/pubsub#owner'
+ name = 'pubsub'
+ plugin_attrib = 'pubsub_owner'
+ interfaces = set(tuple())
+ plugin_attrib_map = {}
+ plugin_tag_map = {}
+
+registerStanzaPlugin(Iq, PubsubOwner)
+
+class DefaultConfig(ElementBase):
+ namespace = 'http://jabber.org/protocol/pubsub#owner'
+ name = 'default'
+ plugin_attrib = 'default'
+ interfaces = set(('node', 'type', 'config'))
+ plugin_attrib_map = {}
+ plugin_tag_map = {}
+
+ def __init__(self, *args, **kwargs):
+ ElementBase.__init__(self, *args, **kwargs)
+
+ def getType(self):
+ t = self._getAttr('type')
+ if not t: t = 'leaf'
+ return t
+
+ def getConfig(self):
+ return self['form']
+
+ def setConfig(self, value):
+ self['form'].setStanzaValues(value.getStanzaValues())
+ return self
+
+registerStanzaPlugin(PubsubOwner, DefaultConfig)
+registerStanzaPlugin(DefaultConfig, xep_0004.Form)
+
+class OwnerAffiliations(Affiliations):
+ namespace = 'http://jabber.org/protocol/pubsub#owner'
+ interfaces = set(('node'))
+ plugin_attrib_map = {}
+ plugin_tag_map = {}
+
+ def append(self, affiliation):
+ if not isinstance(affiliation, OwnerAffiliation):
+ raise TypeError
+ self.xml.append(affiliation.xml)
+ return self.affiliations.append(affiliation)
+
+registerStanzaPlugin(PubsubOwner, OwnerAffiliations)
+
+class OwnerAffiliation(Affiliation):
+ namespace = 'http://jabber.org/protocol/pubsub#owner'
+ interfaces = set(('affiliation', 'jid'))
+ plugin_attrib_map = {}
+ plugin_tag_map = {}
+
+class OwnerConfigure(Configure):
+ namespace = 'http://jabber.org/protocol/pubsub#owner'
+ interfaces = set(('node', 'config'))
+ plugin_attrib_map = {}
+ plugin_tag_map = {}
+
+registerStanzaPlugin(PubsubOwner, OwnerConfigure)
+
+class OwnerDefault(OwnerConfigure):
+ namespace = 'http://jabber.org/protocol/pubsub#owner'
+ interfaces = set(('node', 'config'))
+ plugin_attrib_map = {}
+ plugin_tag_map = {}
+
+ def getConfig(self):
+ return self['form']
+
+ def setConfig(self, value):
+ self['form'].setStanzaValues(value.getStanzaValues())
+ return self
+
+registerStanzaPlugin(PubsubOwner, OwnerDefault)
+registerStanzaPlugin(OwnerDefault, xep_0004.Form)
+
+class OwnerDelete(ElementBase, OptionalSetting):
+ namespace = 'http://jabber.org/protocol/pubsub#owner'
+ name = 'delete'
+ plugin_attrib = 'delete'
+ plugin_attrib_map = {}
+ plugin_tag_map = {}
+ interfaces = set(('node',))
+
+registerStanzaPlugin(PubsubOwner, OwnerDelete)
+
+class OwnerPurge(ElementBase, OptionalSetting):
+ namespace = 'http://jabber.org/protocol/pubsub#owner'
+ name = 'purge'
+ plugin_attrib = name
+ plugin_attrib_map = {}
+ plugin_tag_map = {}
+
+registerStanzaPlugin(PubsubOwner, OwnerPurge)
+
+class OwnerRedirect(ElementBase):
+ namespace = 'http://jabber.org/protocol/pubsub#owner'
+ name = 'redirect'
+ plugin_attrib = name
+ interfaces = set(('node', 'jid'))
+ plugin_attrib_map = {}
+ plugin_tag_map = {}
+
+ def setJid(self, value):
+ self._setAttr('jid', str(value))
+
+ def getJid(self):
+ return JID(self._getAttr('jid'))
+
+registerStanzaPlugin(OwnerDelete, OwnerRedirect)
+
+class OwnerSubscriptions(Subscriptions):
+ namespace = 'http://jabber.org/protocol/pubsub#owner'
+ interfaces = set(('node',))
+ plugin_attrib_map = {}
+ plugin_tag_map = {}
+
+ def append(self, subscription):
+ if not isinstance(subscription, OwnerSubscription):
+ raise TypeError
+ self.xml.append(subscription.xml)
+ return self.subscriptions.append(subscription)
+
+registerStanzaPlugin(PubsubOwner, OwnerSubscriptions)
+
+class OwnerSubscription(ElementBase):
+ namespace = 'http://jabber.org/protocol/pubsub#owner'
+ name = 'subscription'
+ plugin_attrib = name
+ interfaces = set(('jid', 'subscription'))
+ plugin_attrib_map = {}
+ plugin_tag_map = {}
+
+ def setJid(self, value):
+ self._setAttr('jid', str(value))
+
+ def getJid(self):
+ return JID(self._getAttr('from'))
diff --git a/sleekxmpp/plugins/xep_0066/__init__.py b/sleekxmpp/plugins/xep_0066/__init__.py
new file mode 100644
index 00000000..ebfbd0c2
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0066/__init__.py
@@ -0,0 +1,11 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+from sleekxmpp.plugins.xep_0066 import stanza
+from sleekxmpp.plugins.xep_0066.stanza import OOB, OOBTransfer
+from sleekxmpp.plugins.xep_0066.oob import xep_0066
diff --git a/sleekxmpp/plugins/xep_0066/oob.py b/sleekxmpp/plugins/xep_0066/oob.py
new file mode 100644
index 00000000..98cb81cd
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0066/oob.py
@@ -0,0 +1,154 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+import logging
+
+from sleekxmpp.stanza import Message, Presence, Iq
+from sleekxmpp.exceptions import XMPPError
+from sleekxmpp.xmlstream import register_stanza_plugin
+from sleekxmpp.xmlstream.handler import Callback
+from sleekxmpp.xmlstream.matcher import StanzaPath
+from sleekxmpp.plugins.base import base_plugin
+from sleekxmpp.plugins.xep_0066 import stanza
+
+
+log = logging.getLogger(__name__)
+
+
+class xep_0066(base_plugin):
+
+ """
+ XEP-0066: Out-of-Band Data
+
+ Out-of-Band Data is a basic method for transferring files between
+ XMPP agents. The URL of the resource in question is sent to the receiving
+ entity, which then downloads the resource before responding to the OOB
+ request. OOB is also used as a generic means to transmit URLs in other
+ stanzas to indicate where to find additional information.
+
+ Also see <http://www.xmpp.org/extensions/xep-0066.html>.
+
+ Events:
+ oob_transfer -- Raised when a request to download a resource
+ has been received.
+
+ Methods:
+ send_oob -- Send a request to another entity to download a file
+ or other addressable resource.
+ """
+
+ def plugin_init(self):
+ """Start the XEP-0066 plugin."""
+ self.xep = '0066'
+ self.description = 'Out-of-Band Transfer'
+ self.stanza = stanza
+
+ self.url_handlers = {'global': self._default_handler,
+ 'jid': {}}
+
+ register_stanza_plugin(Iq, stanza.OOBTransfer)
+ register_stanza_plugin(Message, stanza.OOB)
+ register_stanza_plugin(Presence, stanza.OOB)
+
+ self.xmpp.register_handler(
+ Callback('OOB Transfer',
+ StanzaPath('iq@type=set/oob_transfer'),
+ self._handle_transfer))
+
+ def post_init(self):
+ """Handle cross-plugin dependencies."""
+ base_plugin.post_init(self)
+ self.xmpp['xep_0030'].add_feature(stanza.OOBTransfer.namespace)
+ self.xmpp['xep_0030'].add_feature(stanza.OOB.namespace)
+
+ def register_url_handler(self, jid=None, handler=None):
+ """
+ Register a handler to process download requests, either for all
+ JIDs or a single JID.
+
+ Arguments:
+ jid -- If None, then set the handler as a global default.
+ handler -- If None, then remove the existing handler for the
+ given JID, or reset the global handler if the JID
+ is None.
+ """
+ if jid is None:
+ if handler is not None:
+ self.url_handlers['global'] = handler
+ else:
+ self.url_handlers['global'] = self._default_handler
+ else:
+ if handler is not None:
+ self.url_handlers['jid'][jid] = handler
+ else:
+ del self.url_handlers['jid'][jid]
+
+ def send_oob(self, to, url, desc=None, ifrom=None, **iqargs):
+ """
+ Initiate a basic file transfer by sending the URL of
+ a file or other resource.
+
+ Arguments:
+ url -- The URL of the resource to transfer.
+ desc -- An optional human readable description of the item
+ that is to be transferred.
+ ifrom -- Specifiy the sender's JID.
+ block -- If true, block and wait for the stanzas' reply.
+ timeout -- The time in seconds to block while waiting for
+ a reply. If None, then wait indefinitely.
+ callback -- Optional callback to execute when a reply is
+ received instead of blocking and waiting for
+ the reply.
+ """
+ iq = self.xmpp.Iq()
+ iq['type'] = 'set'
+ iq['to'] = to
+ if ifrom:
+ iq['from'] = ifrom
+ iq['oob_transfer']['url'] = url
+ iq['oob_transfer']['desc'] = desc
+ return iq.send(**iqargs)
+
+ def _run_url_handler(self, iq):
+ """
+ Execute the appropriate handler for a transfer request.
+
+ Arguments:
+ iq -- The Iq stanza containing the OOB transfer request.
+ """
+ if iq['to'] in self.url_handlers['jid']:
+ return self.url_handlers['jid'][jid](iq)
+ else:
+ if self.url_handlers['global']:
+ self.url_handlers['global'](iq)
+ else:
+ raise XMPPError('service-unavailable')
+
+ def _default_handler(self, iq):
+ """
+ As a safe default, don't actually download files.
+
+ Register a new handler using self.register_url_handler to
+ screen requests and download files.
+
+ Arguments:
+ iq -- The Iq stanza containing the OOB transfer request.
+ """
+ raise XMPPError('service-unavailable')
+
+ def _handle_transfer(self, iq):
+ """
+ Handle receiving an out-of-band transfer request.
+
+ Arguments:
+ iq -- An Iq stanza containing an OOB transfer request.
+ """
+ log.debug('Received out-of-band data request for %s from %s:' % (
+ iq['oob_transfer']['url'], iq['from']))
+ self._run_url_handler(iq)
+ iq.reply().send()
diff --git a/sleekxmpp/plugins/xep_0066/stanza.py b/sleekxmpp/plugins/xep_0066/stanza.py
new file mode 100644
index 00000000..21387485
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0066/stanza.py
@@ -0,0 +1,33 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+from sleekxmpp.xmlstream import ElementBase
+
+
+class OOBTransfer(ElementBase):
+
+ """
+ """
+
+ name = 'query'
+ namespace = 'jabber:iq:oob'
+ plugin_attrib = 'oob_transfer'
+ interfaces = set(('url', 'desc', 'sid'))
+ sub_interfaces = set(('url', 'desc'))
+
+
+class OOB(ElementBase):
+
+ """
+ """
+
+ name = 'x'
+ namespace = 'jabber:x:oob'
+ plugin_attrib = 'oob'
+ interfaces = set(('url', 'desc'))
+ sub_interfaces = interfaces
diff --git a/sleekxmpp/plugins/xep_0078.py b/sleekxmpp/plugins/xep_0078.py
deleted file mode 100644
index bb6a4632..00000000
--- a/sleekxmpp/plugins/xep_0078.py
+++ /dev/null
@@ -1,72 +0,0 @@
-"""
- SleekXMPP: The Sleek XMPP Library
- Copyright (C) 2010 Nathanael C. Fritz
- This file is part of SleekXMPP.
-
- See the file LICENSE for copying permission.
-"""
-from __future__ import with_statement
-from xml.etree import cElementTree as ET
-import logging
-import hashlib
-from . import base
-
-
-log = logging.getLogger(__name__)
-
-
-class xep_0078(base.base_plugin):
- """
- XEP-0078 NON-SASL Authentication
- """
- def plugin_init(self):
- self.description = "Non-SASL Authentication (broken)"
- self.xep = "0078"
- self.xmpp.add_event_handler("session_start", self.check_stream)
- #disabling until I fix conflict with PLAIN
- #self.xmpp.registerFeature("<auth xmlns='http://jabber.org/features/iq-auth'/>", self.auth)
- self.streamid = ''
-
- def check_stream(self, xml):
- self.streamid = xml.attrib['id']
- if xml.get('version', '0') != '1.0':
- self.auth()
-
- def auth(self, xml=None):
- log.debug("Starting jabber:iq:auth Authentication")
- auth_request = self.xmpp.makeIqGet()
- auth_request_query = ET.Element('{jabber:iq:auth}query')
- auth_request.attrib['to'] = self.xmpp.boundjid.host
- username = ET.Element('username')
- username.text = self.xmpp.username
- auth_request_query.append(username)
- auth_request.append(auth_request_query)
- result = auth_request.send()
- rquery = result.find('{jabber:iq:auth}query')
- attempt = self.xmpp.makeIqSet()
- query = ET.Element('{jabber:iq:auth}query')
- resource = ET.Element('resource')
- resource.text = self.xmpp.resource
- query.append(username)
- query.append(resource)
- if rquery.find('{jabber:iq:auth}digest') is None:
- log.warning("Authenticating via jabber:iq:auth Plain.")
- password = ET.Element('password')
- password.text = self.xmpp.password
- query.append(password)
- else:
- log.debug("Authenticating via jabber:iq:auth Digest")
- digest = ET.Element('digest')
- digest.text = hashlib.sha1(b"%s%s" % (self.streamid, self.xmpp.password)).hexdigest()
- query.append(digest)
- attempt.append(query)
- result = attempt.send()
- if result.attrib['type'] == 'result':
- with self.xmpp.lock:
- self.xmpp.authenticated = True
- self.xmpp.sessionstarted = True
- self.xmpp.event("session_start")
- else:
- log.info("Authentication failed")
- self.xmpp.disconnect()
- self.xmpp.event("failed_auth")
diff --git a/sleekxmpp/plugins/xep_0078/__init__.py b/sleekxmpp/plugins/xep_0078/__init__.py
new file mode 100644
index 00000000..5a2bda77
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0078/__init__.py
@@ -0,0 +1,12 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+from sleekxmpp.plugins.xep_0078 import stanza
+from sleekxmpp.plugins.xep_0078.stanza import IqAuth, AuthFeature
+from sleekxmpp.plugins.xep_0078.legacyauth import xep_0078
+
diff --git a/sleekxmpp/plugins/xep_0078/legacyauth.py b/sleekxmpp/plugins/xep_0078/legacyauth.py
new file mode 100644
index 00000000..bdd2df67
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0078/legacyauth.py
@@ -0,0 +1,108 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2011 Nathanael C. Fritz
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+import logging
+import hashlib
+import random
+
+from sleekxmpp.stanza import Iq, StreamFeatures
+from sleekxmpp.xmlstream import ElementBase, ET, register_stanza_plugin
+from sleekxmpp.plugins.base import base_plugin
+from sleekxmpp.plugins.xep_0078 import stanza
+
+
+log = logging.getLogger(__name__)
+
+
+class xep_0078(base_plugin):
+
+ """
+ XEP-0078 NON-SASL Authentication
+
+ This XEP is OBSOLETE in favor of using SASL, so DO NOT use this plugin
+ unless you are forced to use an old XMPP server implementation.
+ """
+
+ def plugin_init(self):
+ self.xep = "0078"
+ self.description = "Non-SASL Authentication"
+ self.stanza = stanza
+
+ self.xmpp.register_feature('auth',
+ self._handle_auth,
+ restart=False,
+ order=self.config.get('order', 15))
+
+ register_stanza_plugin(Iq, stanza.IqAuth)
+ register_stanza_plugin(StreamFeatures, stanza.AuthFeature)
+
+
+ def _handle_auth(self, features):
+ # If we can or have already authenticated with SASL, do nothing.
+ if 'mechanisms' in features['features']:
+ return False
+ if self.xmpp.authenticated:
+ return False
+
+ log.debug("Starting jabber:iq:auth Authentication")
+
+ # Step 1: Request the auth form
+ iq = self.xmpp.Iq()
+ iq['type'] = 'get'
+ iq['to'] = self.xmpp.boundjid.host
+ iq['auth']['username'] = self.xmpp.boundjid.user
+ resp = iq.send(now=True)
+
+ if resp is None or resp['type'] != 'result':
+ log.info("Authentication failed: %s" % resp['error']['condition'])
+ self.xmpp.event('failed_auth', resp, direct=True)
+ self.xmpp.disconnect()
+ return True
+
+ # Step 2: Fill out auth form for either password or digest auth
+ iq = self.xmpp.Iq()
+ iq['type'] = 'set'
+ iq['auth']['username'] = self.xmpp.boundjid.user
+
+ # A resource is required, so create a random one if necessary
+ if self.xmpp.boundjid.resource:
+ iq['auth']['resource'] = self.xmpp.boundjid.resource
+ else:
+ iq['auth']['resource'] = '%s' % random.random()
+
+ if 'digest' in resp['auth']['fields']:
+ log.debug('Authenticating via jabber:iq:auth Digest')
+ if sys.version_info < (3, 0):
+ stream_id = bytes(self.xmpp.stream_id)
+ password = bytes(self.xmpp.password)
+ else:
+ stream_id = bytes(self.xmpp.stream_id, encoding='utf-8')
+ password = bytes(self.xmpp.password, encoding='utf-8')
+
+ digest = hashlib.sha1(b'%s%s' % (stream_id, password)).hexdigest()
+ iq['auth']['digest'] = digest
+ else:
+ log.warning('Authenticating via jabber:iq:auth Plain.')
+ iq['auth']['password'] = self.xmpp.password
+
+ # Step 3: Send credentials
+ result = iq.send(now=True)
+ if result is not None and result.attrib['type'] == 'result':
+ self.xmpp.features.add('auth')
+
+ self.xmpp.authenticated = True
+ log.debug("Established Session")
+ self.xmpp.sessionstarted = True
+ self.xmpp.session_started_event.set()
+ self.xmpp.event('session_start')
+ else:
+ log.info("Authentication failed")
+ self.xmpp.disconnect()
+ self.xmpp.event("failed_auth")
+
+ return True
diff --git a/sleekxmpp/plugins/xep_0078/stanza.py b/sleekxmpp/plugins/xep_0078/stanza.py
new file mode 100644
index 00000000..86ba09ad
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0078/stanza.py
@@ -0,0 +1,43 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2011 Nathanael C. Fritz
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+from sleekxmpp.xmlstream import ElementBase, ET, register_stanza_plugin
+
+
+class IqAuth(ElementBase):
+ namespace = 'jabber:iq:auth'
+ name = 'query'
+ plugin_attrib = 'auth'
+ interfaces = set(('fields', 'username', 'password', 'resource', 'digest'))
+ sub_interfaces = set(('username', 'password', 'resource', 'digest'))
+ plugin_tag_map = {}
+ plugin_attrib_map = {}
+
+ def get_fields(self):
+ fields = set()
+ for field in self.sub_interfaces:
+ if self.xml.find('{%s}%s' % (self.namespace, field)) is not None:
+ fields.add(field)
+ return fields
+
+ def set_resource(self, value):
+ self._set_sub_text('resource', value, keep=True)
+
+ def set_password(self, value):
+ self._set_sub_text('password', value, keep=True)
+
+
+class AuthFeature(ElementBase):
+ namespace = 'http://jabber.org/features/iq-auth'
+ name = 'auth'
+ plugin_attrib = 'auth'
+ interfaces = set()
+ plugin_tag_map = {}
+ plugin_attrib_map = {}
+
+
diff --git a/sleekxmpp/plugins/xep_0082.py b/sleekxmpp/plugins/xep_0082.py
new file mode 100644
index 00000000..d3c4cc56
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0082.py
@@ -0,0 +1,206 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+import logging
+import datetime as dt
+
+from sleekxmpp.plugins.base import base_plugin
+from sleekxmpp.thirdparty import tzutc, tzoffset, parse_iso
+
+
+# =====================================================================
+# To make it easier for stanzas without direct access to plugin objects
+# to use the XEP-0082 utility methods, we will define them as top-level
+# functions and then just reference them in the plugin itself.
+
+def parse(time_str):
+ """
+ Convert a string timestamp into a datetime object.
+
+ Arguments:
+ time_str -- A formatted timestamp string.
+ """
+ return parse_iso(time_str)
+
+
+def format_date(time_obj):
+ """
+ Return a formatted string version of a date object.
+
+ Format:
+ YYYY-MM-DD
+
+ Arguments:
+ time_obj -- A date or datetime object.
+ """
+ if isinstance(time_obj, dt.datetime):
+ time_obj = time_obj.date()
+ return time_obj.isoformat()
+
+def format_time(time_obj):
+ """
+ Return a formatted string version of a time object.
+
+ format:
+ hh:mm:ss[.sss][TZD]
+
+ arguments:
+ time_obj -- A time or datetime object.
+ """
+ if isinstance(time_obj, dt.datetime):
+ time_obj = time_obj.timetz()
+ timestamp = time_obj.isoformat()
+ if time_obj.tzinfo == tzutc():
+ timestamp = timestamp[:-6]
+ return '%sZ' % timestamp
+ return timestamp
+
+def format_datetime(time_obj):
+ """
+ Return a formatted string version of a datetime object.
+
+ Format:
+ YYYY-MM-DDThh:mm:ss[.sss]TZD
+
+ arguments:
+ time_obj -- A datetime object.
+ """
+ timestamp = time_obj.isoformat('T')
+ if time_obj.tzinfo == tzutc():
+ timestamp = timestamp[:-6]
+ return '%sZ' % timestamp
+ return timestamp
+
+def date(year=None, month=None, day=None):
+ """
+ Create a date only timestamp for the given instant.
+
+ Unspecified components default to their current counterparts.
+
+ Arguments:
+ year -- Integer value of the year (4 digits)
+ month -- Integer value of the month
+ day -- Integer value of the day of the month.
+ """
+ today = dt.datetime.today()
+ if year is None:
+ year = today.year
+ if month is None:
+ month = today.month
+ if day is None:
+ day = today.day
+ return format_date(dt.date(year, month, day))
+
+def time(hour=None, min=None, sec=None, micro=None, offset=None):
+ """
+ Create a time only timestamp for the given instant.
+
+ Unspecified components default to their current counterparts.
+
+ Arguments:
+ hour -- Integer value of the hour.
+ min -- Integer value of the number of minutes.
+ sec -- Integer value of the number of seconds.
+ micro -- Integer value of the number of microseconds.
+ offset -- Either a positive or negative number of seconds
+ to offset from UTC to match a desired timezone,
+ or a tzinfo object.
+ """
+ now = dt.datetime.utcnow()
+ if hour is None:
+ hour = now.hour
+ if min is None:
+ min = now.minute
+ if sec is None:
+ sec = now.second
+ if micro is None:
+ micro = now.microsecond
+ if offset is None:
+ offset = tzutc()
+ elif not isinstance(offset, dt.tzinfo):
+ offset = tzoffset(None, offset)
+ time = dt.time(hour, min, sec, micro, offset)
+ return format_time(time)
+
+def datetime(year=None, month=None, day=None, hour=None,
+ min=None, sec=None, micro=None, offset=None,
+ separators=True):
+ """
+ Create a datetime timestamp for the given instant.
+
+ Unspecified components default to their current counterparts.
+
+ Arguments:
+ year -- Integer value of the year (4 digits)
+ month -- Integer value of the month
+ day -- Integer value of the day of the month.
+ hour -- Integer value of the hour.
+ min -- Integer value of the number of minutes.
+ sec -- Integer value of the number of seconds.
+ micro -- Integer value of the number of microseconds.
+ offset -- Either a positive or negative number of seconds
+ to offset from UTC to match a desired timezone,
+ or a tzinfo object.
+ """
+ now = dt.datetime.utcnow()
+ if year is None:
+ year = now.year
+ if month is None:
+ month = now.month
+ if day is None:
+ day = now.day
+ if hour is None:
+ hour = now.hour
+ if min is None:
+ min = now.minute
+ if sec is None:
+ sec = now.second
+ if micro is None:
+ micro = now.microsecond
+ if offset is None:
+ offset = tzutc()
+ elif not isinstance(offset, dt.tzinfo):
+ offset = tzoffset(None, offset)
+
+ date = dt.datetime(year, month, day, hour,
+ min, sec, micro, offset)
+ return format_datetime(date)
+
+class xep_0082(base_plugin):
+
+ """
+ XEP-0082: XMPP Date and Time Profiles
+
+ XMPP uses a subset of the formats allowed by ISO 8601 as a matter of
+ pragmatism based on the relatively few formats historically used by
+ the XMPP.
+
+ Also see <http://www.xmpp.org/extensions/xep-0082.html>.
+
+ Methods:
+ date -- Create a time stamp using the Date profile.
+ datetime -- Create a time stamp using the DateTime profile.
+ time -- Create a time stamp using the Time profile.
+ format_date -- Format an existing date object.
+ format_datetime -- Format an existing datetime object.
+ format_time -- Format an existing time object.
+ parse -- Convert a time string into a Python datetime object.
+ """
+
+ def plugin_init(self):
+ """Start the XEP-0082 plugin."""
+ self.xep = '0082'
+ self.description = 'XMPP Date and Time Profiles'
+
+ self.date = date
+ self.datetime = datetime
+ self.time = time
+ self.format_date = format_date
+ self.format_datetime = format_datetime
+ self.format_time = format_time
+ self.parse = parse
diff --git a/sleekxmpp/plugins/xep_0092/version.py b/sleekxmpp/plugins/xep_0092/version.py
index 1ca6c15e..ac0924b8 100644
--- a/sleekxmpp/plugins/xep_0092/version.py
+++ b/sleekxmpp/plugins/xep_0092/version.py
@@ -35,7 +35,7 @@ class xep_0092(base_plugin):
self.stanza = sleekxmpp.plugins.xep_0092.stanza
self.name = self.config.get('name', 'SleekXMPP')
- self.version = self.config.get('version', '0.1-dev')
+ self.version = self.config.get('version', sleekxmpp.__version__)
self.os = self.config.get('os', '')
self.getVersion = self.get_version
diff --git a/sleekxmpp/plugins/xep_0199/ping.py b/sleekxmpp/plugins/xep_0199/ping.py
index d1e08e61..0fa22f8a 100644
--- a/sleekxmpp/plugins/xep_0199/ping.py
+++ b/sleekxmpp/plugins/xep_0199/ping.py
@@ -108,7 +108,7 @@ class xep_0199(base_plugin):
iq -- The ping request.
"""
log.debug("Pinged by %s" % iq['from'])
- iq.reply().enable('ping').send()
+ iq.reply().send()
def send_ping(self, jid, timeout=None, errorfalse=False,
ifrom=None, block=True, callback=None):
diff --git a/sleekxmpp/plugins/xep_0202.py b/sleekxmpp/plugins/xep_0202.py
deleted file mode 100644
index 3b31c97a..00000000
--- a/sleekxmpp/plugins/xep_0202.py
+++ /dev/null
@@ -1,117 +0,0 @@
-"""
- SleekXMPP: The Sleek XMPP Library
- Copyright (C) 2010 Nathanael C. Fritz
- This file is part of SleekXMPP.
-
- See the file LICENSE for copying permission.
-"""
-
-from datetime import datetime, tzinfo
-import logging
-import time
-
-from . import base
-from .. stanza.iq import Iq
-from .. xmlstream.handler.callback import Callback
-from .. xmlstream.matcher.xpath import MatchXPath
-from .. xmlstream import ElementBase, ET, JID, register_stanza_plugin
-
-
-log = logging.getLogger(__name__)
-
-
-class EntityTime(ElementBase):
- name = 'time'
- namespace = 'urn:xmpp:time'
- plugin_attrib = 'entity_time'
- interfaces = set(('tzo', 'utc'))
- sub_interfaces = set(('tzo', 'utc'))
-
- #def get_tzo(self):
- # TODO: Right now it returns a string but maybe it should
- # return a datetime.tzinfo object or maybe a datetime.timedelta?
- #pass
-
- def set_tzo(self, tzo):
- if isinstance(tzo, tzinfo):
- td = datetime.now(tzo).utcoffset() # What if we are faking the time? datetime.now() shouldn't be used here'
- seconds = td.seconds + td.days * 24 * 3600
- sign = ('+' if seconds >= 0 else '-')
- minutes = abs(seconds // 60)
- tzo = '{sign}{hours:02d}:{minutes:02d}'.format(sign=sign, hours=minutes//60, minutes=minutes%60)
- elif not isinstance(tzo, str):
- raise TypeError('The time should be a string or a datetime.tzinfo object.')
- self._set_sub_text('tzo', tzo)
-
- def get_utc(self):
- # Returns a datetime object instead the string. Is this a good idea?
- value = self._get_sub_text('utc')
- if '.' in value:
- return datetime.strptime(value, '%Y-%m-%dT%H:%M:%S.%fZ')
- else:
- return datetime.strptime(value, '%Y-%m-%dT%H:%M:%SZ')
-
- def set_utc(self, tim=None):
- if isinstance(tim, datetime):
- if tim.utcoffset():
- tim = tim - tim.utcoffset()
- tim = tim.strftime('%Y-%m-%dT%H:%M:%SZ')
- elif isinstance(tim, time.struct_time):
- tim = time.strftime('%Y-%m-%dT%H:%M:%SZ', tim)
- elif not isinstance(tim, str):
- raise TypeError('The time should be a string or a datetime.datetime or time.struct_time object.')
-
- self._set_sub_text('utc', tim)
-
-
-class xep_0202(base.base_plugin):
- """
- XEP-0202 Entity Time
- """
- def plugin_init(self):
- self.description = "Entity Time"
- self.xep = "0202"
-
- self.xmpp.registerHandler(
- Callback('Time Request',
- MatchXPath('{%s}iq/{%s}time' % (self.xmpp.default_ns,
- EntityTime.namespace)),
- self.handle_entity_time_query))
- register_stanza_plugin(Iq, EntityTime)
-
- self.xmpp.add_event_handler('entity_time_request', self.handle_entity_time)
-
-
- def post_init(self):
- base.base_plugin.post_init(self)
-
- self.xmpp.plugin['xep_0030'].add_feature('urn:xmpp:time')
-
- def handle_entity_time_query(self, iq):
- if iq['type'] == 'get':
- log.debug("Entity time requested by %s" % iq['from'])
- self.xmpp.event('entity_time_request', iq)
- elif iq['type'] == 'result':
- log.debug("Entity time result from %s" % iq['from'])
- self.xmpp.event('entity_time', iq)
-
- def handle_entity_time(self, iq):
- iq = iq.reply()
- iq.enable('entity_time')
- tzo = time.strftime('%z') # %z is not on all ANSI C libraries
- tzo = tzo[:3] + ':' + tzo[3:]
- iq['entity_time']['tzo'] = tzo
- iq['entity_time']['utc'] = datetime.utcnow()
- iq.send()
-
- def get_entity_time(self, jid):
- iq = self.xmpp.makeIqGet()
- iq.enable('entity_time')
- iq.attrib['to'] = jid
- iq.attrib['from'] = self.xmpp.boundjid.full
- id = iq.get('id')
- result = iq.send()
- if result and result is not None and result.get('type', 'error') != 'error':
- return {'utc': result['entity_time']['utc'], 'tzo': result['entity_time']['tzo']}
- else:
- return False
diff --git a/sleekxmpp/plugins/xep_0202/__init__.py b/sleekxmpp/plugins/xep_0202/__init__.py
new file mode 100644
index 00000000..a34b2376
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0202/__init__.py
@@ -0,0 +1,12 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+
+from sleekxmpp.plugins.xep_0202 import stanza
+from sleekxmpp.plugins.xep_0202.stanza import EntityTime
+from sleekxmpp.plugins.xep_0202.time import xep_0202
diff --git a/sleekxmpp/plugins/xep_0202/stanza.py b/sleekxmpp/plugins/xep_0202/stanza.py
new file mode 100644
index 00000000..b6ccc960
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0202/stanza.py
@@ -0,0 +1,127 @@
+"""
+ 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 logging
+import datetime as dt
+
+from sleekxmpp.xmlstream import ElementBase
+from sleekxmpp.plugins import xep_0082
+from sleekxmpp.thirdparty import tzutc, tzoffset
+
+
+class EntityTime(ElementBase):
+
+ """
+ The <time> element represents the local time for an XMPP agent.
+ The time is expressed in UTC to make synchronization easier
+ between entities, but the offset for the local timezone is also
+ included.
+
+ Example <time> stanzas:
+ <iq type="result">
+ <time xmlns="urn:xmpp:time">
+ <utc>2011-07-03T11:37:12.234569</utc>
+ <tzo>-07:00</tzo>
+ </time>
+ </iq>
+
+ Stanza Interface:
+ time -- The local time for the entity (updates utc and tzo).
+ utc -- The UTC equivalent to local time.
+ tzo -- The local timezone offset from UTC.
+
+ Methods:
+ get_time -- Return local time datetime object.
+ set_time -- Set UTC and TZO fields.
+ del_time -- Remove both UTC and TZO fields.
+ get_utc -- Return datetime object of UTC time.
+ set_utc -- Set the UTC time.
+ get_tzo -- Return tzinfo object.
+ set_tzo -- Set the local timezone offset.
+ """
+
+ name = 'time'
+ namespace = 'urn:xmpp:time'
+ plugin_attrib = 'entity_time'
+ interfaces = set(('tzo', 'utc', 'time'))
+ sub_interfaces = interfaces
+
+ def set_time(self, value):
+ """
+ Set both the UTC and TZO fields given a time object.
+
+ Arguments:
+ value -- A datetime object or properly formatted
+ string equivalent.
+ """
+ date = value
+ if not isinstance(value, dt.datetime):
+ date = xep_0082.parse(value)
+ self['utc'] = date
+ self['tzo'] = date.tzinfo
+
+ def get_time(self):
+ """
+ Return the entity's local time based on the UTC and TZO data.
+ """
+ date = self['utc']
+ tz = self['tzo']
+ return date.astimezone(tz)
+
+ def del_time(self):
+ """Remove both the UTC and TZO fields."""
+ del self['utc']
+ del self['tzo']
+
+ def get_tzo(self):
+ """
+ Return the timezone offset from UTC as a tzinfo object.
+ """
+ tzo = self._get_sub_text('tzo')
+ if tzo == '':
+ tzo = 'Z'
+ time = xep_0082.parse('00:00:00%s' % tzo)
+ return time.tzinfo
+
+ def set_tzo(self, value):
+ """
+ Set the timezone offset from UTC.
+
+ Arguments:
+ value -- Either a tzinfo object or the number of
+ seconds (positive or negative) to offset.
+ """
+ time = xep_0082.time(offset=value)
+ if xep_0082.parse(time).tzinfo == tzutc():
+ self._set_sub_text('tzo', 'Z')
+ else:
+ self._set_sub_text('tzo', time[-6:])
+
+ def get_utc(self):
+ """
+ Return the time in UTC as a datetime object.
+ """
+ value = self._get_sub_text('utc')
+ if value == '':
+ return xep_0082.parse(xep_0082.datetime())
+ return xep_0082.parse('%sZ' % value)
+
+ def set_utc(self, value):
+ """
+ Set the time in UTC.
+
+ Arguments:
+ value -- A datetime object or properly formatted
+ string equivalent.
+ """
+ date = value
+ if not isinstance(value, dt.datetime):
+ date = xep_0082.parse(value)
+ date = date.astimezone(tzutc())
+ value = xep_0082.format_datetime(date)[:-1]
+ self._set_sub_text('utc', value)
diff --git a/sleekxmpp/plugins/xep_0202/time.py b/sleekxmpp/plugins/xep_0202/time.py
new file mode 100644
index 00000000..bcad8bc8
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0202/time.py
@@ -0,0 +1,92 @@
+"""
+ 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 logging
+
+from sleekxmpp.stanza.iq import Iq
+from sleekxmpp.xmlstream import register_stanza_plugin
+from sleekxmpp.xmlstream.handler import Callback
+from sleekxmpp.xmlstream.matcher import StanzaPath
+from sleekxmpp.plugins.base import base_plugin
+from sleekxmpp.plugins import xep_0082
+from sleekxmpp.plugins.xep_0202 import stanza
+
+
+log = logging.getLogger(__name__)
+
+
+class xep_0202(base_plugin):
+
+ """
+ XEP-0202: Entity Time
+ """
+
+ def plugin_init(self):
+ """Start the XEP-0203 plugin."""
+ self.xep = '0202'
+ self.description = 'Entity Time'
+ self.stanza = stanza
+
+ self.tz_offset = self.config.get('tz_offset', 0)
+
+ # As a default, respond to time requests with the
+ # local time returned by XEP-0082. However, a
+ # custom function can be supplied which accepts
+ # the JID of the entity to query for the time.
+ self.local_time = self.config.get('local_time', None)
+ if not self.local_time:
+ self.local_time = lambda x: xep_0082.datetime(offset=self.tz_offset)
+
+ self.xmpp.registerHandler(
+ Callback('Entity Time',
+ StanzaPath('iq/entity_time'),
+ self._handle_time_request))
+ register_stanza_plugin(Iq, stanza.EntityTime)
+
+ def post_init(self):
+ """Handle cross-plugin interactions."""
+ base_plugin.post_init(self)
+ self.xmpp['xep_0030'].add_feature('urn:xmpp:time')
+
+
+ def _handle_time_request(self, iq):
+ """
+ Respond to a request for the local time.
+
+ The time is taken from self.local_time(), which may be replaced
+ during plugin configuration with a function that maps JIDs to
+ times.
+
+ Arguments:
+ iq -- The Iq time request stanza.
+ """
+ iq.reply()
+ iq['entity_time']['time'] = self.local_time(iq['to'])
+ iq.send()
+
+ def get_entity_time(self, to, ifrom=None, **iqargs):
+ """
+ Request the time from another entity.
+
+ Arguments:
+ to -- JID of the entity to query.
+ ifrom -- Specifiy the sender's JID.
+ block -- If true, block and wait for the stanzas' reply.
+ timeout -- The time in seconds to block while waiting for
+ a reply. If None, then wait indefinitely.
+ callback -- Optional callback to execute when a reply is
+ received instead of blocking and waiting for
+ the reply.
+ """
+ iq = self.xmpp.Iq()
+ iq['type'] = 'get'
+ iq['to'] = 'to'
+ if ifrom:
+ iq['from'] = 'ifrom'
+ iq.enable('entity_time')
+ return iq.send(**iqargs)
diff --git a/sleekxmpp/plugins/xep_0203/__init__.py b/sleekxmpp/plugins/xep_0203/__init__.py
new file mode 100644
index 00000000..445ccf37
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0203/__init__.py
@@ -0,0 +1,12 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+from sleekxmpp.plugins.xep_0203 import stanza
+from sleekxmpp.plugins.xep_0203.stanza import Delay
+from sleekxmpp.plugins.xep_0203.delay import xep_0203
+
diff --git a/sleekxmpp/plugins/xep_0203/delay.py b/sleekxmpp/plugins/xep_0203/delay.py
new file mode 100644
index 00000000..8ff14d18
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0203/delay.py
@@ -0,0 +1,36 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+
+from sleekxmpp.stanza import Message, Presence
+from sleekxmpp.xmlstream import register_stanza_plugin
+from sleekxmpp.plugins.base import base_plugin
+from sleekxmpp.plugins.xep_0203 import stanza
+
+
+class xep_0203(base_plugin):
+
+ """
+ XEP-0203: Delayed Delivery
+
+ XMPP stanzas are sometimes withheld for delivery due to the recipient
+ being offline, or are resent in order to establish recent history as
+ is the case with MUCS. In any case, it is important to know when the
+ stanza was originally sent, not just when it was last received.
+
+ Also see <http://www.xmpp.org/extensions/xep-0203.html>.
+ """
+
+ def plugin_init(self):
+ """Start the XEP-0203 plugin."""
+ self.xep = '0203'
+ self.description = 'Delayed Delivery'
+ self.stanza = stanza
+
+ register_stanza_plugin(Message, stanza.Delay)
+ register_stanza_plugin(Presence, stanza.Delay)
diff --git a/sleekxmpp/plugins/xep_0203/stanza.py b/sleekxmpp/plugins/xep_0203/stanza.py
new file mode 100644
index 00000000..baae4cd3
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0203/stanza.py
@@ -0,0 +1,41 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+import datetime as dt
+
+from sleekxmpp.xmlstream import ElementBase
+from sleekxmpp.plugins import xep_0082
+
+
+class Delay(ElementBase):
+
+ """
+ """
+
+ name = 'delay'
+ namespace = 'urn:xmpp:delay'
+ plugin_attrib = 'delay'
+ interfaces = set(('from', 'stamp', 'text'))
+
+ def get_stamp(self):
+ timestamp = self._get_attr('stamp')
+ return xep_0082.parse(timestamp)
+
+ def set_stamp(self, value):
+ if isinstance(value, dt.datetime):
+ value = xep_0082.format_datetime(value)
+ self._set_attr('stamp', value)
+
+ def get_text(self):
+ return self.xml.text
+
+ def set_text(self, value):
+ self.xml.text = value
+
+ def del_text(self):
+ self.xml.text = ''
diff --git a/sleekxmpp/plugins/xep_0224/__init__.py b/sleekxmpp/plugins/xep_0224/__init__.py
new file mode 100644
index 00000000..62f5bf82
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0224/__init__.py
@@ -0,0 +1,11 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+from sleekxmpp.plugins.xep_0224 import stanza
+from sleekxmpp.plugins.xep_0224.stanza import Attention
+from sleekxmpp.plugins.xep_0224.attention import xep_0224
diff --git a/sleekxmpp/plugins/xep_0224/attention.py b/sleekxmpp/plugins/xep_0224/attention.py
new file mode 100644
index 00000000..41d7a0f1
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0224/attention.py
@@ -0,0 +1,72 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+import logging
+
+from sleekxmpp.stanza import Message
+from sleekxmpp.xmlstream import register_stanza_plugin
+from sleekxmpp.xmlstream.handler import Callback
+from sleekxmpp.xmlstream.matcher import StanzaPath
+from sleekxmpp.plugins.base import base_plugin
+from sleekxmpp.plugins.xep_0224 import stanza
+
+
+log = logging.getLogger(__name__)
+
+
+class xep_0224(base_plugin):
+
+ """
+ XEP-0224: Attention
+ """
+
+ def plugin_init(self):
+ """Start the XEP-0224 plugin."""
+ self.xep = '0224'
+ self.description = 'Attention'
+ self.stanza = stanza
+
+ register_stanza_plugin(Message, stanza.Attention)
+
+ self.xmpp.register_handler(
+ Callback('Attention',
+ StanzaPath('message/attention'),
+ self._handle_attention))
+
+ def post_init(self):
+ """Handle cross-plugin dependencies."""
+ base_plugin.post_init(self)
+ self.xmpp['xep_0030'].add_feature(stanza.Attention.namespace)
+
+ def request_attention(self, to, mfrom=None, mbody=''):
+ """
+ Send an attention message with an optional body.
+
+ Arguments:
+ to -- The attention request recipient's JID.
+ mfrom -- Optionally specify the sender of the attention request.
+ mbody -- An optional message body to include in the request.
+ """
+ m = self.xmpp.Message()
+ m['to'] = to
+ m['type'] = 'headline'
+ m['attention'] = True
+ if mfrom:
+ m['from'] = mfrom
+ m['body'] = mbody
+ m.send()
+
+ def _handle_attention(self, msg):
+ """
+ Raise an event after receiving a message with an attention request.
+
+ Arguments:
+ msg -- A message stanza with an attention element.
+ """
+ log.debug("Received attention request from: %s" % msg['from'])
+ self.xmpp.event('attention', msg)
diff --git a/sleekxmpp/plugins/xep_0224/stanza.py b/sleekxmpp/plugins/xep_0224/stanza.py
new file mode 100644
index 00000000..f15172d9
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0224/stanza.py
@@ -0,0 +1,40 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+from sleekxmpp.xmlstream import ElementBase, ET
+
+
+class Attention(ElementBase):
+
+ """
+ """
+
+ name = 'attention'
+ namespace = 'urn:xmpp:attention:0'
+ plugin_attrib = 'attention'
+ interfaces = set(('attention',))
+ is_extension = True
+
+ def setup(self, xml):
+ return True
+
+ def set_attention(self, value):
+ if value:
+ xml = ET.Element(self.tag_name())
+ self.parent().xml.append(xml)
+ else:
+ self.del_attention()
+
+ def get_attention(self):
+ xml = self.parent().xml.find(self.tag_name())
+ return xml is not None
+
+ def del_attention(self):
+ xml = self.parent().xml.find(self.tag_name())
+ if xml is not None:
+ self.parent().xml.remove(xml)
diff --git a/sleekxmpp/stanza/__init__.py b/sleekxmpp/stanza/__init__.py
index dbf7b86f..4bd37dc5 100644
--- a/sleekxmpp/stanza/__init__.py
+++ b/sleekxmpp/stanza/__init__.py
@@ -8,7 +8,8 @@
from sleekxmpp.stanza.error import Error
-from sleekxmpp.stanza.stream_error import StreamError
from sleekxmpp.stanza.iq import Iq
from sleekxmpp.stanza.message import Message
from sleekxmpp.stanza.presence import Presence
+from sleekxmpp.stanza.stream_features import StreamFeatures
+from sleekxmpp.stanza.stream_error import StreamError
diff --git a/sleekxmpp/stanza/error.py b/sleekxmpp/stanza/error.py
index 5d1ce50d..93231a48 100644
--- a/sleekxmpp/stanza/error.py
+++ b/sleekxmpp/stanza/error.py
@@ -88,7 +88,9 @@ class Error(ElementBase):
"""Return the condition element's name."""
for child in self.xml.getchildren():
if "{%s}" % self.condition_ns in child.tag:
- return child.tag.split('}', 1)[-1]
+ cond = child.tag.split('}', 1)[-1]
+ if cond in self.conditions:
+ return cond
return ''
def set_condition(self, value):
diff --git a/sleekxmpp/stanza/message.py b/sleekxmpp/stanza/message.py
index cb3d344c..3518fc7a 100644
--- a/sleekxmpp/stanza/message.py
+++ b/sleekxmpp/stanza/message.py
@@ -97,7 +97,7 @@ class Message(RootStanza):
clear -- Indicates if existing content should be removed
before replying. Defaults to True.
"""
- StanzaBase.reply(self)
+ StanzaBase.reply(self, clear)
if self['type'] == 'groupchat':
self['to'] = self['to'].bare
diff --git a/sleekxmpp/stanza/rootstanza.py b/sleekxmpp/stanza/rootstanza.py
index bc11476e..9e1d1cfa 100644
--- a/sleekxmpp/stanza/rootstanza.py
+++ b/sleekxmpp/stanza/rootstanza.py
@@ -64,8 +64,7 @@ class RootStanza(StanzaBase):
# log the error
log.exception('Error handling {%s}%s stanza' %
(self.namespace, self.name))
- # Finally raise the exception, so it can be handled (or not)
- # at a higher level by using sys.excepthook.
- raise e
+ # Finally raise the exception to a global exception handler
+ self.stream.exception(e)
register_stanza_plugin(RootStanza, Error)
diff --git a/sleekxmpp/stanza/stream_features.py b/sleekxmpp/stanza/stream_features.py
new file mode 100644
index 00000000..b800011f
--- /dev/null
+++ b/sleekxmpp/stanza/stream_features.py
@@ -0,0 +1,54 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+from sleekxmpp.xmlstream import ElementBase, StanzaBase, ET
+from sleekxmpp.xmlstream import register_stanza_plugin
+
+
+class StreamFeatures(StanzaBase):
+
+ """
+ """
+
+ name = 'features'
+ namespace = 'http://etherx.jabber.org/streams'
+ interfaces = set(('features', 'required', 'optional'))
+ sub_interfaces = interfaces
+ plugin_tag_map = {}
+ plugin_attrib_map = {}
+
+ def setup(self, xml):
+ StanzaBase.setup(self, xml)
+ self.values = self.values
+
+ def get_features(self):
+ """
+ """
+ return self.plugins
+
+ def set_features(self, value):
+ """
+ """
+ pass
+
+ def del_features(self):
+ """
+ """
+ pass
+
+ def get_required(self):
+ """
+ """
+ features = self['features']
+ return [f for n, f in features.items() if f['required']]
+
+ def get_optional(self):
+ """
+ """
+ features = self['features']
+ return [f for n, f in features.items() if not f['required']]
diff --git a/sleekxmpp/test/sleektest.py b/sleekxmpp/test/sleektest.py
index 7802a9bc..cb5031f7 100644
--- a/sleekxmpp/test/sleektest.py
+++ b/sleekxmpp/test/sleektest.py
@@ -318,9 +318,11 @@ class SleekTest(unittest.TestCase):
self.xmpp.socket.recv_data(header)
elif socket == 'live':
self.xmpp.socket_class = TestLiveSocket
+
def wait_for_session(x):
self.xmpp.socket.clear()
skip_queue.put('started')
+
self.xmpp.add_event_handler('session_start', wait_for_session)
self.xmpp.connect()
else:
diff --git a/sleekxmpp/thirdparty/__init__.py b/sleekxmpp/thirdparty/__init__.py
index 276ac3cc..1c7bf651 100644
--- a/sleekxmpp/thirdparty/__init__.py
+++ b/sleekxmpp/thirdparty/__init__.py
@@ -2,3 +2,6 @@ try:
from collections import OrderedDict
except:
from sleekxmpp.thirdparty.ordereddict import OrderedDict
+
+from sleekxmpp.thirdparty import suelta
+from sleekxmpp.thirdparty.mini_dateutil import tzutc, tzoffset, parse_iso
diff --git a/sleekxmpp/thirdparty/mini_dateutil.py b/sleekxmpp/thirdparty/mini_dateutil.py
new file mode 100644
index 00000000..6af5ffde
--- /dev/null
+++ b/sleekxmpp/thirdparty/mini_dateutil.py
@@ -0,0 +1,267 @@
+# This module is a very stripped down version of the dateutil
+# package for when dateutil has not been installed. As a replacement
+# for dateutil.parser.parse, the parsing methods from
+# http://blog.mfabrik.com/2008/06/30/relativity-of-time-shortcomings-in-python-datetime-and-workaround/
+
+#As such, the following copyrights and licenses applies:
+
+
+# dateutil - Extensions to the standard python 2.3+ datetime module.
+#
+# Copyright (c) 2003-2011 - Gustavo Niemeyer <gustavo@niemeyer.net>
+#
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#
+# * Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+# * Neither the name of the copyright holder nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+# fixed_dateime
+#
+# Copyright (c) 2008, Red Innovation Ltd., Finland
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+# * Neither the name of Red Innovation nor the names of its contributors
+# may be used to endorse or promote products derived from this software
+# without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY RED INNOVATION ``AS IS'' AND ANY
+# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+# DISCLAIMED. IN NO EVENT SHALL RED INNOVATION BE LIABLE FOR ANY
+# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+
+import re
+import datetime
+
+
+ZERO = datetime.timedelta(0)
+
+
+try:
+ from dateutil.parser import parse as parse_iso
+ from dateutil.tz import tzoffset, tzutc
+except:
+ # As a stopgap, define the two timezones here based
+ # on the dateutil code.
+
+ class tzutc(datetime.tzinfo):
+
+ def utcoffset(self, dt):
+ return ZERO
+
+ def dst(self, dt):
+ return ZERO
+
+ def tzname(self, dt):
+ return "UTC"
+
+ def __eq__(self, other):
+ return (isinstance(other, tzutc) or
+ (isinstance(other, tzoffset) and other._offset == ZERO))
+
+ def __ne__(self, other):
+ return not self.__eq__(other)
+
+ def __repr__(self):
+ return "%s()" % self.__class__.__name__
+
+ __reduce__ = object.__reduce__
+
+ class tzoffset(datetime.tzinfo):
+
+ def __init__(self, name, offset):
+ self._name = name
+ self._offset = datetime.timedelta(seconds=offset)
+
+ def utcoffset(self, dt):
+ return self._offset
+
+ def dst(self, dt):
+ return ZERO
+
+ def tzname(self, dt):
+ return self._name
+
+ def __eq__(self, other):
+ return (isinstance(other, tzoffset) and
+ self._offset == other._offset)
+
+ def __ne__(self, other):
+ return not self.__eq__(other)
+
+ def __repr__(self):
+ return "%s(%s, %s)" % (self.__class__.__name__,
+ repr(self._name),
+ self._offset.days*86400+self._offset.seconds)
+
+ __reduce__ = object.__reduce__
+
+
+ _fixed_offset_tzs = { }
+ UTC = tzutc()
+
+ def _get_fixed_offset_tz(offsetmins):
+ """For internal use only: Returns a tzinfo with
+ the given fixed offset. This creates only one instance
+ for each offset; the zones are kept in a dictionary"""
+
+ if offsetmins == 0:
+ return UTC
+
+ if not offsetmins in _fixed_offset_tzs:
+ if offsetmins < 0:
+ sign = '-'
+ absoff = -offsetmins
+ else:
+ sign = '+'
+ absoff = offsetmins
+
+ name = "UTC%s%02d:%02d" % (sign, int(absoff / 60), absoff % 60)
+ inst = tzoffset(offsetmins, name)
+ _fixed_offset_tzs[offsetmins] = inst
+
+ return _fixed_offset_tzs[offsetmins]
+
+
+ _iso8601_parser = re.compile("""
+ ^
+ (?P<year> [0-9]{4})?(?P<ymdsep>-?)?
+ (?P<month>[0-9]{2})?(?P=ymdsep)?
+ (?P<day> [0-9]{2})?
+
+ (?: # time part... optional... at least hour must be specified
+ (?:T|\s+)?
+ (?P<hour>[0-9]{2})
+ (?:
+ # minutes, separated with :, or none, from hours
+ (?P<hmssep>[:]?)
+ (?P<minute>[0-9]{2})
+ (?:
+ # same for seconds, separated with :, or none, from hours
+ (?P=hmssep)
+ (?P<second>[0-9]{2})
+ )?
+ )?
+
+ # fractions
+ (?: [,.] (?P<frac>[0-9]{1,10}))?
+
+ # timezone, Z, +-hh or +-hh:?mm. MUST BE, but complain if not there.
+ (
+ (?P<tzempty>Z)
+ |
+ (?P<tzh>[+-][0-9]{2})
+ (?: :? # optional separator
+ (?P<tzm>[0-9]{2})
+ )?
+ )?
+ )?
+ $
+ """, re.X) # """
+
+ def parse_iso(timestamp):
+ """Internal function for parsing a timestamp in
+ ISO 8601 format"""
+
+ timestamp = timestamp.strip()
+
+ m = _iso8601_parser.match(timestamp)
+ if not m:
+ raise ValueError("Not a proper ISO 8601 timestamp!: %s" % timestamp)
+
+ vals = m.groupdict()
+ def_vals = {'year': 1970, 'month': 1, 'day': 1}
+ for key in vals:
+ if vals[key] is None:
+ vals[key] = def_vals.get(key, 0)
+ elif key not in ['ymdsep', 'hmssep', 'tzempty']:
+ vals[key] = int(vals[key])
+
+ year = vals['year']
+ month = vals['month']
+ day = vals['day']
+
+ h, min, s, us = None, None, None, 0
+ frac = 0
+ if m.group('tzempty') == None and m.group('tzh') == None:
+ raise ValueError("Not a proper ISO 8601 timestamp: " +
+ "missing timezone (Z or +hh[:mm])!")
+
+ if m.group('frac'):
+ frac = m.group('frac')
+ power = len(frac)
+ frac = int(frac) / 10.0 ** power
+
+ if m.group('hour'):
+ h = vals['hour']
+
+ if m.group('minute'):
+ min = vals['minute']
+
+ if m.group('second'):
+ s = vals['second']
+
+ if frac != None:
+ # ok, fractions of hour?
+ if min == None:
+ frac, min = _math.modf(frac * 60.0)
+ min = int(min)
+
+ # fractions of second?
+ if s == None:
+ frac, s = _math.modf(frac * 60.0)
+ s = int(s)
+
+ # and extract microseconds...
+ us = int(frac * 1000000)
+
+ if m.group('tzempty') == 'Z':
+ offsetmins = 0
+ else:
+ # timezone: hour diff with sign
+ offsetmins = vals['tzh'] * 60
+ tzm = m.group('tzm')
+
+ # add optional minutes
+ if tzm != None:
+ tzm = int(tzm)
+ offsetmins += tzm if offsetmins > 0 else -tzm
+
+ tz = _get_fixed_offset_tz(offsetmins)
+ return datetime.datetime(year, month, day, h, min, s, us, tz)
diff --git a/sleekxmpp/thirdparty/suelta/LICENSE b/sleekxmpp/thirdparty/suelta/LICENSE
new file mode 100644
index 00000000..6eee4f33
--- /dev/null
+++ b/sleekxmpp/thirdparty/suelta/LICENSE
@@ -0,0 +1,21 @@
+This software is subject to "The MIT License"
+
+Copyright 2007-2010 David Alan Cridland
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/sleekxmpp/thirdparty/suelta/PLAYING-NICELY b/sleekxmpp/thirdparty/suelta/PLAYING-NICELY
new file mode 100644
index 00000000..393b8078
--- /dev/null
+++ b/sleekxmpp/thirdparty/suelta/PLAYING-NICELY
@@ -0,0 +1,27 @@
+Hi.
+
+This is a short note explaining the license in non-legally-binding terms, and
+describing how I hope to see people work with the licensing.
+
+First off, the license is permissive, and more or less allows you to do
+anything, as long as you leave my credit and copyright intact.
+
+You can, and are very much welcome to, include this in commercial works, and
+in code that has tightly controlled distribution, as well as open-source.
+
+If it doesn't work - and I have no doubt that there are bugs - then this is
+largely your problem.
+
+If you do find a bug, though, do let me know - although you don't have to.
+
+And if you fix it, I'd greatly appreciate a patch, too. Please give me a
+licensing statement, and a copyright statement, along with your patch.
+
+Similarly, any enhancements are welcome, and also will need copyright and
+licensing. Please stick to a license which is compatible with the MIT license,
+and consider assignment (as required) to me to simplify licensing. (Public
+domain does not exist in the UK, sorry).
+
+Thanks,
+
+Dave.
diff --git a/sleekxmpp/thirdparty/suelta/README b/sleekxmpp/thirdparty/suelta/README
new file mode 100644
index 00000000..c32463a4
--- /dev/null
+++ b/sleekxmpp/thirdparty/suelta/README
@@ -0,0 +1,8 @@
+Suelta - A pure-Python SASL client library
+
+Suelta is a SASL library, providing you with authentication and in some cases
+security layers.
+
+It supports a wide range of typical SASL mechanisms, including the MTI for
+all known protocols.
+
diff --git a/sleekxmpp/thirdparty/suelta/__init__.py b/sleekxmpp/thirdparty/suelta/__init__.py
new file mode 100644
index 00000000..04f0cbad
--- /dev/null
+++ b/sleekxmpp/thirdparty/suelta/__init__.py
@@ -0,0 +1,26 @@
+# Copyright 2007-2010 David Alan Cridland
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+
+from sleekxmpp.thirdparty.suelta.saslprep import saslprep
+from sleekxmpp.thirdparty.suelta.sasl import *
+from sleekxmpp.thirdparty.suelta.mechanisms import *
+
+__version__ = '2.0'
+__version_info__ = (2, 0, 0)
diff --git a/sleekxmpp/thirdparty/suelta/exceptions.py b/sleekxmpp/thirdparty/suelta/exceptions.py
new file mode 100644
index 00000000..625cca0e
--- /dev/null
+++ b/sleekxmpp/thirdparty/suelta/exceptions.py
@@ -0,0 +1,31 @@
+class SASLError(Exception):
+
+ def __init__(self, sasl, text, mech=None):
+ """
+ :param sasl: The main `suelta.SASL` object.
+ :param text: Descpription of the error.
+ :param mech: Optional reference to the mechanism object.
+
+ :type sasl: `suelta.SASL`
+ """
+ self.sasl = sasl
+ self.text = text
+ self.mech = mech
+
+ def __str__(self):
+ if self.mech is None:
+ return 'SASL Error: %s' % self.text
+ else:
+ return 'SASL Error (%s): %s' % (self.mech, self.text)
+
+
+class SASLCancelled(SASLError):
+
+ def __init__(self, sasl, mech=None):
+ """
+ :param sasl: The main `suelta.SASL` object.
+ :param mech: Optional reference to the mechanism object.
+
+ :type sasl: `suelta.SASL`
+ """
+ super(SASLCancelled, self).__init__(sasl, "User cancelled", mech)
diff --git a/sleekxmpp/thirdparty/suelta/mechanisms/__init__.py b/sleekxmpp/thirdparty/suelta/mechanisms/__init__.py
new file mode 100644
index 00000000..5cb2ee3d
--- /dev/null
+++ b/sleekxmpp/thirdparty/suelta/mechanisms/__init__.py
@@ -0,0 +1,5 @@
+from sleekxmpp.thirdparty.suelta.mechanisms.anonymous import ANONYMOUS
+from sleekxmpp.thirdparty.suelta.mechanisms.plain import PLAIN
+from sleekxmpp.thirdparty.suelta.mechanisms.cram_md5 import CRAM_MD5
+from sleekxmpp.thirdparty.suelta.mechanisms.digest_md5 import DIGEST_MD5
+from sleekxmpp.thirdparty.suelta.mechanisms.scram_hmac import SCRAM_HMAC
diff --git a/sleekxmpp/thirdparty/suelta/mechanisms/anonymous.py b/sleekxmpp/thirdparty/suelta/mechanisms/anonymous.py
new file mode 100644
index 00000000..e44e91a2
--- /dev/null
+++ b/sleekxmpp/thirdparty/suelta/mechanisms/anonymous.py
@@ -0,0 +1,36 @@
+from sleekxmpp.thirdparty.suelta.sasl import Mechanism, register_mechanism
+from sleekxmpp.thirdparty.suelta.exceptions import SASLError, SASLCancelled
+
+
+class ANONYMOUS(Mechanism):
+
+ """
+ """
+
+ def __init__(self, sasl, name):
+ """
+ """
+ super(ANONYMOUS, self).__init__(sasl, name, 0)
+
+ def get_values(self):
+ """
+ """
+ return {}
+
+ def process(self, challenge=None):
+ """
+ """
+ return b'Anonymous, Suelta'
+
+ def okay(self):
+ """
+ """
+ return True
+
+ def get_user(self):
+ """
+ """
+ return 'anonymous'
+
+
+register_mechanism('ANONYMOUS', 0, ANONYMOUS, use_hashes=False)
diff --git a/sleekxmpp/thirdparty/suelta/mechanisms/cram_md5.py b/sleekxmpp/thirdparty/suelta/mechanisms/cram_md5.py
new file mode 100644
index 00000000..ba44befe
--- /dev/null
+++ b/sleekxmpp/thirdparty/suelta/mechanisms/cram_md5.py
@@ -0,0 +1,63 @@
+import sys
+import hmac
+
+from sleekxmpp.thirdparty.suelta.util import hash, bytes
+from sleekxmpp.thirdparty.suelta.sasl import Mechanism, register_mechanism
+from sleekxmpp.thirdparty.suelta.exceptions import SASLError, SASLCancelled
+
+
+class CRAM_MD5(Mechanism):
+
+ """
+ """
+
+ def __init__(self, sasl, name):
+ """
+ """
+ super(CRAM_MD5, self).__init__(sasl, name, 2)
+
+ self.hash = hash(name[5:])
+ if self.hash is None:
+ raise SASLCancelled(self.sasl, self)
+ if not self.sasl.tls_active():
+ if not self.sasl.sec_query(self, 'CRAM-MD5'):
+ raise SASLCancelled(self.sasl, self)
+
+ def prep(self):
+ """
+ """
+ if 'savepass' not in self.values:
+ if self.sasl.sec_query(self, 'CLEAR-PASSWORD'):
+ self.values['savepass'] = True
+
+ if 'savepass' not in self.values:
+ del self.values['password']
+
+ def process(self, challenge):
+ """
+ """
+ if challenge is None:
+ return None
+
+ self.check_values(['username', 'password'])
+ username = bytes(self.values['username'])
+ password = bytes(self.values['password'])
+
+ mac = hmac.HMAC(key=password, digestmod=self.hash)
+
+ mac.update(challenge)
+
+ return username + b' ' + bytes(mac.hexdigest())
+
+ def okay(self):
+ """
+ """
+ return True
+
+ def get_user(self):
+ """
+ """
+ return self.values['username']
+
+
+register_mechanism('CRAM-', 20, CRAM_MD5)
diff --git a/sleekxmpp/thirdparty/suelta/mechanisms/digest_md5.py b/sleekxmpp/thirdparty/suelta/mechanisms/digest_md5.py
new file mode 100644
index 00000000..5492c553
--- /dev/null
+++ b/sleekxmpp/thirdparty/suelta/mechanisms/digest_md5.py
@@ -0,0 +1,273 @@
+import sys
+
+import random
+
+from sleekxmpp.thirdparty.suelta.util import hash, bytes, quote
+from sleekxmpp.thirdparty.suelta.sasl import Mechanism, register_mechanism
+from sleekxmpp.thirdparty.suelta.exceptions import SASLError, SASLCancelled
+
+
+
+def parse_challenge(stuff):
+ """
+ """
+ ret = {}
+ var = b''
+ val = b''
+ in_var = True
+ in_quotes = False
+ new = False
+ escaped = False
+ for c in stuff:
+ if sys.version_info >= (3, 0):
+ c = bytes([c])
+ if in_var:
+ if c.isspace():
+ continue
+ if c == b'=':
+ in_var = False
+ new = True
+ else:
+ var += c
+ else:
+ if new:
+ if c == b'"':
+ in_quotes = True
+ else:
+ val += c
+ new = False
+ elif in_quotes:
+ if escaped:
+ escaped = False
+ val += c
+ else:
+ if c == b'\\':
+ escaped = True
+ elif c == b'"':
+ in_quotes = False
+ else:
+ val += c
+ else:
+ if c == b',':
+ if var:
+ ret[var] = val
+ var = b''
+ val = b''
+ in_var = True
+ else:
+ val += c
+ if var:
+ ret[var] = val
+ return ret
+
+
+class DIGEST_MD5(Mechanism):
+
+ """
+ """
+
+ enc_magic = 'Digest session key to client-to-server signing key magic'
+ dec_magic = 'Digest session key to server-to-client signing key magic'
+
+ def __init__(self, sasl, name):
+ """
+ """
+ super(DIGEST_MD5, self).__init__(sasl, name, 3)
+
+ self.hash = hash(name[7:])
+ if self.hash is None:
+ raise SASLCancelled(self.sasl, self)
+
+ if not self.sasl.tls_active():
+ if not self.sasl.sec_query(self, '-ENCRYPTION, DIGEST-MD5'):
+ raise SASLCancelled(self.sasl, self)
+
+ self._rspauth_okay = False
+ self._digest_uri = None
+ self._a1 = None
+ self._enc_buf = b''
+ self._enc_key = None
+ self._enc_seq = 0
+ self._max_buffer = 65536
+ self._dec_buf = b''
+ self._dec_key = None
+ self._dec_seq = 0
+ self._qops = [b'auth']
+ self._qop = b'auth'
+
+ def MAC(self, seq, msg, key):
+ """
+ """
+ mac = hmac.HMAC(key=key, digestmod=self.hash)
+ seqnum = num_to_bytes(seq)
+ mac.update(seqnum)
+ mac.update(msg)
+ return mac.digest()[:10] + b'\x00\x01' + seqnum
+
+
+ def encode(self, text):
+ """
+ """
+ self._enc_buf += text
+
+ def flush(self):
+ """
+ """
+ result = b''
+ # Leave buffer space for the MAC
+ mbuf = self._max_buffer - 10 - 2 - 4
+
+ while self._enc_buf:
+ msg = self._encbuf[:mbuf]
+ mac = self.MAC(self._enc_seq, msg, self._enc_key, self.hash)
+ self._enc_seq += 1
+ msg += mac
+ result += num_to_bytes(len(msg)) + msg
+ self._enc_buf = self._enc_buf[mbuf:]
+
+ return result
+
+ def decode(self, text):
+ """
+ """
+ self._dec_buf += text
+ result = b''
+
+ while len(self._dec_buf) > 4:
+ num = bytes_to_num(self._dec_buf)
+ if len(self._dec_buf) < (num + 4):
+ return result
+
+ mac = self._dec_buf[4:4 + num]
+ self._dec_buf = self._dec_buf[4 + num:]
+ msg = mac[:-16]
+
+ mac_conf = self.MAC(self._dec_mac, msg, self._dec_key)
+ if mac[-16:] != mac_conf:
+ self._desc_sec = None
+ return result
+
+ self._dec_seq += 1
+ result += msg
+
+ return result
+
+ def response(self):
+ """
+ """
+ vitals = ['username']
+ if not self.has_values(['key_hash']):
+ vitals.append('password')
+ self.check_values(vitals)
+
+ resp = {}
+ if 'auth-int' in self._qops:
+ self._qop = b'auth-int'
+ resp['qop'] = self._qop
+ if 'realm' in self.values:
+ resp['realm'] = quote(self.values['realm'])
+
+ resp['username'] = quote(bytes(self.values['username']))
+ resp['nonce'] = quote(self.values['nonce'])
+ if self.values['nc']:
+ self._cnonce = self.values['cnonce']
+ else:
+ self._cnonce = bytes('%s' % random.random())[2:]
+ resp['cnonce'] = quote(self._cnonce)
+ self.values['nc'] += 1
+ resp['nc'] = bytes('%08x' % self.values['nc'])
+
+ service = bytes(self.sasl.service)
+ host = bytes(self.sasl.host)
+ self._digest_uri = service + b'/' + host
+ resp['digest-uri'] = quote(self._digest_uri)
+
+ a2 = b'AUTHENTICATE:' + self._digest_uri
+ if self._qop != b'auth':
+ a2 += b':00000000000000000000000000000000'
+ resp['maxbuf'] = b'16777215' # 2**24-1
+ resp['response'] = self.gen_hash(a2)
+ return b','.join([bytes(k) + b'=' + bytes(v) for k, v in resp.items()])
+
+ def gen_hash(self, a2):
+ """
+ """
+ if not self.has_values(['key_hash']):
+ key_hash = self.hash()
+ user = bytes(self.values['username'])
+ password = bytes(self.values['password'])
+ realm = bytes(self.values['realm'])
+ kh = user + b':' + realm + b':' + password
+ key_hash.update(kh)
+ self.values['key_hash'] = key_hash.digest()
+
+ a1 = self.hash(self.values['key_hash'])
+ a1h = b':' + self.values['nonce'] + b':' + self._cnonce
+ a1.update(a1h)
+ response = self.hash()
+ self._a1 = a1.digest()
+ rv = bytes(a1.hexdigest().lower())
+ rv += b':' + self.values['nonce']
+ rv += b':' + bytes('%08x' % self.values['nc'])
+ rv += b':' + self._cnonce
+ rv += b':' + self._qop
+ rv += b':' + bytes(self.hash(a2).hexdigest().lower())
+ response.update(rv)
+ return bytes(response.hexdigest().lower())
+
+ def mutual_auth(self, cmp_hash):
+ """
+ """
+ a2 = b':' + self._digest_uri
+ if self._qop != b'auth':
+ a2 += b':00000000000000000000000000000000'
+ if self.gen_hash(a2) == cmp_hash:
+ self._rspauth_okay = True
+
+ def prep(self):
+ """
+ """
+ if 'password' in self.values:
+ del self.values['password']
+ self.values['cnonce'] = self._cnonce
+
+ def process(self, challenge=None):
+ """
+ """
+ if challenge is None:
+ if self.has_values(['username', 'realm', 'nonce', 'key_hash',
+ 'nc', 'cnonce', 'qops']):
+ self._qops = self.values['qops']
+ return self.response()
+ else:
+ return None
+
+ d = parse_challenge(challenge)
+ if b'rspauth' in d:
+ self.mutual_auth(d[b'rspauth'])
+ else:
+ if b'realm' not in d:
+ d[b'realm'] = self.sasl.def_realm
+ for key in ['nonce', 'realm']:
+ if bytes(key) in d:
+ self.values[key] = d[bytes(key)]
+ self.values['nc'] = 0
+ self._qops = [b'auth']
+ if b'qop' in d:
+ self._qops = [x.strip() for x in d[b'qop'].split(b',')]
+ self.values['qops'] = self._qops
+ if b'maxbuf' in d:
+ self._max_buffer = int(d[b'maxbuf'])
+ return self.response()
+
+ def okay(self):
+ """
+ """
+ if self._rspauth_okay and self._qop == b'auth-int':
+ self._enc_key = self.hash(self._a1 + self.enc_magic).digest()
+ self._dec_key = self.hash(self._a1 + self.dec_magic).digest()
+ self.encoding = True
+ return self._rspauth_okay
+
+
+register_mechanism('DIGEST-', 30, DIGEST_MD5)
diff --git a/sleekxmpp/thirdparty/suelta/mechanisms/plain.py b/sleekxmpp/thirdparty/suelta/mechanisms/plain.py
new file mode 100644
index 00000000..ab17095e
--- /dev/null
+++ b/sleekxmpp/thirdparty/suelta/mechanisms/plain.py
@@ -0,0 +1,61 @@
+import sys
+
+from sleekxmpp.thirdparty.suelta.util import bytes
+from sleekxmpp.thirdparty.suelta.sasl import Mechanism, register_mechanism
+from sleekxmpp.thirdparty.suelta.exceptions import SASLError, SASLCancelled
+
+
+class PLAIN(Mechanism):
+
+ """
+ """
+
+ def __init__(self, sasl, name):
+ """
+ """
+ super(PLAIN, self).__init__(sasl, name)
+
+ if not self.sasl.tls_active():
+ if not self.sasl.sec_query(self, '-ENCRYPTION, PLAIN'):
+ raise SASLCancelled(self.sasl, self)
+ else:
+ if not self.sasl.sec_query(self, '+ENCRYPTION, PLAIN'):
+ raise SASLCancelled(self.sasl, self)
+
+ self.check_values(['username', 'password'])
+
+ def prep(self):
+ """
+ Prepare for processing by deleting the password if
+ the user has not approved storing it in the clear.
+ """
+ if 'savepass' not in self.values:
+ if self.sasl.sec_query(self, 'CLEAR-PASSWORD'):
+ self.values['savepass'] = True
+
+ if 'savepass' not in self.values:
+ del self.values['password']
+
+ return True
+
+ def process(self, challenge=None):
+ """
+ Process a challenge request and return the response.
+
+ :param challenge: A challenge issued by the server that
+ must be answered for authentication.
+ """
+ user = bytes(self.values['username'])
+ password = bytes(self.values['password'])
+ return b'\x00' + user + b'\x00' + password
+
+ def okay(self):
+ """
+ Mutual authentication is not supported by PLAIN.
+
+ :returns: ``True``
+ """
+ return True
+
+
+register_mechanism('PLAIN', 1, PLAIN, use_hashes=False)
diff --git a/sleekxmpp/thirdparty/suelta/mechanisms/scram_hmac.py b/sleekxmpp/thirdparty/suelta/mechanisms/scram_hmac.py
new file mode 100644
index 00000000..e0020329
--- /dev/null
+++ b/sleekxmpp/thirdparty/suelta/mechanisms/scram_hmac.py
@@ -0,0 +1,176 @@
+import sys
+import hmac
+import random
+from base64 import b64encode, b64decode
+
+from sleekxmpp.thirdparty.suelta.util import hash, bytes, num_to_bytes, bytes_to_num, XOR
+from sleekxmpp.thirdparty.suelta.sasl import Mechanism, register_mechanism
+from sleekxmpp.thirdparty.suelta.exceptions import SASLError, SASLCancelled
+
+
+def parse_challenge(challenge):
+ """
+ """
+ items = {}
+ for key, value in [item.split(b'=', 1) for item in challenge.split(b',')]:
+ items[key] = value
+ return items
+
+
+class SCRAM_HMAC(Mechanism):
+
+ """
+ """
+
+ def __init__(self, sasl, name):
+ """
+ """
+ super(SCRAM_HMAC, self).__init__(sasl, name, 0)
+
+ self._cb = False
+ if name[-5:] == '-PLUS':
+ name = name[:-5]
+ self._cb = True
+
+ self.hash = hash(self.name[6:])
+ if self.hash is None:
+ raise SASLCancelled(self.sasl, self)
+ if not self.sasl.tls_active():
+ if not self.sasl.sec_query(self, '-ENCRYPTION, SCRAM'):
+ raise SASLCancelled(self.sasl, self)
+
+ self._step = 0
+ self._rspauth = False
+
+ def HMAC(self, key, msg):
+ """
+ """
+ return hmac.HMAC(key=key, msg=msg, digestmod=self.hash).digest()
+
+ def Hi(self, text, salt, iterations):
+ """
+ """
+ text = bytes(text)
+ ui_1 = self.HMAC(text, salt + b'\0\0\0\01')
+ ui = ui_1
+ for i in range(iterations - 1):
+ ui_1 = self.HMAC(text, ui_1)
+ ui = XOR(ui, ui_1)
+ return ui
+
+ def H(self, text):
+ """
+ """
+ return self.hash(text).digest()
+
+ def prep(self):
+ if 'password' in self.values:
+ del self.values['password']
+
+ def process(self, challenge=None):
+ """
+ """
+ steps = {
+ 0: self.process_one,
+ 1: self.process_two,
+ 2: self.process_three
+ }
+ return steps[self._step](challenge)
+
+ def process_one(self, challenge):
+ """
+ """
+ vitals = ['username']
+ if 'SaltedPassword' not in self.values:
+ vitals.append('password')
+ if 'Iterations' not in self.values:
+ vitals.append('password')
+
+ self.check_values(vitals)
+
+ username = bytes(self.values['username'])
+
+ self._step = 1
+ self._cnonce = bytes(('%s' % random.random())[2:])
+ self._soup = b'n=' + username + b',r=' + self._cnonce
+ self._gs2header = b''
+
+ if not self.sasl.tls_active():
+ if self._cb:
+ self._gs2header = b'p=tls-unique,,'
+ else:
+ self._gs2header = b'y,,'
+ else:
+ self._gs2header = b'n,,'
+
+ return self._gs2header + self._soup
+
+ def process_two(self, challenge):
+ """
+ """
+ data = parse_challenge(challenge)
+
+ self._step = 2
+ self._soup += b',' + challenge + b','
+ self._nonce = data[b'r']
+ self._salt = b64decode(data[b's'])
+ self._iter = int(data[b'i'])
+
+ if self._nonce[:len(self._cnonce)] != self._cnonce:
+ raise SASLCancelled(self.sasl, self)
+
+ cbdata = self.sasl.tls_active()
+ c = self._gs2header
+ if not cbdata and self._cb:
+ c += None
+
+ r = b'c=' + b64encode(c).replace(b'\n', b'')
+ r += b',r=' + self._nonce
+ self._soup += r
+
+ if 'Iterations' in self.values:
+ if self.values['Iterations'] != self._iter:
+ if 'SaltedPassword' in self.values:
+ del self.values['SaltedPassword']
+ if 'Salt' in self.values:
+ if self.values['Salt'] != self._salt:
+ if 'SaltedPassword' in self.values:
+ del self.values['SaltedPassword']
+
+ self.values['Iterations'] = self._iter
+ self.values['Salt'] = self._salt
+
+ if 'SaltedPassword' not in self.values:
+ self.check_values(['password'])
+ password = bytes(self.values['password'])
+ salted_pass = self.Hi(password, self._salt, self._iter)
+ self.values['SaltedPassword'] = salted_pass
+
+ salted_pass = self.values['SaltedPassword']
+ client_key = self.HMAC(salted_pass, b'Client Key')
+ stored_key = self.H(client_key)
+ client_sig = self.HMAC(stored_key, self._soup)
+ client_proof = XOR(client_key, client_sig)
+ r += b',p=' + b64encode(client_proof).replace(b'\n', b'')
+ server_key = self.HMAC(self.values['SaltedPassword'], b'Server Key')
+ self.server_sig = self.HMAC(server_key, self._soup)
+ return r
+
+ def process_three(self, challenge=None):
+ """
+ """
+ data = parse_challenge(challenge)
+ if b64decode(data[b'v']) == self.server_sig:
+ self._rspauth = True
+
+ def okay(self):
+ """
+ """
+ return self._rspauth
+
+ def get_user(self):
+ return self.values['username']
+
+
+register_mechanism('SCRAM-', 60, SCRAM_HMAC)
+register_mechanism('SCRAM-', 70, SCRAM_HMAC, extra='-PLUS')
diff --git a/sleekxmpp/thirdparty/suelta/sasl.py b/sleekxmpp/thirdparty/suelta/sasl.py
new file mode 100644
index 00000000..2ae9ae61
--- /dev/null
+++ b/sleekxmpp/thirdparty/suelta/sasl.py
@@ -0,0 +1,402 @@
+from sleekxmpp.thirdparty.suelta.util import hashes
+from sleekxmpp.thirdparty.suelta.saslprep import saslprep
+
+#: Global session storage for user answers to requested mechanism values
+#: and security questions. This allows the user's preferences to be
+#: persisted across multiple SASL authentication attempts made by the
+#: same process.
+SESSION = {'answers': {},
+ 'passwords': {},
+ 'sec_queries': {},
+ 'stash': {},
+ 'stash_file': ''}
+
+#: Global registry mapping mechanism names to implementation classes.
+MECHANISMS = {}
+
+#: Global registry mapping mechanism names to security scores.
+MECH_SEC_SCORES = {}
+
+
+def register_mechanism(basename, basescore, impl, extra=None, use_hashes=True):
+ """
+ Add a SASL mechanism to the registry of available mechanisms.
+
+ :param basename: The base name of the mechanism type, such as ``CRAM-``.
+ :param basescore: The base security score for this type of mechanism.
+ :param impl: The class implementing the mechanism.
+ :param extra: Any additional qualifiers to the mechanism name,
+ such as ``-PLUS``.
+ :param use_hashes: If ``True``, then register the mechanism for use with
+ all available hashes.
+ """
+ n = 0
+ if use_hashes:
+ for hashing_alg in hashes():
+ n += 1
+ name = basename + hashing_alg
+ if extra is not None:
+ name += extra
+ MECHANISMS[name] = impl
+ MECH_SEC_SCORES[name] = basescore + n
+ else:
+ MECHANISMS[basename] = impl
+ MECH_SEC_SCORES[basename] = basescore
+
+
+def set_stash_file(filename):
+ """
+ Enable or disable storing the stash to disk.
+
+ If the filename is ``None``, then disable using a stash file.
+
+ :param filename: The path to the file to store the stash data.
+ """
+ SESSION['stash_file'] = filename
+ try:
+ import marshal
+ stash_file = file(filename)
+ SESSION['stash'] = marshal.load(stash_file)
+ except:
+ SESSION['stash'] = {}
+
+
+def sec_query_allow(mech, query):
+ """
+ Quick default to allow all feature combinations which could
+ negatively affect security.
+
+ :param mech: The chosen SASL mechanism
+ :param query: An encoding of the combination of enabled and
+ disabled features which may affect security.
+
+ :returns: ``True``
+ """
+ return True
+
+
+class SASL(object):
+
+ """
+ """
+
+ def __init__(self, host, service, mech=None, username=None,
+ min_sec=0, request_values=None, sec_query=None,
+ tls_active=None, def_realm=None):
+ """
+ :param string host: The host of the service requiring authentication.
+ :param string service: The name of the underlying protocol in use.
+ :param string mech: Optional name of the SASL mechanism to use.
+ If given, only this mechanism may be used for
+ authentication.
+ :param string username: The username to use when authenticating.
+ :param request_values: Reference to a function for supplying
+ values requested by mechanisms, such
+ as passwords. (See above)
+ :param sec_query: Reference to a function for approving or
+ denying feature combinations which could
+ negatively impact security. (See above)
+ :param tls_active: Function for indicating if TLS has been
+ negotiated. (See above)
+ :param integer min_sec: The minimum security level accepted. This
+ only allows for SASL mechanisms whose
+ security rating is greater than `min_sec`.
+ :param string def_realm: The default realm, if different than `host`.
+
+ :type request_values: :func:`request_values`
+ :type sec_query: :func:`sec_query`
+ :type tls_active: :func:`tls_active`
+ """
+ self.host = host
+ self.def_realm = def_realm or host
+ self.service = service
+ self.user = username
+ self.mech = mech
+ self.min_sec = min_sec - 1
+
+ self.request_values = request_values
+ self._sec_query = sec_query
+ if tls_active is not None:
+ self.tls_active = tls_active
+ else:
+ self.tls_active = lambda: False
+
+ self.try_username = self.user
+ self.try_password = None
+
+ self.stash_id = None
+ self.testkey = None
+
+ def reset_stash_id(self, username):
+ """
+ Reset the ID for the stash for persisting user data.
+
+ :param username: The username to base the new ID on.
+ """
+ username = saslprep(username)
+ self.user = username
+ self.try_username = self.user
+ self.testkey = [self.user, self.host, self.service]
+ self.stash_id = '\0'.join(self.testkey)
+
+ def sec_query(self, mech, query):
+ """
+ Request authorization from the user to use a combination
+ of features which could negatively affect security.
+
+ The ``sec_query`` callback when creating the SASL object will
+ be called if the query has not been answered before. Otherwise,
+ the query response will be pulled from ``SESSION['sec_queries']``.
+
+ If no ``sec_query`` callback was provided, then all queries
+ will be denied.
+
+ :param mech: The chosen SASL mechanism
+ :param query: An encoding of the combination of enabled and
+ disabled features which may affect security.
+ :rtype: bool
+ """
+ if self._sec_query is None:
+ return False
+ if query in SESSION['sec_queries']:
+ return SESSION['sec_queries'][query]
+ resp = self._sec_query(mech, query)
+ if resp:
+ SESSION['sec_queries'][query] = resp
+
+ return resp
+
+ def find_password(self, mech):
+ """
+ Find and return the user's password, if it has been entered before
+ during this session.
+
+ :param mech: The chosen SASL mechanism.
+ """
+ if self.try_password is not None:
+ return self.try_password
+ if self.testkey is None:
+ return
+
+ testkey = self.testkey[:]
+ lockout = 1
+
+ def find_username(self):
+ """Find and return user's username if known."""
+ return self.try_username
+
+ def success(self, mech):
+ mech.preprep()
+ if 'password' in mech.values:
+ testkey = self.testkey[:]
+ while len(testkey):
+ tk = '\0'.join(testkey)
+ if tk in SESSION['passwords']:
+ break
+ SESSION['passwords'][tk] = mech.values['password']
+ testkey = testkey[:-1]
+ mech.prep()
+ mech.save_values()
+
+ def failure(self, mech):
+ mech.clear()
+ self.testkey = self.testkey[:-1]
+
+ def choose_mechanism(self, mechs, force_plain=False):
+ """
+ Choose the most secure mechanism from a list of mechanisms.
+
+ If ``force_plain`` is given, return the ``PLAIN`` mechanism.
+
+ :param mechs: A list of mechanism names.
+ :param force_plain: If ``True``, force the selection of the
+ ``PLAIN`` mechanism.
+ :returns: A SASL mechanism object, or ``None`` if no mechanism
+ could be selected.
+ """
+ # Handle selection of PLAIN and ANONYMOUS
+ if force_plain:
+ return MECHANISMS['PLAIN'](self, 'PLAIN')
+
+ if self.user is not None:
+ requested_mech = '*' if self.mech is None else self.mech
+ else:
+ if self.mech is None:
+ requested_mech = 'ANONYMOUS'
+ else:
+ requested_mech = self.mech
+ if requested_mech == '*' and self.user in ['', 'anonymous', None]:
+ requested_mech = 'ANONYMOUS'
+
+ # If a specific mechanism was requested, try it
+ if requested_mech != '*':
+ if requested_mech in MECHANISMS and \
+ requested_mech in MECH_SEC_SCORES:
+ return MECHANISMS[requested_mech](self, requested_mech)
+ return None
+
+ # Pick the best mechanism based on its security score
+ best_score = self.min_sec
+ best_mech = None
+ for name in mechs:
+ if name in MECH_SEC_SCORES:
+ if MECH_SEC_SCORES[name] > best_score:
+ best_score = MECH_SEC_SCORES[name]
+ best_mech = name
+ if best_mech is not None:
+ best_mech = MECHANISMS[best_mech](self, best_mech)
+
+ return best_mech
+
+
+class Mechanism(object):
+
+ """
+ """
+
+ def __init__(self, sasl, name, version=0, use_stash=True):
+ self.name = name
+ self.sasl = sasl
+ self.use_stash = use_stash
+
+ self.encoding = False
+ self.values = {}
+
+ if use_stash:
+ self.load_values()
+
+ def load_values(self):
+ """Retrieve user data from the stash."""
+ self.values = {}
+ if not self.use_stash:
+ return False
+ if self.sasl.stash_id is not None:
+ if self.sasl.stash_id in SESSION['stash']:
+ if SESSION['stash'][self.sasl.stash_id]['mech'] == self.name:
+ values = SESSION['stash'][self.sasl.stash_id]['values']
+ self.values.update(values)
+ if self.sasl.user is not None:
+ if not self.has_values(['username']):
+ self.values['username'] = self.sasl.user
+ return None
+
+ def save_values(self):
+ """
+ Save user data to the session stash.
+
+ If a stash file name has been set using ``SESSION['stash_file']``,
+ the saved values will be persisted to disk.
+ """
+ if not self.use_stash:
+ return False
+ if self.sasl.stash_id is not None:
+ if self.sasl.stash_id not in SESSION['stash']:
+ SESSION['stash'][self.sasl.stash_id] = {}
+ SESSION['stash'][self.sasl.stash_id]['values'] = self.values
+ SESSION['stash'][self.sasl.stash_id]['mech'] = self.name
+ if SESSION['stash_file'] not in ['', None]:
+ import marshal
+ stash_file = file(SESSION['stash_file'], 'wb')
+ marshal.dump(SESSION['stash'], stash_file)
+
+ def clear(self):
+ """Reset all user data, except the username."""
+ username = None
+ if 'username' in self.values:
+ username = self.values['username']
+ self.values = {}
+ if username is not None:
+ self.values['username'] = username
+ self.save_values()
+ self.values = {}
+ self.load_values()
+
+ def okay(self):
+ """
+ Indicate if mutual authentication has completed successfully.
+
+ :rtype: bool
+ """
+ return False
+
+ def preprep(self):
+ """Ensure that the stash ID has been set before processing."""
+ if self.sasl.stash_id is None:
+ if 'username' in self.values:
+ self.sasl.reset_stash_id(self.values['username'])
+
+ def prep(self):
+ """
+ Prepare stored values for processing.
+
+ For example, by removing extra copies of passwords from memory.
+ """
+ pass
+
+ def process(self, challenge=None):
+ """
+ Process a challenge request and return the response.
+
+ :param challenge: A challenge issued by the server that
+ must be answered for authentication.
+ """
+ raise NotImplemented
+
+ def fulfill(self, values):
+ """
+ Provide requested values to the mechanism.
+
+ :param values: A dictionary of requested values.
+ """
+ if 'password' in values:
+ values['password'] = saslprep(values['password'])
+ self.values.update(values)
+
+ def missing_values(self, keys):
+ """
+ Return a dictionary of value names that have not been given values
+ by the user, or retrieved from the stash.
+
+ :param keys: A list of value names to check.
+ :rtype: dict
+ """
+ vals = {}
+ for name in keys:
+ if name not in self.values or self.values[name] is None:
+ if self.use_stash:
+ if name == 'username':
+ value = self.sasl.find_username()
+ if value is not None:
+ self.sasl.reset_stash_id(value)
+ self.values[name] = value
+ break
+ if name == 'password':
+ value = self.sasl.find_password(self)
+ if value is not None:
+ self.values[name] = value
+ break
+ vals[name] = None
+ return vals
+
+ def has_values(self, keys):
+ """
+ Check that the given values have been retrieved from the user,
+ or from the stash.
+
+ :param keys: A list of value names to check.
+ """
+ return len(self.missing_values(keys)) == 0
+
+ def check_values(self, keys):
+ """
+ Request missing values from the user.
+
+ :param keys: A list of value names to request, if missing.
+ """
+ vals = self.missing_values(keys)
+ if vals:
+ self.sasl.request_values(self, vals)
+
+ def get_user(self):
+ """Return the username usd for this mechanism."""
+ return self.values['username']
diff --git a/sleekxmpp/thirdparty/suelta/saslprep.py b/sleekxmpp/thirdparty/suelta/saslprep.py
new file mode 100644
index 00000000..fe58d58b
--- /dev/null
+++ b/sleekxmpp/thirdparty/suelta/saslprep.py
@@ -0,0 +1,78 @@
+from __future__ import unicode_literals
+
+import sys
+import stringprep
+import unicodedata
+
+
+def saslprep(text, strict=True):
+ """
+ Return a processed version of the given string, using the SASLPrep
+ profile of stringprep.
+
+ :param text: The string to process, in UTF-8.
+ :param strict: If ``True``, prevent the use of unassigned code points.
+ """
+
+ if sys.version_info < (3, 0):
+ if type(text) == str:
+ text = text.decode('us-ascii')
+
+ # Mapping:
+ #
+ # - non-ASCII space characters [StringPrep, C.1.2] that can be
+ # mapped to SPACE (U+0020), and
+ #
+ # - the 'commonly mapped to nothing' characters [StringPrep, B.1]
+ # that can be mapped to nothing.
+ buffer = ''
+ for char in text:
+ if stringprep.in_table_c12(char):
+ buffer += ' '
+ elif not stringprep.in_table_b1(char):
+ buffer += char
+
+ # Normalization using form KC
+ text = unicodedata.normalize('NFKC', buffer)
+
+ # Check for bidirectional string
+ buffer = ''
+ first_is_randal = False
+ if text:
+ first_is_randal = stringprep.in_table_d1(text[0])
+ if first_is_randal and not stringprep.in_table_d1(text[-1]):
+ raise UnicodeError('Section 6.3 [end]')
+
+ # Check for prohibited characters
+ for x in range(len(text)):
+ if strict and stringprep.in_table_a1(text[x]):
+ raise UnicodeError('Unassigned Codepoint')
+ if stringprep.in_table_c12(text[x]):
+ raise UnicodeError('In table C.1.2')
+ if stringprep.in_table_c21(text[x]):
+ raise UnicodeError('In table C.2.1')
+ if stringprep.in_table_c22(text[x]):
+ raise UnicodeError('In table C.2.2')
+ if stringprep.in_table_c3(text[x]):
+ raise UnicodeError('In table C.3')
+ if stringprep.in_table_c4(text[x]):
+ raise UnicodeError('In table C.4')
+ if stringprep.in_table_c5(text[x]):
+ raise UnicodeError('In table C.5')
+ if stringprep.in_table_c6(text[x]):
+ raise UnicodeError('In table C.6')
+ if stringprep.in_table_c7(text[x]):
+ raise UnicodeError('In table C.7')
+ if stringprep.in_table_c8(text[x]):
+ raise UnicodeError('In table C.8')
+ if stringprep.in_table_c9(text[x]):
+ raise UnicodeError('In table C.9')
+ if x:
+ if first_is_randal and stringprep.in_table_d2(text[x]):
+ raise UnicodeError('Section 6.2')
+ if not first_is_randal and \
+ x != len(text) - 1 and \
+ stringprep.in_table_d1(text[x]):
+ raise UnicodeError('Section 6.3')
+
+ return text
diff --git a/sleekxmpp/thirdparty/suelta/util.py b/sleekxmpp/thirdparty/suelta/util.py
new file mode 100644
index 00000000..7d822a81
--- /dev/null
+++ b/sleekxmpp/thirdparty/suelta/util.py
@@ -0,0 +1,118 @@
+"""
+"""
+
+import sys
+import hashlib
+
+
+def bytes(text):
+ """
+ Convert Unicode text to UTF-8 encoded bytes.
+
+ Since Python 2.6+ and Python 3+ have similar but incompatible
+ signatures, this function unifies the two to keep code sane.
+
+ :param text: Unicode text to convert to bytes
+ :rtype: bytes (Python3), str (Python2.6+)
+ """
+ if sys.version_info < (3, 0):
+ import __builtin__
+ return __builtin__.bytes(text)
+ else:
+ import builtins
+ if isinstance(text, builtins.bytes):
+ # We already have bytes, so do nothing
+ return text
+ if isinstance(text, list):
+ # Convert a list of integers to bytes
+ return builtins.bytes(text)
+ else:
+ # Convert UTF-8 text to bytes
+ return builtins.bytes(text, encoding='utf-8')
+
+
+def quote(text):
+ """
+ Enclose in quotes and escape internal slashes and double quotes.
+
+ :param text: A Unicode or byte string.
+ """
+ text = bytes(text)
+ return b'"' + text.replace(b'\\', b'\\\\').replace(b'"', b'\\"') + b'"'
+
+
+def num_to_bytes(num):
+ """
+ Convert an integer into a four byte sequence.
+
+ :param integer num: An integer to convert to its byte representation.
+ """
+ bval = b''
+ bval += bytes(chr(0xFF & (num >> 24)))
+ bval += bytes(chr(0xFF & (num >> 16)))
+ bval += bytes(chr(0xFF & (num >> 8)))
+ bval += bytes(chr(0xFF & (num >> 0)))
+ return bval
+
+
+def bytes_to_num(bval):
+ """
+ Convert a four byte sequence to an integer.
+
+ :param bytes bval: A four byte sequence to turn into an integer.
+ """
+ num = 0
+ num += ord(bval[0] << 24)
+ num += ord(bval[1] << 16)
+ num += ord(bval[2] << 8)
+ num += ord(bval[3])
+ return num
+
+
+def XOR(x, y):
+ """
+ Return the results of an XOR operation on two equal length byte strings.
+
+ :param bytes x: A byte string
+ :param bytes y: A byte string
+ :rtype: bytes
+ """
+ result = b''
+ for a, b in zip(x, y):
+ if sys.version_info < (3, 0):
+ result += chr((ord(a) ^ ord(b)))
+ else:
+ result += bytes([a ^ b])
+ return result
+
+
+def hash(name):
+ """
+ Return a hash function implementing the given algorithm.
+
+ :param name: The name of the hashing algorithm to use.
+ :type name: string
+
+ :rtype: function
+ """
+ name = name.lower()
+ if name.startswith('sha-'):
+ name = 'sha' + name[4:]
+ if name in dir(hashlib):
+ return getattr(hashlib, name)
+ return None
+
+
+def hashes():
+ """
+ Return a list of available hashing algorithms.
+
+ :rtype: list of strings
+ """
+ t = []
+ if 'md5' in dir(hashlib):
+ t = ['MD5']
+ if 'md2' in dir(hashlib):
+ t += ['MD2']
+ hashes = ['SHA-' + h[3:] for h in dir(hashlib) if h.startswith('sha')]
+ return t + hashes
diff --git a/sleekxmpp/xmlstream/stanzabase.py b/sleekxmpp/xmlstream/stanzabase.py
index d9a4636a..a2826ead 100644
--- a/sleekxmpp/xmlstream/stanzabase.py
+++ b/sleekxmpp/xmlstream/stanzabase.py
@@ -482,7 +482,8 @@ class ElementBase(object):
if plugin:
if plugin not in self.plugins:
self.init_plugin(plugin)
- handler = getattr(self.plugins[plugin], set_method, None)
+ handler = getattr(self.plugins[plugin],
+ set_method, None)
if handler:
return handler(value)
@@ -1064,7 +1065,9 @@ class ElementBase(object):
Defaults to True.
"""
stanza_ns = '' if top_level_ns else self.namespace
- return tostring(self.xml, xmlns='', stanza_ns=stanza_ns)
+ return tostring(self.xml, xmlns='',
+ stanza_ns=stanza_ns,
+ top_level=not top_level_ns)
def __repr__(self):
"""
@@ -1282,7 +1285,8 @@ class StanzaBase(ElementBase):
stanza_ns = '' if top_level_ns else self.namespace
return tostring(self.xml, xmlns='',
stanza_ns=stanza_ns,
- stream=self.stream)
+ stream=self.stream,
+ top_level=not top_level_ns)
# To comply with PEP8, method names now use underscores.
diff --git a/sleekxmpp/xmlstream/tostring/tostring.py b/sleekxmpp/xmlstream/tostring.py
index 38b08d82..f9674b15 100644
--- a/sleekxmpp/xmlstream/tostring/tostring.py
+++ b/sleekxmpp/xmlstream/tostring.py
@@ -6,8 +6,14 @@
See the file LICENSE for copying permission.
"""
+import sys
-def tostring(xml=None, xmlns='', stanza_ns='', stream=None, outbuffer=''):
+if sys.version_info < (3, 0):
+ import types
+
+
+def tostring(xml=None, xmlns='', stanza_ns='', stream=None,
+ outbuffer='', top_level=False):
"""
Serialize an XML object to a Unicode string.
@@ -26,6 +32,8 @@ def tostring(xml=None, xmlns='', stanza_ns='', stream=None, outbuffer=''):
stream -- The XML stream that generated the XML object.
outbuffer -- Optional buffer for storing serializations during
recursive calls.
+ top_level -- Indicates that the element is the outermost
+ element.
"""
# Add previous results to the start of the output.
output = [outbuffer]
@@ -39,14 +47,21 @@ def tostring(xml=None, xmlns='', stanza_ns='', stream=None, outbuffer=''):
else:
tag_xmlns = ''
+ default_ns = ''
+ stream_ns = ''
+ if stream:
+ default_ns = stream.default_ns
+ stream_ns = stream.stream_ns
+
# Output the tag name and derived namespace of the element.
namespace = ''
- if tag_xmlns not in ['', xmlns, stanza_ns]:
+ if top_level and tag_xmlns not in ['', default_ns, stream_ns] or \
+ tag_xmlns not in ['', xmlns, stanza_ns, stream_ns]:
namespace = ' xmlns="%s"' % tag_xmlns
- if stream and tag_xmlns in stream.namespace_map:
- mapped_namespace = stream.namespace_map[tag_xmlns]
- if mapped_namespace:
- tag_name = "%s:%s" % (mapped_namespace, tag_name)
+ if stream and tag_xmlns in stream.namespace_map:
+ mapped_namespace = stream.namespace_map[tag_xmlns]
+ if mapped_namespace:
+ tag_name = "%s:%s" % (mapped_namespace, tag_name)
output.append("<%s" % tag_name)
output.append(namespace)
@@ -93,6 +108,10 @@ def xml_escape(text):
Arguments:
text -- The XML text to convert.
"""
+ if sys.version_info < (3, 0):
+ if type(text) != types.UnicodeType:
+ text = unicode(text, 'utf-8', 'ignore')
+
text = list(text)
escapes = {'&': '&amp;',
'<': '&lt;',
diff --git a/sleekxmpp/xmlstream/tostring/__init__.py b/sleekxmpp/xmlstream/tostring/__init__.py
deleted file mode 100644
index 5852cba2..00000000
--- a/sleekxmpp/xmlstream/tostring/__init__.py
+++ /dev/null
@@ -1,19 +0,0 @@
-"""
- 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 the correct tostring and xml_escape functions based on the Python
-# version in order to properly handle Unicode.
-
-if sys.version_info < (3, 0):
- from sleekxmpp.xmlstream.tostring.tostring26 import tostring, xml_escape
-else:
- from sleekxmpp.xmlstream.tostring.tostring import tostring, xml_escape
-
-__all__ = ['tostring', 'xml_escape']
diff --git a/sleekxmpp/xmlstream/tostring/tostring26.py b/sleekxmpp/xmlstream/tostring/tostring26.py
deleted file mode 100644
index 11501780..00000000
--- a/sleekxmpp/xmlstream/tostring/tostring26.py
+++ /dev/null
@@ -1,110 +0,0 @@
-"""
- SleekXMPP: The Sleek XMPP Library
- Copyright (C) 2010 Nathanael C. Fritz
- This file is part of SleekXMPP.
-
- See the file LICENSE for copying permission.
-"""
-
-from __future__ import unicode_literals
-import types
-
-
-def tostring(xml=None, xmlns='', stanza_ns='', stream=None, outbuffer=''):
- """
- Serialize an XML object to a Unicode string.
-
- If namespaces are provided using xmlns or stanza_ns, then elements
- that use those namespaces will not include the xmlns attribute in
- the output.
-
- Arguments:
- xml -- The XML object to serialize. If the value is None,
- then the XML object contained in this stanza
- object will be used.
- xmlns -- Optional namespace of an element wrapping the XML
- object.
- stanza_ns -- The namespace of the stanza object that contains
- the XML object.
- stream -- The XML stream that generated the XML object.
- outbuffer -- Optional buffer for storing serializations during
- recursive calls.
- """
- # Add previous results to the start of the output.
- output = [outbuffer]
-
- # Extract the element's tag name.
- tag_name = xml.tag.split('}', 1)[-1]
-
- # Extract the element's namespace if it is defined.
- if '}' in xml.tag:
- tag_xmlns = xml.tag.split('}', 1)[0][1:]
- else:
- tag_xmlns = u''
-
- # Output the tag name and derived namespace of the element.
- namespace = u''
- if tag_xmlns not in ['', xmlns, stanza_ns]:
- namespace = u' xmlns="%s"' % tag_xmlns
- if stream and tag_xmlns in stream.namespace_map:
- mapped_namespace = stream.namespace_map[tag_xmlns]
- if mapped_namespace:
- tag_name = u"%s:%s" % (mapped_namespace, tag_name)
- output.append(u"<%s" % tag_name)
- output.append(namespace)
-
- # Output escaped attribute values.
- for attrib, value in xml.attrib.items():
- value = xml_escape(value)
- if '}' not in attrib:
- output.append(' %s="%s"' % (attrib, value))
- else:
- attrib_ns = attrib.split('}')[0][1:]
- attrib = attrib.split('}')[1]
- if stream and attrib_ns in stream.namespace_map:
- mapped_ns = stream.namespace_map[attrib_ns]
- if mapped_ns:
- output.append(' %s:%s="%s"' % (mapped_ns,
- attrib,
- value))
-
- if len(xml) or xml.text:
- # If there are additional child elements to serialize.
- output.append(u">")
- if xml.text:
- output.append(xml_escape(xml.text))
- if len(xml):
- for child in xml.getchildren():
- output.append(tostring(child, tag_xmlns, stanza_ns, stream))
- output.append(u"</%s>" % tag_name)
- elif xml.text:
- # If we only have text content.
- output.append(u">%s</%s>" % (xml_escape(xml.text), tag_name))
- else:
- # Empty element.
- output.append(u" />")
- if xml.tail:
- # If there is additional text after the element.
- output.append(xml_escape(xml.tail))
- return u''.join(output)
-
-
-def xml_escape(text):
- """
- Convert special characters in XML to escape sequences.
-
- Arguments:
- text -- The XML text to convert.
- """
- if type(text) != types.UnicodeType:
- text = list(unicode(text, 'utf-8', 'ignore'))
- else:
- text = list(text)
- escapes = {u'&': u'&amp;',
- u'<': u'&lt;',
- u'>': u'&gt;',
- u"'": u'&apos;',
- u'"': u'&quot;'}
- for i, c in enumerate(text):
- text[i] = escapes.get(c, c)
- return u''.join(text)
diff --git a/sleekxmpp/xmlstream/xmlstream.py b/sleekxmpp/xmlstream/xmlstream.py
index 5bc71f04..5ba4269f 100644
--- a/sleekxmpp/xmlstream/xmlstream.py
+++ b/sleekxmpp/xmlstream/xmlstream.py
@@ -8,6 +8,7 @@
from __future__ import with_statement, unicode_literals
+import base64
import copy
import logging
import signal
@@ -23,6 +24,7 @@ try:
except ImportError:
import Queue as queue
+import sleekxmpp
from sleekxmpp.thirdparty.statemachine import StateMachine
from sleekxmpp.xmlstream import Scheduler, tostring
from sleekxmpp.xmlstream.stanzabase import StanzaBase, ET
@@ -107,7 +109,13 @@ class XMLStream(object):
stream_header -- The closing tag of the stream's root element.
use_ssl -- Flag indicating if SSL should be used.
use_tls -- Flag indicating if TLS should be used.
+ use_proxy -- Flag indicating that an HTTP Proxy should be used.
stop -- threading Event used to stop all threads.
+ proxy_config -- An optional dictionary with the following entries:
+ host -- The host offering proxy services.
+ port -- The port for the proxy service.
+ username -- Optional username for the proxy.
+ password -- Optional password for the proxy.
auto_reconnect -- Flag to determine whether we auto reconnect.
reconnect_max_delay -- Maximum time to delay between connection
@@ -180,6 +188,9 @@ class XMLStream(object):
self.use_ssl = False
self.use_tls = False
+ self.use_proxy = False
+
+ self.proxy_config = {}
self.default_ns = ''
self.stream_header = "<stream>"
@@ -322,6 +333,12 @@ class XMLStream(object):
log.debug('Waiting %s seconds before connecting.' % delay)
time.sleep(delay)
+ if self.use_proxy:
+ connected = self._connect_proxy()
+ if not connected:
+ self.reconnect_delay = delay
+ return False
+
if self.use_ssl and self.ssl_support:
log.debug("Socket Wrapped for SSL")
if self.ca_certs is None:
@@ -341,8 +358,10 @@ class XMLStream(object):
self.socket = ssl_socket
try:
- log.debug("Connecting to %s:%s" % self.address)
- self.socket.connect(self.address)
+ if not self.use_proxy:
+ log.debug("Connecting to %s:%s" % self.address)
+ self.socket.connect(self.address)
+
self.set_socket(self.socket, ignore=True)
#this event is where you should set your application state
self.event("connected", direct=True)
@@ -356,22 +375,86 @@ class XMLStream(object):
self.reconnect_delay = delay
return False
- def disconnect(self, reconnect=False):
+ def _connect_proxy(self):
+ """Attempt to connect using an HTTP Proxy."""
+
+ # Extract the proxy address, and optional credentials
+ address = (self.proxy_config['host'], int(self.proxy_config['port']))
+ cred = None
+ if self.proxy_config['username']:
+ username = self.proxy_config['username']
+ password = self.proxy_config['password']
+
+ cred = '%s:%s' % (username, password)
+ if sys.version_info < (3, 0):
+ cred = bytes(cred)
+ else:
+ cred = bytes(cred, 'utf-8')
+ cred = base64.b64encode(cred).decode('utf-8')
+
+ # Build the HTTP headers for connecting to the XMPP server
+ headers = ['CONNECT %s:%s HTTP/1.0' % self.address,
+ 'Host: %s:%s' % self.address,
+ 'Proxy-Connection: Keep-Alive',
+ 'Pragma: no-cache',
+ 'User-Agent: SleekXMPP/%s' % sleekxmpp.__version__]
+ if cred:
+ headers.append('Proxy-Authorization: Basic %s' % cred)
+ headers = '\r\n'.join(headers) + '\r\n\r\n'
+
+ try:
+ log.debug("Connecting to proxy: %s:%s" % address)
+ self.socket.connect(address)
+ self.send_raw(headers, now=True)
+ resp = ''
+ while '\r\n\r\n' not in resp:
+ resp += self.socket.recv(1024).decode('utf-8')
+ log.debug('RECV: %s' % resp)
+
+ lines = resp.split('\r\n')
+ if '200' not in lines[0]:
+ self.event('proxy_error', resp)
+ log.error('Proxy Error: %s' % lines[0])
+ return False
+
+ # Proxy connection established, continue connecting
+ # with the XMPP server.
+ return True
+ except Socket.error as serr:
+ error_msg = "Could not connect to %s:%s. Socket Error #%s: %s"
+ self.event('socket_error', serr)
+ log.error(error_msg % (self.address[0], self.address[1],
+ serr.errno, serr.strerror))
+ return False
+
+ def disconnect(self, reconnect=False, wait=False):
"""
Terminate processing and close the XML streams.
Optionally, the connection may be reconnected and
resume processing afterwards.
+ If the disconnect should take place after all items
+ in the send queue have been sent, use wait=True. However,
+ take note: If you are constantly adding items to the queue
+ such that it is never empty, then the disconnect will
+ not occur and the call will continue to block.
+
Arguments:
reconnect -- Flag indicating if the connection
and processing should be restarted.
Defaults to False.
+ wait -- Flag indicating if the send queue should
+ be emptied before disconnecting.
"""
self.state.transition('connected', 'disconnected', wait=0.0,
- func=self._disconnect, args=(reconnect,))
+ func=self._disconnect, args=(reconnect, wait))
+
+ def _disconnect(self, reconnect=False, wait=False):
+ # Wait for the send queue to empty.
+ if wait:
+ self.send_queue.join()
- def _disconnect(self, reconnect=False):
# Send the end of stream marker.
self.send_raw(self.stream_footer, now=True)
self.session_started_event.clear()
@@ -748,7 +831,7 @@ class XMLStream(object):
self.send_queue.put(data)
return True
- def process(self, threaded=True):
+ def process(self, **kwargs):
"""
Initialize the XML streams and begin processing events.
@@ -756,15 +839,29 @@ class XMLStream(object):
by HANDLER_THREADS.
Arguments:
+ block -- If block=False then event dispatcher will run
+ in a separate thread, allowing for the stream to be
+ used in the background for another application.
+ Otherwise, process(block=True) blocks the current thread.
+ Defaults to False.
+
+ **threaded is deprecated and included for API compatibility**
threaded -- If threaded=True then event dispatcher will run
in a separate thread, allowing for the stream to be
used in the background for another application.
Defaults to True.
- Event handlers and the send queue will be threaded
- regardless of this parameter's value.
+ Event handlers and the send queue will be threaded
+ regardless of these parameters.
"""
- self._thread_excepthook()
+ if 'threaded' in kwargs and 'block' in kwargs:
+ raise ValueError("process() called with both " + \
+ "block and threaded arguments")
+ elif 'block' in kwargs:
+ threaded = not(kwargs.get('block', False))
+ else:
+ threaded = kwargs.get('threaded', True)
+
self.scheduler.process(threaded=True)
def start_thread(name, target):
@@ -944,13 +1041,14 @@ class XMLStream(object):
func -- The event handler to execute.
args -- Arguments to the event handler.
"""
+ orig = copy.copy(args[0])
try:
func(*args)
except Exception as e:
error_msg = 'Error processing event handler: %s'
log.exception(error_msg % str(func))
- if hasattr(args[0], 'exception'):
- args[0].exception(e)
+ if hasattr(orig, 'exception'):
+ orig.exception(e)
def _event_runner(self):
"""
@@ -973,6 +1071,7 @@ class XMLStream(object):
etype, handler = event[0:2]
args = event[2:]
+ orig = copy.copy(args[0])
if etype == 'stanza':
try:
@@ -980,7 +1079,7 @@ class XMLStream(object):
except Exception as e:
error_msg = 'Error processing stream handler: %s'
log.exception(error_msg % handler.name)
- args[0].exception(e)
+ orig.exception(e)
elif etype == 'schedule':
try:
log.debug('Scheduled event: %s' % args)
@@ -989,6 +1088,7 @@ class XMLStream(object):
log.exception('Error processing scheduled task')
elif etype == 'event':
func, threaded, disposable = handler
+ orig = copy.copy(args[0])
try:
if threaded:
x = threading.Thread(
@@ -1001,8 +1101,8 @@ class XMLStream(object):
except Exception as e:
error_msg = 'Error processing event handler: %s'
log.exception(error_msg % str(func))
- if hasattr(args[0], 'exception'):
- args[0].exception(e)
+ if hasattr(orig, 'exception'):
+ orig.exception(e)
elif etype == 'quit':
log.debug("Quitting event runner thread")
return False
@@ -1034,6 +1134,7 @@ class XMLStream(object):
log.debug("SEND: %s" % data)
try:
self.socket.send(data.encode('utf-8'))
+ self.send_queue.task_done()
except Socket.error as serr:
self.event('socket_error', serr)
log.warning("Failed to send %s" % data)
@@ -1049,30 +1150,16 @@ class XMLStream(object):
self.event_queue.put(('quit', None, None))
return
- def _thread_excepthook(self):
+ def exception(self, exception):
"""
- If a threaded event handler raises an exception, there is no way to
- catch it except with an excepthook. Currently, each thread has its own
- excepthook, but ideally we could use the main sys.excepthook.
+ Process an unknown exception.
- Modifies threading.Thread to use sys.excepthook when an exception
- is not caught.
- """
- init_old = threading.Thread.__init__
-
- def init(self, *args, **kwargs):
- init_old(self, *args, **kwargs)
- run_old = self.run
+ Meant to be overridden.
- def run_with_except_hook(*args, **kw):
- try:
- run_old(*args, **kw)
- except (KeyboardInterrupt, SystemExit):
- raise
- except:
- sys.excepthook(*sys.exc_info())
- self.run = run_with_except_hook
- threading.Thread.__init__ = init
+ Arguments:
+ exception -- An unhandled exception object.
+ """
+ pass
# To comply with PEP8, method names now use underscores.
diff --git a/tests/test_stanza_xep_0060.py b/tests/test_stanza_xep_0060.py
index 8e6e820d..d42c11bd 100644
--- a/tests/test_stanza_xep_0060.py
+++ b/tests/test_stanza_xep_0060.py
@@ -1,6 +1,6 @@
from sleekxmpp.test import *
import sleekxmpp.plugins.xep_0004 as xep_0004
-import sleekxmpp.plugins.stanza_pubsub as pubsub
+import sleekxmpp.plugins.xep_0060.stanza as pubsub
class TestPubsubStanzas(SleekTest):
diff --git a/tests/test_stream_exceptions.py b/tests/test_stream_exceptions.py
index bc01c2a7..c41edbb2 100644
--- a/tests/test_stream_exceptions.py
+++ b/tests/test_stream_exceptions.py
@@ -12,9 +12,78 @@ class TestStreamExceptions(SleekTest):
"""
def tearDown(self):
- sys.excepthook = sys.__excepthook__
self.stream_close()
+ def testExceptionReply(self):
+ """Test that raising an exception replies with the original stanza."""
+
+ def message(msg):
+ msg.reply()
+ msg['body'] = 'Body changed'
+ raise XMPPError(clear=False)
+
+ self.stream_start()
+ self.xmpp.add_event_handler('message', message)
+
+ self.recv("""
+ <message>
+ <body>This is going to cause an error.</body>
+ </message>
+ """)
+
+ self.send("""
+ <message type="error">
+ <body>This is going to cause an error.</body>
+ <error type="cancel" code="500">
+ <undefined-condition
+ xmlns="urn:ietf:params:xml:ns:xmpp-stanzas" />
+ </error>
+ </message>
+ """)
+
+ def testExceptionContinueWorking(self):
+ """Test that Sleek continues to respond after an XMPPError is raised."""
+
+ def message(msg):
+ msg.reply()
+ msg['body'] = 'Body changed'
+ raise XMPPError(clear=False)
+
+ self.stream_start()
+ self.xmpp.add_event_handler('message', message)
+
+ self.recv("""
+ <message>
+ <body>This is going to cause an error.</body>
+ </message>
+ """)
+
+ self.send("""
+ <message type="error">
+ <body>This is going to cause an error.</body>
+ <error type="cancel" code="500">
+ <undefined-condition
+ xmlns="urn:ietf:params:xml:ns:xmpp-stanzas" />
+ </error>
+ </message>
+ """)
+
+ self.recv("""
+ <message>
+ <body>This is going to cause an error.</body>
+ </message>
+ """)
+
+ self.send("""
+ <message type="error">
+ <body>This is going to cause an error.</body>
+ <error type="cancel" code="500">
+ <undefined-condition
+ xmlns="urn:ietf:params:xml:ns:xmpp-stanzas" />
+ </error>
+ </message>
+ """)
+
def testXMPPErrorException(self):
"""Test raising an XMPPError exception."""
@@ -124,9 +193,8 @@ class TestStreamExceptions(SleekTest):
def catch_error(*args, **kwargs):
raised_errors.append(True)
- sys.excepthook = catch_error
-
self.stream_start()
+ self.xmpp.exception = catch_error
self.xmpp.add_event_handler('message', message)
self.recv("""
@@ -149,6 +217,58 @@ class TestStreamExceptions(SleekTest):
self.assertEqual(raised_errors, [True], "Exception was not raised: %s" % raised_errors)
+ def testUnknownException(self):
+ """Test Sleek continues to respond after an unknown exception."""
+
+ raised_errors = []
+
+ def message(msg):
+ raise ValueError("Did something wrong")
+
+ def catch_error(*args, **kwargs):
+ raised_errors.append(True)
+
+ self.stream_start()
+ self.xmpp.exception = catch_error
+ self.xmpp.add_event_handler('message', message)
+
+ self.recv("""
+ <message>
+ <body>This is going to cause an error.</body>
+ </message>
+ """)
+
+ self.send("""
+ <message type="error">
+ <error type="cancel" code="500">
+ <undefined-condition
+ xmlns="urn:ietf:params:xml:ns:xmpp-stanzas" />
+ <text xmlns="urn:ietf:params:xml:ns:xmpp-stanzas">
+ SleekXMPP got into trouble.
+ </text>
+ </error>
+ </message>
+ """)
+
+ self.recv("""
+ <message>
+ <body>This is going to cause an error.</body>
+ </message>
+ """)
+
+ self.send("""
+ <message type="error">
+ <error type="cancel" code="500">
+ <undefined-condition
+ xmlns="urn:ietf:params:xml:ns:xmpp-stanzas" />
+ <text xmlns="urn:ietf:params:xml:ns:xmpp-stanzas">
+ SleekXMPP got into trouble.
+ </text>
+ </error>
+ </message>
+ """)
+
+ self.assertEqual(raised_errors, [True, True], "Exceptions were not raised: %s" % raised_errors)
suite = unittest.TestLoader().loadTestsFromTestCase(TestStreamExceptions)
diff --git a/tests/test_stream_presence.py b/tests/test_stream_presence.py
index 1d5caa98..0b086266 100644
--- a/tests/test_stream_presence.py
+++ b/tests/test_stream_presence.py
@@ -184,5 +184,56 @@ class TestStreamPresence(SleekTest):
self.assertEqual(events, expected,
"Incorrect events triggered: %s" % events)
+ def test_presence_events(self):
+ """Test that presence events are raised."""
+
+ events = []
+
+ self.stream_start()
+
+ ptypes = ['available', 'away', 'dnd', 'xa', 'chat',
+ 'unavailable', 'subscribe', 'subscribed',
+ 'unsubscribe', 'unsubscribed']
+
+ for ptype in ptypes:
+ handler = lambda p: events.append(p['type'])
+ self.xmpp.add_event_handler('presence_%s' % ptype, handler)
+
+ self.recv("""
+ <presence />
+ """)
+ self.recv("""
+ <presence><show>away</show></presence>
+ """)
+ self.recv("""
+ <presence><show>dnd</show></presence>
+ """)
+ self.recv("""
+ <presence><show>xa</show></presence>
+ """)
+ self.recv("""
+ <presence><show>chat</show></presence>
+ """)
+ self.recv("""
+ <presence type="unavailable" />
+ """)
+ self.recv("""
+ <presence type="subscribe" />
+ """)
+ self.recv("""
+ <presence type="subscribed" />
+ """)
+ self.recv("""
+ <presence type="unsubscribe" />
+ """)
+ self.recv("""
+ <presence type="unsubscribed" />
+ """)
+
+ time.sleep(.5)
+
+ self.assertEqual(events, ptypes,
+ "Not all events raised: %s" % events)
+
suite = unittest.TestLoader().loadTestsFromTestCase(TestStreamPresence)
diff --git a/tests/test_stream_xep_0030.py b/tests/test_stream_xep_0030.py
index c960fc7a..1666d3a1 100644
--- a/tests/test_stream_xep_0030.py
+++ b/tests/test_stream_xep_0030.py
@@ -12,7 +12,6 @@ class TestStreamDisco(SleekTest):
"""
def tearDown(self):
- sys.excepthook = sys.__excepthook__
self.stream_close()
def testInfoEmptyDefaultNode(self):
@@ -531,11 +530,6 @@ class TestStreamDisco(SleekTest):
raised_exceptions = []
- def catch_exception(*args, **kwargs):
- raised_exceptions.append(True)
-
- sys.excepthook = catch_exception
-
self.stream_start(mode='client',
plugins=['xep_0030', 'xep_0059'])
@@ -544,8 +538,14 @@ class TestStreamDisco(SleekTest):
iterator=True)
results.amount = 10
+ def run_test():
+ try:
+ results.next()
+ except StopIteration:
+ raised_exceptions.append(True)
+
t = threading.Thread(name="get_items_iterator",
- target=results.next)
+ target=run_test)
t.start()
self.send("""
diff --git a/tests/test_stream_xep_0066.py b/tests/test_stream_xep_0066.py
new file mode 100644
index 00000000..e3f2ddfa
--- /dev/null
+++ b/tests/test_stream_xep_0066.py
@@ -0,0 +1,44 @@
+import time
+import threading
+
+from sleekxmpp.test import *
+
+
+class TestOOB(SleekTest):
+
+ def tearDown(self):
+ self.stream_close()
+
+ def testSendOOB(self):
+ """Test sending an OOB transfer request."""
+ self.stream_start(plugins=['xep_0066', 'xep_0030'])
+
+ url = 'http://github.com/fritzy/SleekXMPP/blob/master/README'
+
+ t = threading.Thread(
+ name='send_oob',
+ target=self.xmpp['xep_0066'].send_oob,
+ args=('user@example.com', url),
+ kwargs={'desc': 'SleekXMPP README'})
+
+ t.start()
+
+ self.send("""
+ <iq to="user@example.com" type="set" id="1">
+ <query xmlns="jabber:iq:oob">
+ <url>http://github.com/fritzy/SleekXMPP/blob/master/README</url>
+ <desc>SleekXMPP README</desc>
+ </query>
+ </iq>
+ """)
+
+ self.recv("""
+ <iq id="1" type="result"
+ to="tester@localhost"
+ from="user@example.com" />
+ """)
+
+ t.join()
+
+
+suite = unittest.TestLoader().loadTestsFromTestCase(TestOOB)
diff --git a/tests/test_stream_xep_0128.py b/tests/test_stream_xep_0128.py
index 6fee6556..42fc9143 100644
--- a/tests/test_stream_xep_0128.py
+++ b/tests/test_stream_xep_0128.py
@@ -13,7 +13,6 @@ class TestStreamExtendedDisco(SleekTest):
"""
def tearDown(self):
- sys.excepthook = sys.__excepthook__
self.stream_close()
def testUsingExtendedInfo(self):
diff --git a/tests/test_stream_xep_0249.py b/tests/test_stream_xep_0249.py
index f49d1f7e..9a25253f 100644
--- a/tests/test_stream_xep_0249.py
+++ b/tests/test_stream_xep_0249.py
@@ -13,7 +13,6 @@ class TestStreamDirectInvite(SleekTest):
"""
def tearDown(self):
- sys.excepthook = sys.__excepthook__
self.stream_close()
def testReceiveInvite(self):
diff --git a/tests/test_tostring.py b/tests/test_tostring.py
index 638e613a..e456d28e 100644
--- a/tests/test_tostring.py
+++ b/tests/test_tostring.py
@@ -102,11 +102,13 @@ class TestToString(SleekTest):
"""
Test that stanza objects are serialized properly.
"""
+ self.stream_start()
+
utf8_message = '\xe0\xb2\xa0_\xe0\xb2\xa0'
if not hasattr(utf8_message, 'decode'):
# Python 3
utf8_message = bytes(utf8_message, encoding='utf-8')
- msg = Message()
+ msg = self.Message()
msg['body'] = utf8_message.decode('utf-8')
expected = '<message><body>\xe0\xb2\xa0_\xe0\xb2\xa0</body></message>'
result = msg.__str__()
diff --git a/todo1.0 b/todo1.0
index c01526fc..cdb0dcde 100644
--- a/todo1.0
+++ b/todo1.0
@@ -18,8 +18,6 @@ Plugins:
PEP8
Documentation
Stream/Unit tests
- 0050
- Review replacement in github.com/legastero/adhoc
0060
PEP8
Documentation
@@ -29,14 +27,6 @@ Plugins:
PEP8
Documentation
Stream/Unit tests
- 0086
- PEP8
- Documentation
- Consider any simplifications.
- 0202
- PEP8
- Documentation
- Stream/Unit tests
gmail_notify
PEP8
Documentation