summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore8
-rw-r--r--INSTALL12
-rw-r--r--LICENSE140
-rw-r--r--MANIFEST.in6
-rw-r--r--README.rst182
-rw-r--r--conn_tests/test_pubsubjobs.py171
-rw-r--r--conn_tests/test_pubsubserver.py233
-rw-r--r--conn_tests/testconfig.ini13
-rwxr-xr-xconn_tests/testpubsub.py350
-rw-r--r--docs/_static/haiku.css1
-rw-r--r--docs/_static/header.pngbin98587 -> 16588 bytes
-rw-r--r--docs/index.rst1
-rwxr-xr-xexamples/muc.py5
-rw-r--r--ez_setup.py233
-rwxr-xr-xsetup.py103
-rw-r--r--sleekxmpp/__init__.py18
-rw-r--r--sleekxmpp/basexmpp.py792
-rw-r--r--sleekxmpp/clientxmpp.py310
-rw-r--r--sleekxmpp/componentxmpp.py162
-rw-r--r--sleekxmpp/exceptions.py90
-rw-r--r--sleekxmpp/features/__init__.py9
-rw-r--r--sleekxmpp/features/feature_bind/__init__.py10
-rw-r--r--sleekxmpp/features/feature_bind/bind.py65
-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.py131
-rw-r--r--sleekxmpp/features/feature_mechanisms/stanza/__init__.py15
-rw-r--r--sleekxmpp/features/feature_mechanisms/stanza/auth.py49
-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__.py36
-rw-r--r--sleekxmpp/plugins/base.py91
-rw-r--r--sleekxmpp/plugins/gmail_notify.py149
-rw-r--r--sleekxmpp/plugins/jobs.py49
-rw-r--r--sleekxmpp/plugins/old_0004.py421
-rw-r--r--sleekxmpp/plugins/old_0009.py277
-rw-r--r--sleekxmpp/plugins/old_0050.py133
-rw-r--r--sleekxmpp/plugins/old_0060.py313
-rw-r--r--sleekxmpp/plugins/xep_0004/__init__.py11
-rw-r--r--sleekxmpp/plugins/xep_0004/dataforms.py60
-rw-r--r--sleekxmpp/plugins/xep_0004/stanza/__init__.py10
-rw-r--r--sleekxmpp/plugins/xep_0004/stanza/field.py180
-rw-r--r--sleekxmpp/plugins/xep_0004/stanza/form.py254
-rw-r--r--sleekxmpp/plugins/xep_0009/__init__.py11
-rw-r--r--sleekxmpp/plugins/xep_0009/binding.py169
-rw-r--r--sleekxmpp/plugins/xep_0009/remote.py742
-rw-r--r--sleekxmpp/plugins/xep_0009/rpc.py221
-rw-r--r--sleekxmpp/plugins/xep_0009/stanza/RPC.py64
-rw-r--r--sleekxmpp/plugins/xep_0009/stanza/__init__.py9
-rw-r--r--sleekxmpp/plugins/xep_0012.py115
-rw-r--r--sleekxmpp/plugins/xep_0030/__init__.py12
-rw-r--r--sleekxmpp/plugins/xep_0030/disco.py800
-rw-r--r--sleekxmpp/plugins/xep_0030/stanza/__init__.py10
-rw-r--r--sleekxmpp/plugins/xep_0030/stanza/info.py276
-rw-r--r--sleekxmpp/plugins/xep_0030/stanza/items.py136
-rw-r--r--sleekxmpp/plugins/xep_0030/static.py441
-rw-r--r--sleekxmpp/plugins/xep_0033.py161
-rw-r--r--sleekxmpp/plugins/xep_0045.py376
-rw-r--r--sleekxmpp/plugins/xep_0050/__init__.py10
-rw-r--r--sleekxmpp/plugins/xep_0050/adhoc.py614
-rw-r--r--sleekxmpp/plugins/xep_0050/stanza.py185
-rw-r--r--sleekxmpp/plugins/xep_0059/__init__.py10
-rw-r--r--sleekxmpp/plugins/xep_0059/rsm.py119
-rw-r--r--sleekxmpp/plugins/xep_0059/stanza.py108
-rw-r--r--sleekxmpp/plugins/xep_0060/__init__.py2
-rw-r--r--sleekxmpp/plugins/xep_0060/pubsub.py450
-rw-r--r--sleekxmpp/plugins/xep_0060/stanza/__init__.py12
-rw-r--r--sleekxmpp/plugins/xep_0060/stanza/base.py29
-rw-r--r--sleekxmpp/plugins/xep_0060/stanza/pubsub.py300
-rw-r--r--sleekxmpp/plugins/xep_0060/stanza/pubsub_errors.py86
-rw-r--r--sleekxmpp/plugins/xep_0060/stanza/pubsub_event.py112
-rw-r--r--sleekxmpp/plugins/xep_0060/stanza/pubsub_owner.py131
-rw-r--r--sleekxmpp/plugins/xep_0066/__init__.py11
-rw-r--r--sleekxmpp/plugins/xep_0066/oob.py153
-rw-r--r--sleekxmpp/plugins/xep_0066/stanza.py33
-rw-r--r--sleekxmpp/plugins/xep_0078/__init__.py12
-rw-r--r--sleekxmpp/plugins/xep_0078/legacyauth.py119
-rw-r--r--sleekxmpp/plugins/xep_0078/stanza.py43
-rw-r--r--sleekxmpp/plugins/xep_0082.py219
-rw-r--r--sleekxmpp/plugins/xep_0085/__init__.py10
-rw-r--r--sleekxmpp/plugins/xep_0085/chat_states.py49
-rw-r--r--sleekxmpp/plugins/xep_0085/stanza.py73
-rw-r--r--sleekxmpp/plugins/xep_0086/__init__.py10
-rw-r--r--sleekxmpp/plugins/xep_0086/legacy_error.py42
-rw-r--r--sleekxmpp/plugins/xep_0086/stanza.py91
-rw-r--r--sleekxmpp/plugins/xep_0092/__init__.py11
-rw-r--r--sleekxmpp/plugins/xep_0092/stanza.py42
-rw-r--r--sleekxmpp/plugins/xep_0092/version.py87
-rw-r--r--sleekxmpp/plugins/xep_0115/__init__.py11
-rw-r--r--sleekxmpp/plugins/xep_0115/caps.py306
-rw-r--r--sleekxmpp/plugins/xep_0115/stanza.py19
-rw-r--r--sleekxmpp/plugins/xep_0115/static.py147
-rw-r--r--sleekxmpp/plugins/xep_0128/__init__.py10
-rw-r--r--sleekxmpp/plugins/xep_0128/extended_disco.py101
-rw-r--r--sleekxmpp/plugins/xep_0128/static.py73
-rw-r--r--sleekxmpp/plugins/xep_0199/__init__.py10
-rw-r--r--sleekxmpp/plugins/xep_0199/ping.py175
-rw-r--r--sleekxmpp/plugins/xep_0199/stanza.py36
-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.py91
-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/plugins/xep_0249/__init__.py10
-rw-r--r--sleekxmpp/plugins/xep_0249/invite.py79
-rw-r--r--sleekxmpp/plugins/xep_0249/stanza.py39
-rw-r--r--sleekxmpp/roster/__init__.py12
-rw-r--r--sleekxmpp/roster/item.py487
-rw-r--r--sleekxmpp/roster/multi.py189
-rw-r--r--sleekxmpp/roster/single.py304
-rw-r--r--sleekxmpp/stanza/__init__.py15
-rw-r--r--sleekxmpp/stanza/atom.py26
-rw-r--r--sleekxmpp/stanza/error.py146
-rw-r--r--sleekxmpp/stanza/htmlim.py86
-rw-r--r--sleekxmpp/stanza/iq.py241
-rw-r--r--sleekxmpp/stanza/message.py157
-rw-r--r--sleekxmpp/stanza/nick.py78
-rw-r--r--sleekxmpp/stanza/presence.py180
-rw-r--r--sleekxmpp/stanza/rootstanza.py87
-rw-r--r--sleekxmpp/stanza/roster.py127
-rw-r--r--sleekxmpp/stanza/stream_error.py69
-rw-r--r--sleekxmpp/stanza/stream_features.py54
-rw-r--r--sleekxmpp/test/__init__.py11
-rw-r--r--sleekxmpp/test/livesocket.py174
-rw-r--r--sleekxmpp/test/mocksocket.py155
-rw-r--r--sleekxmpp/test/sleektest.py757
-rw-r--r--sleekxmpp/thirdparty/__init__.py7
-rw-r--r--sleekxmpp/thirdparty/mini_dateutil.py267
-rw-r--r--sleekxmpp/thirdparty/ordereddict.py127
-rw-r--r--sleekxmpp/thirdparty/statemachine.py287
-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__.py6
-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/messenger_oauth2.py17
-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/version.py13
-rw-r--r--sleekxmpp/xmlstream/__init__.py19
-rw-r--r--sleekxmpp/xmlstream/filesocket.py47
-rw-r--r--sleekxmpp/xmlstream/handler/__init__.py14
-rw-r--r--sleekxmpp/xmlstream/handler/base.py84
-rw-r--r--sleekxmpp/xmlstream/handler/callback.py79
-rw-r--r--sleekxmpp/xmlstream/handler/waiter.py87
-rw-r--r--sleekxmpp/xmlstream/handler/xmlcallback.py36
-rw-r--r--sleekxmpp/xmlstream/handler/xmlwaiter.py33
-rw-r--r--sleekxmpp/xmlstream/jid.py145
-rw-r--r--sleekxmpp/xmlstream/matcher/__init__.py16
-rw-r--r--sleekxmpp/xmlstream/matcher/base.py31
-rw-r--r--sleekxmpp/xmlstream/matcher/id.py29
-rw-r--r--sleekxmpp/xmlstream/matcher/many.py40
-rw-r--r--sleekxmpp/xmlstream/matcher/stanzapath.py43
-rw-r--r--sleekxmpp/xmlstream/matcher/xmlmask.py158
-rw-r--r--sleekxmpp/xmlstream/matcher/xpath.py86
-rw-r--r--sleekxmpp/xmlstream/scheduler.py228
-rw-r--r--sleekxmpp/xmlstream/stanzabase.py1323
-rw-r--r--sleekxmpp/xmlstream/test.py23
-rw-r--r--sleekxmpp/xmlstream/test.xml2
-rw-r--r--sleekxmpp/xmlstream/testclient.py13
-rw-r--r--sleekxmpp/xmlstream/tostring.py131
-rw-r--r--sleekxmpp/xmlstream/xmlstream.py1479
-rwxr-xr-xtestall.py63
-rw-r--r--tests/__init__.py0
-rw-r--r--tests/live_multiple_streams.py57
-rw-r--r--tests/live_test.py100
-rw-r--r--tests/test_events.py95
-rw-r--r--tests/test_jid.py141
-rw-r--r--tests/test_overall.py29
-rw-r--r--tests/test_stanza_base.py79
-rw-r--r--tests/test_stanza_element.py746
-rw-r--r--tests/test_stanza_error.py81
-rw-r--r--tests/test_stanza_gmail.py88
-rw-r--r--tests/test_stanza_iq.py90
-rw-r--r--tests/test_stanza_message.py57
-rw-r--r--tests/test_stanza_presence.py67
-rw-r--r--tests/test_stanza_roster.py88
-rw-r--r--tests/test_stanza_xep_0004.py198
-rw-r--r--tests/test_stanza_xep_0009.py288
-rw-r--r--tests/test_stanza_xep_0030.py516
-rw-r--r--tests/test_stanza_xep_0033.py111
-rw-r--r--tests/test_stanza_xep_0050.py114
-rw-r--r--tests/test_stanza_xep_0059.py106
-rw-r--r--tests/test_stanza_xep_0060.py575
-rw-r--r--tests/test_stanza_xep_0085.py41
-rw-r--r--tests/test_stream.py79
-rw-r--r--tests/test_stream_exceptions.py274
-rw-r--r--tests/test_stream_filters.py88
-rw-r--r--tests/test_stream_handlers.py201
-rw-r--r--tests/test_stream_presence.py380
-rw-r--r--tests/test_stream_roster.py231
-rw-r--r--tests/test_stream_xep_0030.py576
-rw-r--r--tests/test_stream_xep_0050.py726
-rw-r--r--tests/test_stream_xep_0059.py162
-rw-r--r--tests/test_stream_xep_0060.py794
-rw-r--r--tests/test_stream_xep_0066.py44
-rw-r--r--tests/test_stream_xep_0085.py59
-rw-r--r--tests/test_stream_xep_0092.py69
-rw-r--r--tests/test_stream_xep_0128.py105
-rw-r--r--tests/test_stream_xep_0249.py63
-rw-r--r--tests/test_tostring.py132
-rw-r--r--tox.ini5
222 files changed, 31014 insertions, 2 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 00000000..9b7733be
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,8 @@
+*.pyc
+build/
+dist/
+MANIFEST
+docs/_build/
+*.swp
+.tox/
+.coverage
diff --git a/INSTALL b/INSTALL
new file mode 100644
index 00000000..82f87123
--- /dev/null
+++ b/INSTALL
@@ -0,0 +1,12 @@
+Pre-requisites:
+- Python 3.1 or 2.6
+
+Install:
+> python3 setup.py install
+
+Root install:
+> sudo python3 setup.py install
+
+To test:
+> cd examples
+> python echo_client.py -v -j [USER@example.com] -p [PASSWORD]
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 00000000..612dd1d7
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,140 @@
+Copyright (c) 2010 Nathanael C. Fritz
+
+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.
+
+
+
+
+Licenses of Bundled Third Party 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/MANIFEST.in b/MANIFEST.in
new file mode 100644
index 00000000..f439bbd5
--- /dev/null
+++ b/MANIFEST.in
@@ -0,0 +1,6 @@
+include README.rst
+include LICENSE
+include testall.py
+recursive-include docs Makefile *.bat *.py *.rst *.css *.ttf *.png
+recursive-include examples *.py
+recursive-include tests *.py
diff --git a/README.rst b/README.rst
new file mode 100644
index 00000000..e2c289d9
--- /dev/null
+++ b/README.rst
@@ -0,0 +1,182 @@
+SleekXMPP
+#########
+
+SleekXMPP is an MIT licensed XMPP library for Python 2.6/3.1+,
+and is featured in examples in
+`XMPP: The Definitive Guide <http://oreilly.com/catalog/9780596521271>`_
+by Kevin Smith, Remko Tronçon, and Peter Saint-Andre. If you've arrived
+here from reading the Definitive Guide, please see the notes on updating
+the examples to the latest version of SleekXMPP.
+
+SleekXMPP's design goals and philosphy are:
+
+**Low number of dependencies**
+ Installing and using SleekXMPP should be as simple as possible, without
+ having to deal with long dependency chains.
+
+ As part of reducing the number of dependencies, some third party
+ modules are included with SleekXMPP in the ``thirdparty`` directory.
+ Imports from this module first try to import an existing installed
+ version before loading the packaged version, when possible.
+
+**Every XEP as a plugin**
+ Following Python's "batteries included" approach, the goal is to
+ provide support for all currently active XEPs (final and draft). Since
+ adding XEP support is done through easy to create plugins, the hope is
+ to also provide a solid base for implementing and creating experimental
+ XEPs.
+
+**Rewarding to work with**
+ As much as possible, SleekXMPP should allow things to "just work" using
+ sensible defaults and appropriate abstractions. XML can be ugly to work
+ with, but it doesn't have to be that way.
+
+
+Get the Code
+------------
+
+Get the latest stable version from PyPI::
+
+ pip install sleekxmpp
+
+The latest source code for SleekXMPP may be found on `Github
+<http://github.com/fritzy/SleekXMPP>`_. Releases can be found in the
+``master`` branch, while the latest development version is in the
+``develop`` branch.
+
+**Latest Release**
+ - `1.0 <http://github.com/fritzy/SleekXMPP/zipball/1.0>`_
+
+**Develop Releases**
+ - `Latest Develop Version <http://github.com/fritzy/SleekXMPP/zipball/develop>`_
+
+**Older Stable Releases**
+ - `1.0 RC3 <http://github.com/fritzy/SleekXMPP/zipball/1.0-RC3>`_
+ - `1.0 RC2 <http://github.com/fritzy/SleekXMPP/zipball/1.0-RC2>`_
+ - `1.0 RC1 <http://github.com/fritzy/SleekXMPP/zipball/1.0-RC1>`_
+ - `1.0 Beta6.1 <http://github.com/fritzy/SleekXMPP/zipball/1.0-Beta6.1>`_
+ - `1.0 Beta5 <http://github.com/fritzy/SleekXMPP/zipball/1.0-Beta5>`_
+ - `1.0 Beta4 <http://github.com/fritzy/SleekXMPP/zipball/1.0-Beta4>`_
+ - `1.0 Beta3 <http://github.com/fritzy/SleekXMPP/zipball/1.0-Beta3>`_
+ - `1.0 Beta2 <http://github.com/fritzy/SleekXMPP/zipball/1.0-Beta2>`_
+ - `1.0 Beta1 <http://github.com/fritzy/SleekXMPP/zipball/1.0-Beta1>`_
+
+Installing DNSPython
+---------------------
+If you are using Python3 and wish to use dnspython, you will have to checkout and
+install the ``python3`` branch::
+
+ git clone http://github.com/rthalley/dnspython
+ cd dnspython
+ git checkout python3
+ python3 setup.py install
+
+Discussion
+----------
+A mailing list and XMPP chat room are available for discussing and getting
+help with SleekXMPP.
+
+**Mailing List**
+ `SleekXMPP Discussion on Google Groups <http://groups.google.com/group/sleekxmpp-discussion>`_
+
+**Chat**
+ `sleek@conference.jabber.org <xmpp:sleek@conference.jabber.org?join>`_
+
+Documentation and Testing
+-------------------------
+Documentation can be found both inline in the code, and as a Sphinx project in ``/docs``.
+To generate the Sphinx documentation, follow the commands below. The HTML output will
+be in ``docs/_build/html``::
+
+ cd docs
+ make html
+ open _build/html/index.html
+
+To run the test suite for SleekXMPP::
+
+ python testall.py
+
+
+The SleekXMPP Boilerplate
+-------------------------
+Projects using SleekXMPP tend to follow a basic pattern for setting up client/component
+connections and configuration. Here is the gist of the boilerplate needed for a SleekXMPP
+based project. See the documetation or examples directory for more detailed archetypes for
+SleekXMPP projects::
+
+ import logging
+
+ from sleekxmpp import ClientXMPP
+ from sleekxmpp.exceptions import IqError, IqTimeout
+
+
+ class EchoBot(ClientXMPP):
+
+ def __init__(self, jid, password):
+ ClientXMPP.__init__(self, jid, password)
+
+ self.add_event_handler("session_start", self.session_start)
+ self.add_event_handler("message", self.message)
+
+ self.register_plugin('xep_0030') # Service Discovery
+ self.register_plugin('xep_0199') # XMPP Ping
+
+ # If you are working with an OpenFire server, you will
+ # need to use a different SSL version:
+ # import ssl
+ # self.ssl_version = ssl.PROTOCOL_SSLv3
+
+ def session_start(self, event):
+ self.send_presence()
+
+ # Most get_*/set_* methods from plugins use Iq stanzas, which
+ # can generate IqError and IqTimeout exceptions
+ try:
+ self.get_roster()
+ except IqError as err:
+ logging.error('There was an error getting the roster')
+ logging.error(err.iq['error']['condition'])
+ self.disconnect()
+ except IqTimeout:
+ logging.error('Server is taking too long to respond')
+ self.disconnect()
+
+ def message(self, msg):
+ if msg['type'] in ('chat', 'normal'):
+ msg.reply("Thanks for sending\n%(body)s" % msg).send()
+
+
+ if __name__ == '__main__':
+ # Ideally use optparse or argparse to get JID,
+ # password, and log level.
+
+ logging.basicConfig(level=logging.DEBUG,
+ format='%(levelname)-8s %(message)s')
+
+ xmpp = EchoBot('somejid@example.com', 'use_getpass')
+ xmpp.connect()
+ xmpp.process(block=True)
+
+
+Credits
+-------
+**Main Author:** Nathan Fritz
+ `fritzy@netflint.net <xmpp:fritzy@netflint.net?message>`_,
+ `@fritzy <http://twitter.com/fritzy>`_
+
+ Nathan is also the author of XMPPHP and `Seesmic-AS3-XMPP
+ <http://code.google.com/p/seesmic-as3-xmpp/>`_, and a former member of
+ the XMPP Council.
+
+**Co-Author:** Lance Stout
+ `lancestout@gmail.com <xmpp:lancestout@gmail.com?message>`_,
+ `@lancestout <http://twitter.com/lancestout>`_
+
+**Contributors:**
+ - Brian Beggs (`macdiesel <http://github.com/macdiesel>`_)
+ - Dann Martens (`dannmartens <http://github.com/dannmartens>`_)
+ - Florent Le Coz (`louiz <http://github.com/louiz>`_)
+ - Kevin Smith (`Kev <http://github.com/Kev>`_, http://kismith.co.uk)
+ - Remko Tronçon (`remko <http://github.com/remko>`_, http://el-tramo.be)
+ - Te-jé Rogers (`te-je <http://github.com/te-je>`_)
+ - Thom Nichols (`tomstrummer <http://github.com/tomstrummer>`_)
diff --git a/conn_tests/test_pubsubjobs.py b/conn_tests/test_pubsubjobs.py
new file mode 100644
index 00000000..ec2a2716
--- /dev/null
+++ b/conn_tests/test_pubsubjobs.py
@@ -0,0 +1,171 @@
+import logging
+import sleekxmpp
+from optparse import OptionParser
+from xml.etree import cElementTree as ET
+import os
+import time
+import sys
+import unittest
+import sleekxmpp.plugins.xep_0004
+from sleekxmpp.xmlstream.matcher.stanzapath import StanzaPath
+from sleekxmpp.xmlstream.handler.waiter import Waiter
+try:
+ import configparser
+except ImportError:
+ import ConfigParser as configparser
+try:
+ import queue
+except ImportError:
+ import Queue as queue
+
+class TestClient(sleekxmpp.ClientXMPP):
+ def __init__(self, jid, password):
+ sleekxmpp.ClientXMPP.__init__(self, jid, password)
+ self.add_event_handler("session_start", self.start)
+ #self.add_event_handler("message", self.message)
+ self.waitforstart = queue.Queue()
+
+ def start(self, event):
+ self.getRoster()
+ self.sendPresence()
+ self.waitforstart.put(True)
+
+
+class TestPubsubServer(unittest.TestCase):
+ statev = {}
+
+ def __init__(self, *args, **kwargs):
+ unittest.TestCase.__init__(self, *args, **kwargs)
+
+ def setUp(self):
+ pass
+
+ def test001getdefaultconfig(self):
+ """Get the default node config"""
+ self.xmpp1['xep_0060'].deleteNode(self.pshost, 'testnode2')
+ self.xmpp1['xep_0060'].deleteNode(self.pshost, 'testnode3')
+ self.xmpp1['xep_0060'].deleteNode(self.pshost, 'testnode4')
+ self.xmpp1['xep_0060'].deleteNode(self.pshost, 'testnode5')
+ result = self.xmpp1['xep_0060'].getNodeConfig(self.pshost)
+ self.statev['defaultconfig'] = result
+ self.failUnless(isinstance(result, sleekxmpp.plugins.xep_0004.Form))
+
+ def test002createdefaultnode(self):
+ """Create a node without config"""
+ self.failUnless(self.xmpp1['xep_0060'].create_node(self.pshost, 'testnode1'))
+
+ def test003deletenode(self):
+ """Delete recently created node"""
+ self.failUnless(self.xmpp1['xep_0060'].deleteNode(self.pshost, 'testnode1'))
+
+ def test004createnode(self):
+ """Create a node with a config"""
+ self.statev['defaultconfig'].field['pubsub#access_model'].setValue('open')
+ self.statev['defaultconfig'].field['pubsub#notify_retract'].setValue(True)
+ self.statev['defaultconfig'].field['pubsub#persist_items'].setValue(True)
+ self.statev['defaultconfig'].field['pubsub#presence_based_delivery'].setValue(True)
+ p = self.xmpp2.Presence()
+ p['to'] = self.pshost
+ p.send()
+ self.failUnless(self.xmpp1['xep_0060'].create_node(self.pshost, 'testnode2', self.statev['defaultconfig'], ntype='job'))
+
+ def test005reconfigure(self):
+ """Retrieving node config and reconfiguring"""
+ nconfig = self.xmpp1['xep_0060'].getNodeConfig(self.pshost, 'testnode2')
+ self.failUnless(nconfig, "No configuration returned")
+ #print("\n%s ==\n %s" % (nconfig.getValues(), self.statev['defaultconfig'].getValues()))
+ self.failUnless(nconfig.getValues() == self.statev['defaultconfig'].getValues(), "Configuration does not match")
+ self.failUnless(self.xmpp1['xep_0060'].setNodeConfig(self.pshost, 'testnode2', nconfig))
+
+ def test006subscribetonode(self):
+ """Subscribe to node from account 2"""
+ self.failUnless(self.xmpp2['xep_0060'].subscribe(self.pshost, "testnode2"))
+
+ def test007publishitem(self):
+ """Publishing item"""
+ item = ET.Element('{http://netflint.net/protocol/test}test')
+ w = Waiter('wait publish', StanzaPath('message/pubsub_event/items'))
+ self.xmpp2.registerHandler(w)
+ #result = self.xmpp1['xep_0060'].setItem(self.pshost, "testnode2", (('test1', item),))
+ result = self.xmpp1['jobs'].createJob(self.pshost, "testnode2", 'test1', item)
+ msg = w.wait(5) # got to get a result in 5 seconds
+ self.failUnless(msg != False, "Account #2 did not get message event")
+ #result = self.xmpp1['xep_0060'].setItem(self.pshost, "testnode2", (('test2', item),))
+ result = self.xmpp1['jobs'].createJob(self.pshost, "testnode2", 'test2', item)
+ w = Waiter('wait publish2', StanzaPath('message/pubsub_event/items'))
+ self.xmpp2.registerHandler(w)
+ self.xmpp2['jobs'].claimJob(self.pshost, 'testnode2', 'test1')
+ msg = w.wait(5) # got to get a result in 5 seconds
+ self.xmpp2['jobs'].claimJob(self.pshost, 'testnode2', 'test2')
+ self.xmpp2['jobs'].finishJob(self.pshost, 'testnode2', 'test1')
+ self.xmpp2['jobs'].finishJob(self.pshost, 'testnode2', 'test2')
+ print result
+ #need to add check for update
+
+ def test900cleanup(self):
+ "Cleaning up"
+ #self.failUnless(self.xmpp1['xep_0060'].deleteNode(self.pshost, 'testnode2'), "Could not delete test node.")
+ time.sleep(10)
+
+
+if __name__ == '__main__':
+ #parse command line arguements
+ optp = OptionParser()
+ optp.add_option('-q','--quiet', help='set logging to ERROR', action='store_const', dest='loglevel', const=logging.ERROR, default=logging.INFO)
+ optp.add_option('-d','--debug', help='set logging to DEBUG', action='store_const', dest='loglevel', const=logging.DEBUG, default=logging.INFO)
+ optp.add_option('-v','--verbose', help='set logging to COMM', action='store_const', dest='loglevel', const=5, default=logging.INFO)
+ optp.add_option("-c","--config", dest="configfile", default="config.xml", help="set config file to use")
+ optp.add_option("-n","--nodenum", dest="nodenum", default="1", help="set node number to use")
+ optp.add_option("-p","--pubsub", dest="pubsub", default="1", help="set pubsub host to use")
+ opts,args = optp.parse_args()
+
+ logging.basicConfig(level=opts.loglevel, format='%(levelname)-8s %(message)s')
+
+ #load xml config
+ logging.info("Loading config file: %s" , opts.configfile)
+ config = configparser.RawConfigParser()
+ config.read(opts.configfile)
+
+ #init
+ logging.info("Account 1 is %s" , config.get('account1', 'jid'))
+ xmpp1 = TestClient(config.get('account1','jid'), config.get('account1','pass'))
+ logging.info("Account 2 is %s" , config.get('account2', 'jid'))
+ xmpp2 = TestClient(config.get('account2','jid'), config.get('account2','pass'))
+
+ xmpp1.registerPlugin('xep_0004')
+ xmpp1.registerPlugin('xep_0030')
+ xmpp1.registerPlugin('xep_0060')
+ xmpp1.registerPlugin('xep_0199')
+ xmpp1.registerPlugin('jobs')
+ xmpp2.registerPlugin('xep_0004')
+ xmpp2.registerPlugin('xep_0030')
+ xmpp2.registerPlugin('xep_0060')
+ xmpp2.registerPlugin('xep_0199')
+ xmpp2.registerPlugin('jobs')
+
+ if not config.get('account1', 'server'):
+ # we don't know the server, but the lib can probably figure it out
+ xmpp1.connect()
+ else:
+ xmpp1.connect((config.get('account1', 'server'), 5222))
+ xmpp1.process(threaded=True)
+
+ #init
+ if not config.get('account2', 'server'):
+ # we don't know the server, but the lib can probably figure it out
+ xmpp2.connect()
+ else:
+ xmpp2.connect((config.get('account2', 'server'), 5222))
+ xmpp2.process(threaded=True)
+
+ TestPubsubServer.xmpp1 = xmpp1
+ TestPubsubServer.xmpp2 = xmpp2
+ TestPubsubServer.pshost = config.get('settings', 'pubsub')
+ xmpp1.waitforstart.get(True)
+ xmpp2.waitforstart.get(True)
+ testsuite = unittest.TestLoader().loadTestsFromTestCase(TestPubsubServer)
+
+ alltests_suite = unittest.TestSuite([testsuite])
+ result = unittest.TextTestRunner(verbosity=2).run(alltests_suite)
+ xmpp1.disconnect()
+ xmpp2.disconnect()
diff --git a/conn_tests/test_pubsubserver.py b/conn_tests/test_pubsubserver.py
new file mode 100644
index 00000000..aae77dd3
--- /dev/null
+++ b/conn_tests/test_pubsubserver.py
@@ -0,0 +1,233 @@
+import logging
+import sleekxmpp
+from optparse import OptionParser
+from xml.etree import cElementTree as ET
+import os
+import time
+import sys
+import unittest
+import sleekxmpp.plugins.xep_0004
+from sleekxmpp.xmlstream.matcher.stanzapath import StanzaPath
+from sleekxmpp.xmlstream.handler.waiter import Waiter
+try:
+ import configparser
+except ImportError:
+ import ConfigParser as configparser
+try:
+ import queue
+except ImportError:
+ import Queue as queue
+
+class TestClient(sleekxmpp.ClientXMPP):
+ def __init__(self, jid, password):
+ sleekxmpp.ClientXMPP.__init__(self, jid, password)
+ self.add_event_handler("session_start", self.start)
+ #self.add_event_handler("message", self.message)
+ self.waitforstart = queue.Queue()
+
+ def start(self, event):
+ self.getRoster()
+ self.sendPresence()
+ self.waitforstart.put(True)
+
+
+class TestPubsubServer(unittest.TestCase):
+ statev = {}
+
+ def __init__(self, *args, **kwargs):
+ unittest.TestCase.__init__(self, *args, **kwargs)
+
+ def setUp(self):
+ pass
+
+ def test001getdefaultconfig(self):
+ """Get the default node config"""
+ self.xmpp1['xep_0060'].deleteNode(self.pshost, 'testnode2')
+ self.xmpp1['xep_0060'].deleteNode(self.pshost, 'testnode3')
+ self.xmpp1['xep_0060'].deleteNode(self.pshost, 'testnode4')
+ self.xmpp1['xep_0060'].deleteNode(self.pshost, 'testnode5')
+ result = self.xmpp1['xep_0060'].getNodeConfig(self.pshost)
+ self.statev['defaultconfig'] = result
+ self.failUnless(isinstance(result, sleekxmpp.plugins.xep_0004.Form))
+
+ def test002createdefaultnode(self):
+ """Create a node without config"""
+ self.failUnless(self.xmpp1['xep_0060'].create_node(self.pshost, 'testnode1'))
+
+ def test003deletenode(self):
+ """Delete recently created node"""
+ self.failUnless(self.xmpp1['xep_0060'].deleteNode(self.pshost, 'testnode1'))
+
+ def test004createnode(self):
+ """Create a node with a config"""
+ self.statev['defaultconfig'].field['pubsub#access_model'].setValue('open')
+ self.statev['defaultconfig'].field['pubsub#notify_retract'].setValue(True)
+ self.failUnless(self.xmpp1['xep_0060'].create_node(self.pshost, 'testnode2', self.statev['defaultconfig']))
+
+ def test005reconfigure(self):
+ """Retrieving node config and reconfiguring"""
+ nconfig = self.xmpp1['xep_0060'].getNodeConfig(self.pshost, 'testnode2')
+ self.failUnless(nconfig, "No configuration returned")
+ #print("\n%s ==\n %s" % (nconfig.getValues(), self.statev['defaultconfig'].getValues()))
+ self.failUnless(nconfig.getValues() == self.statev['defaultconfig'].getValues(), "Configuration does not match")
+ self.failUnless(self.xmpp1['xep_0060'].setNodeConfig(self.pshost, 'testnode2', nconfig))
+
+ def test006subscribetonode(self):
+ """Subscribe to node from account 2"""
+ self.failUnless(self.xmpp2['xep_0060'].subscribe(self.pshost, "testnode2"))
+
+ def test007publishitem(self):
+ """Publishing item"""
+ item = ET.Element('{http://netflint.net/protocol/test}test')
+ w = Waiter('wait publish', StanzaPath('message/pubsub_event/items'))
+ self.xmpp2.registerHandler(w)
+ result = self.xmpp1['xep_0060'].setItem(self.pshost, "testnode2", (('test1', item),))
+ msg = w.wait(5) # got to get a result in 5 seconds
+ self.failUnless(msg != False, "Account #2 did not get message event")
+ self.failUnless(result)
+ #need to add check for update
+
+ def test008updateitem(self):
+ """Updating item"""
+ item = ET.Element('{http://netflint.net/protocol/test}test', {'someattr': 'hi there'})
+ w = Waiter('wait publish', StanzaPath('message/pubsub_event/items'))
+ self.xmpp2.registerHandler(w)
+ result = self.xmpp1['xep_0060'].setItem(self.pshost, "testnode2", (('test1', item),))
+ msg = w.wait(5) # got to get a result in 5 seconds
+ self.failUnless(msg != False, "Account #2 did not get message event")
+ self.failUnless(result)
+ #need to add check for update
+
+ def test009deleteitem(self):
+ """Deleting item"""
+ w = Waiter('wait retract', StanzaPath('message/pubsub_event/items@node=testnode2'))
+ self.xmpp2.registerHandler(w)
+ result = self.xmpp1['xep_0060'].deleteItem(self.pshost, "testnode2", "test1")
+ self.failUnless(result, "Got error when deleting item.")
+ msg = w.wait(1)
+ self.failUnless(msg != False, "Did not get retract notice.")
+
+ def test010unsubscribenode(self):
+ "Unsubscribing Account #2"
+ self.failUnless(self.xmpp2['xep_0060'].unsubscribe(self.pshost, "testnode2"), "Got error response when unsubscribing.")
+
+ def test011createcollectionnode(self):
+ "Create a collection node w/ Account #2"
+ self.failUnless(self.xmpp2['xep_0060'].create_node(self.pshost, "testnode3", self.statev['defaultconfig'], True), "Could not create collection node")
+
+ def test012subscribecollection(self):
+ "Subscribe Account #1 to collection"
+ self.failUnless(self.xmpp1['xep_0060'].subscribe(self.pshost, "testnode3"))
+
+ def test013assignnodetocollection(self):
+ "Assign node to collection"
+ self.failUnless(self.xmpp2['xep_0060'].addNodeToCollection(self.pshost, 'testnode2', 'testnode3'))
+
+ def test014publishcollection(self):
+ """Publishing item to collection child"""
+ item = ET.Element('{http://netflint.net/protocol/test}test')
+ w = Waiter('wait publish2', StanzaPath('message/pubsub_event/items@node=testnode2'))
+ self.xmpp1.registerHandler(w)
+ result = self.xmpp2['xep_0060'].setItem(self.pshost, "testnode2", (('test2', item),))
+ msg = w.wait(5) # got to get a result in 5 seconds
+ self.failUnless(msg != False, "Account #1 did not get message event: perhaps node was advertised incorrectly?")
+ self.failUnless(result)
+
+# def test016speedtest(self):
+# "Uncached speed test"
+# import time
+# start = time.time()
+# for y in range(0, 50000, 1000):
+# start2 = time.time()
+# for x in range(y, y+1000):
+# self.failUnless(self.xmpp1['xep_0060'].subscribe(self.pshost, "testnode4", subscribee="testuser%s@whatever" % x))
+# print time.time() - start2
+# seconds = time.time() - start
+# print "--", seconds
+# print "---------"
+# time.sleep(15)
+# self.failUnless(self.xmpp1['xep_0060'].deleteNode(self.pshost, 'testnode4'), "Could not delete non-cached test node")
+
+# def test015speedtest(self):
+# "cached speed test"
+# result = self.xmpp1['xep_0060'].getNodeConfig(self.pshost)
+# self.statev['defaultconfig'] = result
+# self.statev['defaultconfig'].field['pubsub#node_type'].setValue("leaf")
+# self.statev['defaultconfig'].field['sleek#saveonchange'].setValue(True)
+# self.failUnless(self.xmpp1['xep_0060'].create_node(self.pshost, 'testnode4', self.statev['defaultconfig']))
+# self.statev['defaultconfig'].field['sleek#saveonchange'].setValue(False)
+# self.failUnless(self.xmpp1['xep_0060'].create_node(self.pshost, 'testnode5', self.statev['defaultconfig']))
+# start = time.time()
+# for y in range(0, 50000, 1000):
+# start2 = time.time()
+# for x in range(y, y+1000):
+# self.failUnless(self.xmpp1['xep_0060'].subscribe(self.pshost, "testnode5", subscribee="testuser%s@whatever" % x))
+# print time.time() - start2
+# seconds = time.time() - start
+# print "--", seconds
+
+ def test900cleanup(self):
+ "Cleaning up"
+ self.failUnless(self.xmpp1['xep_0060'].deleteNode(self.pshost, 'testnode2'), "Could not delete test node.")
+ self.failUnless(self.xmpp1['xep_0060'].deleteNode(self.pshost, 'testnode3'), "Could not delete collection node")
+
+
+if __name__ == '__main__':
+ #parse command line arguements
+ optp = OptionParser()
+ optp.add_option('-q','--quiet', help='set logging to ERROR', action='store_const', dest='loglevel', const=logging.ERROR, default=logging.INFO)
+ optp.add_option('-d','--debug', help='set logging to DEBUG', action='store_const', dest='loglevel', const=logging.DEBUG, default=logging.INFO)
+ optp.add_option('-v','--verbose', help='set logging to COMM', action='store_const', dest='loglevel', const=5, default=logging.INFO)
+ optp.add_option("-c","--config", dest="configfile", default="config.xml", help="set config file to use")
+ optp.add_option("-n","--nodenum", dest="nodenum", default="1", help="set node number to use")
+ optp.add_option("-p","--pubsub", dest="pubsub", default="1", help="set pubsub host to use")
+ opts,args = optp.parse_args()
+
+ logging.basicConfig(level=opts.loglevel, format='%(levelname)-8s %(message)s')
+
+ #load xml config
+ logging.info("Loading config file: %s" , opts.configfile)
+ config = configparser.RawConfigParser()
+ config.read(opts.configfile)
+
+ #init
+ logging.info("Account 1 is %s" , config.get('account1', 'jid'))
+ xmpp1 = TestClient(config.get('account1','jid'), config.get('account1','pass'))
+ logging.info("Account 2 is %s" , config.get('account2', 'jid'))
+ xmpp2 = TestClient(config.get('account2','jid'), config.get('account2','pass'))
+
+ xmpp1.registerPlugin('xep_0004')
+ xmpp1.registerPlugin('xep_0030')
+ xmpp1.registerPlugin('xep_0060')
+ xmpp1.registerPlugin('xep_0199')
+ xmpp2.registerPlugin('xep_0004')
+ xmpp2.registerPlugin('xep_0030')
+ xmpp2.registerPlugin('xep_0060')
+ xmpp2.registerPlugin('xep_0199')
+
+ if not config.get('account1', 'server'):
+ # we don't know the server, but the lib can probably figure it out
+ xmpp1.connect()
+ else:
+ xmpp1.connect((config.get('account1', 'server'), 5222))
+ xmpp1.process(threaded=True)
+
+ #init
+ if not config.get('account2', 'server'):
+ # we don't know the server, but the lib can probably figure it out
+ xmpp2.connect()
+ else:
+ xmpp2.connect((config.get('account2', 'server'), 5222))
+ xmpp2.process(threaded=True)
+
+ TestPubsubServer.xmpp1 = xmpp1
+ TestPubsubServer.xmpp2 = xmpp2
+ TestPubsubServer.pshost = config.get('settings', 'pubsub')
+ xmpp1.waitforstart.get(True)
+ xmpp2.waitforstart.get(True)
+ testsuite = unittest.TestLoader().loadTestsFromTestCase(TestPubsubServer)
+
+ alltests_suite = unittest.TestSuite([testsuite])
+ result = unittest.TextTestRunner(verbosity=2).run(alltests_suite)
+ xmpp1.disconnect()
+ xmpp2.disconnect()
diff --git a/conn_tests/testconfig.ini b/conn_tests/testconfig.ini
new file mode 100644
index 00000000..fc57bd17
--- /dev/null
+++ b/conn_tests/testconfig.ini
@@ -0,0 +1,13 @@
+[settings]
+enabled=true
+pubsub=pubsub.recon
+
+[account1]
+jid=fritzy@recon
+pass=testing123
+server=
+
+[account2]
+jid=fritzy2@recon
+pass=testing123
+server=
diff --git a/conn_tests/testpubsub.py b/conn_tests/testpubsub.py
new file mode 100755
index 00000000..0f46524e
--- /dev/null
+++ b/conn_tests/testpubsub.py
@@ -0,0 +1,350 @@
+"""
+ 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 sleekxmpp
+from optparse import OptionParser
+from xml.etree import cElementTree as ET
+import os
+import time
+import sys
+import Queue
+import thread
+
+
+class testps(sleekxmpp.ClientXMPP):
+ def __init__(self, jid, password, ssl=False, plugin_config = {}, plugin_whitelist=[], nodenum=0, pshost=None):
+ sleekxmpp.ClientXMPP.__init__(self, jid, password, ssl, plugin_config, plugin_whitelist)
+ self.registerPlugin('xep_0004')
+ self.registerPlugin('xep_0030')
+ self.registerPlugin('xep_0060')
+ self.registerPlugin('xep_0092')
+ self.add_handler("<message xmlns='jabber:client'><event xmlns='http://jabber.org/protocol/pubsub#event' /></message>", self.pubsubEventHandler, name='Pubsub Event', threaded=True)
+ self.add_event_handler("session_start", self.start, threaded=True)
+ self.add_handler("<iq type='error' />", self.handleError, name='Iq Error')
+ self.events = Queue.Queue()
+ self.default_config = None
+ self.ps = self.plugin['xep_0060']
+ self.node = "pstestnode_%s"
+ self.pshost = pshost
+ if pshost is None:
+ self.pshost = self.boundjid.host
+ self.nodenum = int(nodenum)
+ self.leafnode = self.nodenum + 1
+ self.collectnode = self.nodenum + 2
+ self.lasterror = ''
+ self.sprintchars = 0
+ self.defaultconfig = None
+ self.tests = ['test_defaultConfig', 'test_createDefaultNode', 'test_getNodes', 'test_deleteNode', 'test_createWithConfig', 'test_reconfigureNode', 'test_subscribeToNode', 'test_addItem', 'test_updateItem', 'test_deleteItem', 'test_unsubscribeNode', 'test_createCollection', 'test_subscribeCollection', 'test_addNodeCollection', 'test_deleteNodeCollection', 'test_addCollectionNode', 'test_deleteCollectionNode', 'test_unsubscribeNodeCollection', 'test_deleteCollection']
+ self.passed = 0
+ self.width = 120
+
+ def start(self, event):
+ #TODO: make this configurable
+ self.getRoster()
+ self.sendPresence(ppriority=20)
+ thread.start_new(self.test_all, tuple())
+
+ def sprint(self, msg, end=False, color=False):
+ length = len(msg)
+ if color:
+ if color == "red":
+ color = "1;31"
+ elif color == "green":
+ color = "0;32"
+ msg = "%s%s%s" % ("\033[%sm" % color, msg, "\033[0m")
+ if not end:
+ sys.stdout.write(msg)
+ self.sprintchars += length
+ else:
+ self.sprint("%s%s" % ("." * (self.width - self.sprintchars - length), msg))
+ print('')
+ self.sprintchars = 0
+ sys.stdout.flush()
+
+ def pubsubEventHandler(self, xml):
+ for item in xml.findall('{http://jabber.org/protocol/pubsub#event}event/{http://jabber.org/protocol/pubsub#event}items/{http://jabber.org/protocol/pubsub#event}item'):
+ self.events.put(item.get('id', '__unknown__'))
+ for item in xml.findall('{http://jabber.org/protocol/pubsub#event}event/{http://jabber.org/protocol/pubsub#event}items/{http://jabber.org/protocol/pubsub#event}retract'):
+ self.events.put(item.get('id', '__unknown__'))
+ for item in xml.findall('{http://jabber.org/protocol/pubsub#event}event/{http://jabber.org/protocol/pubsub#event}collection/{http://jabber.org/protocol/pubsub#event}disassociate'):
+ self.events.put(item.get('node', '__unknown__'))
+ for item in xml.findall('{http://jabber.org/protocol/pubsub#event}event/{http://jabber.org/protocol/pubsub#event}collection/{http://jabber.org/protocol/pubsub#event}associate'):
+ self.events.put(item.get('node', '__unknown__'))
+
+ def handleError(self, xml):
+ error = xml.find('{jabber:client}error')
+ self.lasterror = error.getchildren()[0].tag.split('}')[-1]
+
+ def test_all(self):
+ print("Running Publish-Subscribe Tests")
+ version = self.plugin['xep_0092'].getVersion(self.pshost)
+ if version:
+ print("%s %s on %s" % (version.get('name', 'Unknown Server'), version.get('version', 'v?'), version.get('os', 'Unknown OS')))
+ print("=" * self.width)
+ for test in self.tests:
+ testfunc = getattr(self, test)
+ self.sprint("%s" % testfunc.__doc__)
+ if testfunc():
+ self.sprint("Passed", True, "green")
+ self.passed += 1
+ else:
+ if not self.lasterror:
+ self.lasterror = 'No response'
+ self.sprint("Failed (%s)" % self.lasterror, True, "red")
+ self.lasterror = ''
+ print("=" * self.width)
+ self.sprint("Cleaning up...")
+ #self.ps.deleteNode(self.pshost, self.node % self.nodenum)
+ self.ps.deleteNode(self.pshost, self.node % self.leafnode)
+ #self.ps.deleteNode(self.pshost, self.node % self.collectnode)
+ self.sprint("Done", True, "green")
+ self.disconnect()
+ self.sprint("%s" % self.passed, False, "green")
+ self.sprint("/%s Passed -- " % len(self.tests))
+ if len(self.tests) - self.passed:
+ self.sprint("%s" % (len(self.tests) - self.passed), False, "red")
+ else:
+ self.sprint("%s" % (len(self.tests) - self.passed), False, "green")
+ self.sprint(" Failed Tests")
+ print
+ #print "%s/%s Passed -- %s Failed Tests" % (self.passed, len(self.tests), len(self.tests) - self.passed)
+
+ def test_defaultConfig(self):
+ "Retreiving default configuration"
+ result = self.ps.getNodeConfig(self.pshost)
+ if result is False or result is None:
+ return False
+ else:
+ self.defaultconfig = result
+ try:
+ self.defaultconfig.field['pubsub#access_model'].setValue('open')
+ except KeyError:
+ pass
+ try:
+ self.defaultconfig.field['pubsub#notify_retract'].setValue(True)
+ except KeyError:
+ pass
+ return True
+
+ def test_createDefaultNode(self):
+ "Creating default node"
+ return self.ps.create_node(self.pshost, self.node % self.nodenum)
+
+ def test_getNodes(self):
+ "Getting list of nodes"
+ self.ps.getNodes(self.pshost)
+ self.ps.getItems(self.pshost, 'blog')
+ return True
+
+ def test_deleteNode(self):
+ "Deleting node"
+ return self.ps.deleteNode(self.pshost, self.node % self.nodenum)
+
+ def test_createWithConfig(self):
+ "Creating node with config"
+ if self.defaultconfig is None:
+ self.lasterror = "No Avail Config"
+ return False
+ return self.ps.create_node(self.pshost, self.node % self.leafnode, self.defaultconfig)
+
+ def test_reconfigureNode(self):
+ "Retrieving node config and reconfiguring"
+ nconfig = self.ps.getNodeConfig(self.pshost, self.node % self.leafnode)
+ if nconfig == False:
+ return False
+ return self.ps.setNodeConfig(self.pshost, self.node % self.leafnode, nconfig)
+
+ def test_subscribeToNode(self):
+ "Subscribing to node"
+ return self.ps.subscribe(self.pshost, self.node % self.leafnode)
+
+ def test_addItem(self):
+ "Adding item, waiting for notification"
+ item = ET.Element('test')
+ result = self.ps.setItem(self.pshost, self.node % self.leafnode, (('test_node1', item),))
+ if result == False:
+ return False
+ try:
+ event = self.events.get(True, 10)
+ except Queue.Empty:
+ return False
+ if event == 'test_node1':
+ return True
+ return False
+
+ def test_updateItem(self):
+ "Updating item, waiting for notification"
+ item = ET.Element('test')
+ item.attrib['crap'] = 'yup, right here'
+ result = self.ps.setItem(self.pshost, self.node % self.leafnode, (('test_node1', item),))
+ if result == False:
+ return False
+ try:
+ event = self.events.get(True, 10)
+ except Queue.Empty:
+ return False
+ if event == 'test_node1':
+ return True
+ return False
+
+ def test_deleteItem(self):
+ "Deleting item, waiting for notification"
+ result = self.ps.deleteItem(self.pshost, self.node % self.leafnode, 'test_node1')
+ if result == False:
+ return False
+ try:
+ event = self.events.get(True, 10)
+ except Queue.Empty:
+ self.lasterror = "No Notification"
+ return False
+ if event == 'test_node1':
+ return True
+ return False
+
+ def test_unsubscribeNode(self):
+ "Unsubscribing from node"
+ return self.ps.unsubscribe(self.pshost, self.node % self.leafnode)
+
+ def test_createCollection(self):
+ "Creating collection node"
+ return self.ps.create_node(self.pshost, self.node % self.collectnode, self.defaultconfig, True)
+
+ def test_subscribeCollection(self):
+ "Subscribing to collection node"
+ return self.ps.subscribe(self.pshost, self.node % self.collectnode)
+
+ def test_addNodeCollection(self):
+ "Assigning node to collection, waiting for notification"
+ config = self.ps.getNodeConfig(self.pshost, self.node % self.leafnode)
+ if not config or config is None:
+ self.lasterror = "Config Error"
+ return False
+ try:
+ config.field['pubsub#collection'].setValue(self.node % self.collectnode)
+ except KeyError:
+ self.sprint("...Missing Field...", False, "red")
+ config.addField('pubsub#collection', value=self.node % self.collectnode)
+ if not self.ps.setNodeConfig(self.pshost, self.node % self.leafnode, config):
+ return False
+ try:
+ event = self.events.get(True, 10)
+ except Queue.Empty:
+ self.lasterror = "No Notification"
+ return False
+ if event == self.node % self.leafnode:
+ return True
+ return False
+
+ def test_deleteNodeCollection(self):
+ "Removing node assignment to collection, waiting for notification"
+ config = self.ps.getNodeConfig(self.pshost, self.node % self.leafnode)
+ if not config or config is None:
+ self.lasterror = "Config Error"
+ return False
+ try:
+ config.field['pubsub#collection'].delValue(self.node % self.collectnode)
+ except KeyError:
+ self.sprint("...Missing Field...", False, "red")
+ config.addField('pubsub#collection', value='')
+ if not self.ps.setNodeConfig(self.pshost, self.node % self.leafnode, config):
+ return False
+ try:
+ event = self.events.get(True, 10)
+ except Queue.Empty:
+ self.lasterror = "No Notification"
+ return False
+ if event == self.node % self.leafnode:
+ return True
+ return False
+
+ def test_addCollectionNode(self):
+ "Assigning node from collection, waiting for notification"
+ config = self.ps.getNodeConfig(self.pshost, self.node % self.collectnode)
+ if not config or config is None:
+ self.lasterror = "Config Error"
+ return False
+ try:
+ config.field['pubsub#children'].setValue(self.node % self.leafnode)
+ except KeyError:
+ self.sprint("...Missing Field...", False, "red")
+ config.addField('pubsub#children', value=self.node % self.leafnode)
+ if not self.ps.setNodeConfig(self.pshost, self.node % self.collectnode, config):
+ return False
+ try:
+ event = self.events.get(True, 10)
+ except Queue.Empty:
+ self.lasterror = "No Notification"
+ return False
+ if event == self.node % self.leafnode:
+ return True
+ return False
+
+ def test_deleteCollectionNode(self):
+ "Removing node from collection, waiting for notification"
+ config = self.ps.getNodeConfig(self.pshost, self.node % self.collectnode)
+ if not config or config is None:
+ self.lasterror = "Config Error"
+ return False
+ try:
+ config.field['pubsub#children'].delValue(self.node % self.leafnode)
+ except KeyError:
+ self.sprint("...Missing Field...", False, "red")
+ config.addField('pubsub#children', value='')
+ if not self.ps.setNodeConfig(self.pshost, self.node % self.collectnode, config):
+ return False
+ try:
+ event = self.events.get(True, 10)
+ except Queue.Empty:
+ self.lasterror = "No Notification"
+ return False
+ if event == self.node % self.leafnode:
+ return True
+ return False
+
+ def test_unsubscribeNodeCollection(self):
+ "Unsubscribing from collection"
+ return self.ps.unsubscribe(self.pshost, self.node % self.collectnode)
+
+ def test_deleteCollection(self):
+ "Deleting collection"
+ return self.ps.deleteNode(self.pshost, self.node % self.collectnode)
+
+if __name__ == '__main__':
+ #parse command line arguements
+ optp = OptionParser()
+ optp.add_option('-q','--quiet', help='set logging to ERROR', action='store_const', dest='loglevel', const=logging.ERROR, default=logging.INFO)
+ optp.add_option('-d','--debug', help='set logging to DEBUG', action='store_const', dest='loglevel', const=logging.DEBUG, default=logging.INFO)
+ optp.add_option('-v','--verbose', help='set logging to COMM', action='store_const', dest='loglevel', const=5, default=logging.INFO)
+ optp.add_option("-c","--config", dest="configfile", default="config.xml", help="set config file to use")
+ optp.add_option("-n","--nodenum", dest="nodenum", default="1", help="set node number to use")
+ optp.add_option("-p","--pubsub", dest="pubsub", default="1", help="set pubsub host to use")
+ opts,args = optp.parse_args()
+
+ logging.basicConfig(level=opts.loglevel, format='%(levelname)-8s %(message)s')
+
+ #load xml config
+ logging.info("Loading config file: %s" , opts.configfile)
+ config = ET.parse(os.path.expanduser(opts.configfile)).find('auth')
+
+ #init
+ logging.info("Logging in as %s" , config.attrib['jid'])
+
+
+ plugin_config = {}
+ plugin_config['xep_0092'] = {'name': 'SleekXMPP Example', 'version': '0.1-dev'}
+ plugin_config['xep_0199'] = {'keepalive': True, 'timeout': 30, 'frequency': 300}
+
+ con = testps(config.attrib['jid'], config.attrib['pass'], plugin_config=plugin_config, plugin_whitelist=[], nodenum=opts.nodenum, pshost=opts.pubsub)
+ if not config.get('server', None):
+ # we don't know the server, but the lib can probably figure it out
+ con.connect()
+ else:
+ con.connect((config.attrib['server'], 5222))
+ con.process(threaded=False)
+ print("")
diff --git a/docs/_static/haiku.css b/docs/_static/haiku.css
index 41e0eb45..3d8ee6a7 100644
--- a/docs/_static/haiku.css
+++ b/docs/_static/haiku.css
@@ -409,7 +409,6 @@ div.viewcode-block:target {
padding: 0 12px;
}
-
#from_andyet {
-webkit-box-shadow: #CCC 0px 0px 3px;
background: rgba(255, 255, 255, 1);
diff --git a/docs/_static/header.png b/docs/_static/header.png
index 19310618..2aaa53a1 100644
--- a/docs/_static/header.png
+++ b/docs/_static/header.png
Binary files differ
diff --git a/docs/index.rst b/docs/index.rst
index 9ccb1b27..fe7df7f9 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -59,7 +59,6 @@ SleekXMPP's design goals and philosphy are:
sensible defaults and appropriate abstractions. XML can be ugly to work
with, but it doesn't have to be that way.
-
Here's your first SleekXMPP Bot:
--------------------------------
diff --git a/examples/muc.py b/examples/muc.py
index 7b93da16..cfb99c23 100755
--- a/examples/muc.py
+++ b/examples/muc.py
@@ -76,8 +76,13 @@ class MUCBot(sleekxmpp.ClientXMPP):
event does not provide any additional
data.
"""
+<<<<<<< HEAD
+ self.getRoster()
+ self.sendPresence()
+=======
self.get_roster()
self.send_presence()
+>>>>>>> docs
self.plugin['xep_0045'].joinMUC(self.room,
self.nick,
# If a room password is needed, use:
diff --git a/ez_setup.py b/ez_setup.py
new file mode 100644
index 00000000..4b983b1e
--- /dev/null
+++ b/ez_setup.py
@@ -0,0 +1,233 @@
+#!python
+"""Bootstrap setuptools installation
+
+If you want to use setuptools in your package's setup.py, just include this
+file in the same directory with it, and add this to the top of your setup.py::
+
+ from ez_setup import use_setuptools
+ use_setuptools()
+
+If you want to require a specific version of setuptools, set a download
+mirror, or use an alternate download directory, you can do so by supplying
+the appropriate options to ``use_setuptools()``.
+
+This file can also be run as a script to install or upgrade setuptools.
+"""
+import sys
+DEFAULT_VERSION = "0.6c7"
+DEFAULT_URL = "http://pypi.python.org/packages/%s/s/setuptools/" % sys.version[:3]
+
+md5_data = {
+ 'setuptools-0.6b1-py2.3.egg': '8822caf901250d848b996b7f25c6e6ca',
+ 'setuptools-0.6b1-py2.4.egg': 'b79a8a403e4502fbb85ee3f1941735cb',
+ 'setuptools-0.6b2-py2.3.egg': '5657759d8a6d8fc44070a9d07272d99b',
+ 'setuptools-0.6b2-py2.4.egg': '4996a8d169d2be661fa32a6e52e4f82a',
+ 'setuptools-0.6b3-py2.3.egg': 'bb31c0fc7399a63579975cad9f5a0618',
+ 'setuptools-0.6b3-py2.4.egg': '38a8c6b3d6ecd22247f179f7da669fac',
+ 'setuptools-0.6b4-py2.3.egg': '62045a24ed4e1ebc77fe039aa4e6f7e5',
+ 'setuptools-0.6b4-py2.4.egg': '4cb2a185d228dacffb2d17f103b3b1c4',
+ 'setuptools-0.6c1-py2.3.egg': 'b3f2b5539d65cb7f74ad79127f1a908c',
+ 'setuptools-0.6c1-py2.4.egg': 'b45adeda0667d2d2ffe14009364f2a4b',
+ 'setuptools-0.6c2-py2.3.egg': 'f0064bf6aa2b7d0f3ba0b43f20817c27',
+ 'setuptools-0.6c2-py2.4.egg': '616192eec35f47e8ea16cd6a122b7277',
+ 'setuptools-0.6c3-py2.3.egg': 'f181fa125dfe85a259c9cd6f1d7b78fa',
+ 'setuptools-0.6c3-py2.4.egg': 'e0ed74682c998bfb73bf803a50e7b71e',
+ 'setuptools-0.6c3-py2.5.egg': 'abef16fdd61955514841c7c6bd98965e',
+ 'setuptools-0.6c4-py2.3.egg': 'b0b9131acab32022bfac7f44c5d7971f',
+ 'setuptools-0.6c4-py2.4.egg': '2a1f9656d4fbf3c97bf946c0a124e6e2',
+ 'setuptools-0.6c4-py2.5.egg': '8f5a052e32cdb9c72bcf4b5526f28afc',
+ 'setuptools-0.6c5-py2.3.egg': 'ee9fd80965da04f2f3e6b3576e9d8167',
+ 'setuptools-0.6c5-py2.4.egg': 'afe2adf1c01701ee841761f5bcd8aa64',
+ 'setuptools-0.6c5-py2.5.egg': 'a8d3f61494ccaa8714dfed37bccd3d5d',
+ 'setuptools-0.6c6-py2.3.egg': '35686b78116a668847237b69d549ec20',
+ 'setuptools-0.6c6-py2.4.egg': '3c56af57be3225019260a644430065ab',
+ 'setuptools-0.6c6-py2.5.egg': 'b2f8a7520709a5b34f80946de5f02f53',
+}
+
+import sys, os
+
+def _validate_md5(egg_name, data):
+ if egg_name in md5_data:
+ from md5 import md5
+ digest = md5(data).hexdigest()
+ if digest != md5_data[egg_name]:
+ print >>sys.stderr, (
+ "md5 validation of %s failed! (Possible download problem?)"
+ % egg_name
+ )
+ sys.exit(2)
+ return data
+
+
+def use_setuptools(
+ version=DEFAULT_VERSION, download_base=DEFAULT_URL, to_dir=os.curdir, min_version=None,
+ download_delay=15
+):
+ """Automatically find/download setuptools and make it available on sys.path
+
+ `version` should be a valid setuptools version number that is available
+ as an egg for download under the `download_base` URL (which should end with
+ a '/'). `to_dir` is the directory where setuptools will be downloaded, if
+ it is not already available. If `download_delay` is specified, it should
+ be the number of seconds that will be paused before initiating a download,
+ should one be required. If an older version of setuptools is installed,
+ this routine will print a message to ``sys.stderr`` and raise SystemExit in
+ an attempt to abort the calling script.
+ """
+ try:
+ import setuptools
+ if setuptools.__version__ == '0.0.1':
+ print >>sys.stderr, (
+ "You have an obsolete version of setuptools installed. Please\n"
+ "remove it from your system entirely before rerunning this script."
+ )
+ sys.exit(2)
+ except ImportError:
+ egg = download_setuptools(version, download_base, to_dir, download_delay)
+ sys.path.insert(0, egg)
+ import setuptools; setuptools.bootstrap_install_from = egg
+
+ import pkg_resources
+ try:
+ if not min_version:
+ min_version = version
+ pkg_resources.require("setuptools>="+min_version)
+
+ except pkg_resources.VersionConflict, e:
+ # XXX could we install in a subprocess here?
+ print >>sys.stderr, (
+ "The required version of setuptools (>=%s) is not available, and\n"
+ "can't be installed while this script is running. Please install\n"
+ " a more recent version first.\n\n(Currently using %r)"
+ ) % (min_version, e.args[0])
+ sys.exit(2)
+
+def download_setuptools(
+ version=DEFAULT_VERSION, download_base=DEFAULT_URL, to_dir=os.curdir,
+ delay = 15
+):
+ """Download setuptools from a specified location and return its filename
+
+ `version` should be a valid setuptools version number that is available
+ as an egg for download under the `download_base` URL (which should end
+ with a '/'). `to_dir` is the directory where the egg will be downloaded.
+ `delay` is the number of seconds to pause before an actual download attempt.
+ """
+ import urllib2, shutil
+ egg_name = "setuptools-%s-py%s.egg" % (version,sys.version[:3])
+ url = download_base + egg_name
+ saveto = os.path.join(to_dir, egg_name)
+ src = dst = None
+ if not os.path.exists(saveto): # Avoid repeated downloads
+ try:
+ from distutils import log
+ if delay:
+ log.warn("""
+---------------------------------------------------------------------------
+This script requires setuptools version %s to run (even to display
+help). I will attempt to download it for you (from
+%s), but
+you may need to enable firewall access for this script first.
+I will start the download in %d seconds.
+
+(Note: if this machine does not have network access, please obtain the file
+
+ %s
+
+and place it in this directory before rerunning this script.)
+---------------------------------------------------------------------------""",
+ version, download_base, delay, url
+ ); from time import sleep; sleep(delay)
+ log.warn("Downloading %s", url)
+ src = urllib2.urlopen(url)
+ # Read/write all in one block, so we don't create a corrupt file
+ # if the download is interrupted.
+ data = _validate_md5(egg_name, src.read())
+ dst = open(saveto,"wb"); dst.write(data)
+ finally:
+ if src: src.close()
+ if dst: dst.close()
+ return os.path.realpath(saveto)
+
+def main(argv, version=DEFAULT_VERSION):
+ """Install or upgrade setuptools and EasyInstall"""
+
+ try:
+ import setuptools
+ except ImportError:
+ egg = None
+ try:
+ egg = download_setuptools(version, delay=0)
+ sys.path.insert(0,egg)
+ from setuptools.command.easy_install import main
+ return main(list(argv)+[egg]) # we're done here
+ finally:
+ if egg and os.path.exists(egg):
+ os.unlink(egg)
+ else:
+ if setuptools.__version__ == '0.0.1':
+ # tell the user to uninstall obsolete version
+ use_setuptools(version)
+
+ req = "setuptools>="+version
+ import pkg_resources
+ try:
+ pkg_resources.require(req)
+ except pkg_resources.VersionConflict:
+ try:
+ from setuptools.command.easy_install import main
+ except ImportError:
+ from easy_install import main
+ main(list(argv)+[download_setuptools(delay=0)])
+ sys.exit(0) # try to force an exit
+ else:
+ if argv:
+ from setuptools.command.easy_install import main
+ main(argv)
+ else:
+ print "Setuptools version",version,"or greater has been installed."
+ print '(Run "ez_setup.py -U setuptools" to reinstall or upgrade.)'
+
+
+
+def update_md5(filenames):
+ """Update our built-in md5 registry"""
+
+ import re
+ from md5 import md5
+
+ for name in filenames:
+ base = os.path.basename(name)
+ f = open(name,'rb')
+ md5_data[base] = md5(f.read()).hexdigest()
+ f.close()
+
+ data = [" %r: %r,\n" % it for it in md5_data.items()]
+ data.sort()
+ repl = "".join(data)
+
+ import inspect
+ srcfile = inspect.getsourcefile(sys.modules[__name__])
+ f = open(srcfile, 'rb'); src = f.read(); f.close()
+
+ match = re.search("\nmd5_data = {\n([^}]+)}", src)
+ if not match:
+ print >>sys.stderr, "Internal error!"
+ sys.exit(2)
+
+ src = src[:match.start(1)] + repl + src[match.end(1):]
+ f = open(srcfile,'w')
+ f.write(src)
+ f.close()
+
+
+if __name__=='__main__':
+ if len(sys.argv)>2 and sys.argv[1]=='--md5update':
+ update_md5(sys.argv[2:])
+ else:
+ main(sys.argv[1:])
+
+
+
+
+
diff --git a/setup.py b/setup.py
new file mode 100755
index 00000000..b86dc1f7
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,103 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2007-2011 Nathanael C. Fritz
+# All Rights Reserved
+#
+# This software is licensed as described in the README.rst and LICENSE
+# file, which you should have received as part of this distribution.
+
+import sys
+import codecs
+try:
+ from setuptools import setup, Command
+except ImportError:
+ from distutils.core import setup, Command
+# from ez_setup import use_setuptools
+
+from testall import TestCommand
+from sleekxmpp.version import __version__
+# if 'cygwin' in sys.platform.lower():
+# min_version = '0.6c6'
+# else:
+# min_version = '0.6a9'
+#
+# try:
+# use_setuptools(min_version=min_version)
+# except TypeError:
+# # locally installed ez_setup won't have min_version
+# use_setuptools()
+#
+# from setuptools import setup, find_packages, Extension, Feature
+
+VERSION = __version__
+DESCRIPTION = 'SleekXMPP is an elegant Python library for XMPP (aka Jabber, Google Talk, etc).'
+with codecs.open('README.rst', 'r', encoding='UTF-8') as readme:
+ LONG_DESCRIPTION = ''.join(readme)
+
+CLASSIFIERS = [ 'Intended Audience :: Developers',
+ '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',
+ ]
+
+packages = [ 'sleekxmpp',
+ 'sleekxmpp/stanza',
+ 'sleekxmpp/test',
+ 'sleekxmpp/roster',
+ 'sleekxmpp/xmlstream',
+ 'sleekxmpp/xmlstream/matcher',
+ 'sleekxmpp/xmlstream/handler',
+ 'sleekxmpp/plugins',
+ 'sleekxmpp/plugins/xep_0004',
+ 'sleekxmpp/plugins/xep_0004/stanza',
+ 'sleekxmpp/plugins/xep_0009',
+ 'sleekxmpp/plugins/xep_0009/stanza',
+ 'sleekxmpp/plugins/xep_0030',
+ '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_0115',
+ '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',
+ ]
+
+setup(
+ name = "sleekxmpp",
+ version = VERSION,
+ description = DESCRIPTION,
+ long_description = LONG_DESCRIPTION,
+ author = 'Nathanael Fritz',
+ author_email = 'fritzy [at] netflint.net',
+ url = 'http://github.com/fritzy/SleekXMPP',
+ license = 'MIT',
+ platforms = [ 'any' ],
+ packages = packages,
+ requires = [ 'dnspython' ],
+ classifiers = CLASSIFIERS,
+ cmdclass = {'test': TestCommand}
+)
diff --git a/sleekxmpp/__init__.py b/sleekxmpp/__init__.py
new file mode 100644
index 00000000..a1f1c0f1
--- /dev/null
+++ b/sleekxmpp/__init__.py
@@ -0,0 +1,18 @@
+"""
+ 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.basexmpp import BaseXMPP
+from sleekxmpp.clientxmpp import ClientXMPP
+from sleekxmpp.componentxmpp import ComponentXMPP
+from sleekxmpp.stanza import Message, Presence, Iq
+from sleekxmpp.xmlstream.handler import *
+from sleekxmpp.xmlstream import XMLStream, RestartStream
+from sleekxmpp.xmlstream.matcher import *
+from sleekxmpp.xmlstream.stanzabase import StanzaBase, ET
+
+from sleekxmpp.version import __version__, __version_info__
diff --git a/sleekxmpp/basexmpp.py b/sleekxmpp/basexmpp.py
new file mode 100644
index 00000000..e4fd03a1
--- /dev/null
+++ b/sleekxmpp/basexmpp.py
@@ -0,0 +1,792 @@
+# -*- coding: utf-8 -*-
+"""
+ sleekxmpp.basexmpp
+ ~~~~~~~~~~~~~~~~~~
+
+ This module provides the common XMPP functionality
+ for both clients and components.
+
+ Part of SleekXMPP: The Sleek XMPP Library
+
+ :copyright: (c) 2011 Nathanael C. Fritz
+ :license: MIT, see LICENSE for more details
+"""
+
+from __future__ import with_statement, unicode_literals
+
+import sys
+import copy
+import logging
+
+import sleekxmpp
+from sleekxmpp import plugins, roster
+from sleekxmpp.exceptions import IqError, IqTimeout
+
+from sleekxmpp.stanza import Message, Presence, Iq, Error, StreamError
+from sleekxmpp.stanza.roster import Roster
+from sleekxmpp.stanza.nick import Nick
+from sleekxmpp.stanza.htmlim import HTMLIM
+
+from sleekxmpp.xmlstream import XMLStream, JID, tostring
+from sleekxmpp.xmlstream import ET, register_stanza_plugin
+from sleekxmpp.xmlstream.matcher import *
+from sleekxmpp.xmlstream.handler import *
+
+
+log = logging.getLogger(__name__)
+
+# In order to make sure that Unicode is handled properly
+# in Python 2.x, reset the default encoding.
+if sys.version_info < (3, 0):
+ reload(sys)
+ sys.setdefaultencoding('utf8')
+
+
+class BaseXMPP(XMLStream):
+
+ """
+ The BaseXMPP class adapts the generic XMLStream class for use
+ with XMPP. It also provides a plugin mechanism to easily extend
+ and add support for new XMPP features.
+
+ :param default_ns: Ensure that the correct default XML namespace
+ is used during initialization.
+ """
+
+ def __init__(self, jid='', default_ns='jabber:client'):
+ XMLStream.__init__(self)
+
+ self.default_ns = default_ns
+ self.stream_ns = 'http://etherx.jabber.org/streams'
+ self.namespace_map[self.stream_ns] = 'stream'
+
+ #: An identifier for the stream as given by the server.
+ self.stream_id = None
+
+ #: The JabberID (JID) used by this connection.
+ self.boundjid = JID(jid)
+
+ #: A dictionary mapping plugin names to plugins.
+ self.plugin = {}
+
+ #: Configuration options for whitelisted plugins.
+ #: If a plugin is registered without any configuration,
+ #: and there is an entry here, it will be used.
+ self.plugin_config = {}
+
+ #: A list of plugins that will be loaded if
+ #: :meth:`register_plugins` is called.
+ self.plugin_whitelist = []
+
+ #: The main roster object. This roster supports multiple
+ #: owner JIDs, as in the case for components. For clients
+ #: which only have a single JID, see :attr:`client_roster`.
+ self.roster = roster.Roster(self)
+ self.roster.add(self.boundjid.bare)
+
+ #: The single roster for the bound JID. This is the
+ #: equivalent of::
+ #:
+ #: self.roster[self.boundjid.bare]
+ self.client_roster = self.roster[self.boundjid.bare]
+
+ #: The distinction between clients and components can be
+ #: important, primarily for choosing how to handle the
+ #: ``'to'`` and ``'from'`` JIDs of stanzas.
+ self.is_component = False
+
+ #: Flag indicating that the initial presence broadcast has
+ #: been sent. Until this happens, some servers may not
+ #: behave as expected when sending stanzas.
+ self.sentpresence = False
+
+ #: A reference to :mod:`sleekxmpp.stanza` to make accessing
+ #: stanza classes easier.
+ self.stanza = sleekxmpp.stanza
+
+ self.register_handler(
+ Callback('IM',
+ MatchXPath('{%s}message/{%s}body' % (self.default_ns,
+ self.default_ns)),
+ self._handle_message))
+ self.register_handler(
+ Callback('Presence',
+ MatchXPath("{%s}presence" % self.default_ns),
+ self._handle_presence))
+ self.register_handler(
+ Callback('Stream Error',
+ MatchXPath("{%s}error" % self.stream_ns),
+ self._handle_stream_error))
+
+ self.add_event_handler('disconnected',
+ self._handle_disconnected)
+ self.add_event_handler('presence_available',
+ self._handle_available)
+ self.add_event_handler('presence_dnd',
+ self._handle_available)
+ self.add_event_handler('presence_xa',
+ self._handle_available)
+ self.add_event_handler('presence_chat',
+ self._handle_available)
+ self.add_event_handler('presence_away',
+ self._handle_available)
+ self.add_event_handler('presence_unavailable',
+ self._handle_unavailable)
+ self.add_event_handler('presence_subscribe',
+ self._handle_subscribe)
+ self.add_event_handler('presence_subscribed',
+ self._handle_subscribed)
+ self.add_event_handler('presence_unsubscribe',
+ self._handle_unsubscribe)
+ self.add_event_handler('presence_unsubscribed',
+ self._handle_unsubscribed)
+ self.add_event_handler('roster_subscription_request',
+ self._handle_new_subscription)
+
+ # Set up the XML stream with XMPP's root stanzas.
+ self.register_stanza(Message)
+ self.register_stanza(Iq)
+ self.register_stanza(Presence)
+ self.register_stanza(StreamError)
+
+ # Initialize a few default stanza plugins.
+ register_stanza_plugin(Iq, Roster)
+ register_stanza_plugin(Message, Nick)
+ register_stanza_plugin(Message, HTMLIM)
+
+ def start_stream_handler(self, xml):
+ """Save the stream ID once the streams have been established.
+
+ :param xml: The incoming stream's root element.
+ """
+ self.stream_id = xml.get('id', '')
+
+ def process(self, *args, **kwargs):
+ """Initialize plugins and begin processing the XML stream.
+
+ The number of threads used for processing stream events is determined
+ by :data:`HANDLER_THREADS`.
+
+ :param bool block: If ``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``.
+ :param bool threaded: **DEPRECATED**
+ If ``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``. This does **not** mean that no
+ threads are used at all if ``threaded=False``.
+
+ Regardless of these threading options, these threads will
+ always exist:
+
+ - The event queue processor
+ - The send queue processor
+ - The scheduler
+ """
+ for name in self.plugin:
+ if not self.plugin[name].post_inited:
+ self.plugin[name].post_init()
+ return XMLStream.process(self, *args, **kwargs)
+
+ def register_plugin(self, plugin, pconfig={}, module=None):
+ """Register and configure a plugin for use in this stream.
+
+ :param plugin: The name of the plugin class. Plugin names must
+ be unique.
+ :param pconfig: A dictionary of configuration data for the plugin.
+ Defaults to an empty dictionary.
+ :param module: Optional refence to the module containing the plugin
+ class if using custom plugins.
+ """
+ try:
+ # Import the given module that contains the plugin.
+ if not module:
+ 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/RFC implementing plugins have some extra logging info.
+ spec = '(CUSTOM) %s'
+ 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 = (spec, self.plugin[plugin].description)
+ log.debug("Loaded Plugin %s %s" % desc)
+ except:
+ log.exception("Unable to load plugin: %s", plugin)
+
+ def register_plugins(self):
+ """Register and initialize all built-in plugins.
+
+ Optionally, the list of plugins loaded may be limited to those
+ contained in :attr:`plugin_whitelist`.
+
+ Plugin configurations stored in :attr:`plugin_config` will be used.
+ """
+ if self.plugin_whitelist:
+ plugin_list = self.plugin_whitelist
+ else:
+ plugin_list = plugins.__all__
+
+ for plugin in plugin_list:
+ if plugin in plugins.__all__:
+ self.register_plugin(plugin,
+ self.plugin_config.get(plugin, {}))
+ else:
+ raise NameError("Plugin %s not in plugins.__all__." % plugin)
+
+ # Resolve plugin inter-dependencies.
+ for plugin in self.plugin:
+ self.plugin[plugin].post_init()
+
+ def __getitem__(self, key):
+ """Return a plugin given its name, if it has been registered."""
+ if key in self.plugin:
+ return self.plugin[key]
+ else:
+ log.warning("Plugin '%s' is not loaded.", key)
+ return False
+
+ def get(self, key, default):
+ """Return a plugin given its name, if it has been registered."""
+ return self.plugin.get(key, default)
+
+ def Message(self, *args, **kwargs):
+ """Create a Message stanza associated with this stream."""
+ return Message(self, *args, **kwargs)
+
+ def Iq(self, *args, **kwargs):
+ """Create an Iq stanza associated with this stream."""
+ return Iq(self, *args, **kwargs)
+
+ def Presence(self, *args, **kwargs):
+ """Create a Presence stanza associated with this stream."""
+ return Presence(self, *args, **kwargs)
+
+ def make_iq(self, id=0, ifrom=None, ito=None, itype=None, iquery=None):
+ """Create a new Iq stanza with a given Id and from JID.
+
+ :param id: An ideally unique ID value for this stanza thread.
+ Defaults to 0.
+ :param ifrom: The from :class:`~sleekxmpp.xmlstream.jid.JID`
+ to use for this stanza.
+ :param ito: The destination :class:`~sleekxmpp.xmlstream.jid.JID`
+ for this stanza.
+ :param itype: The :class:`~sleekxmpp.stanza.iq.Iq`'s type,
+ one of: ``'get'``, ``'set'``, ``'result'``,
+ or ``'error'``.
+ :param iquery: Optional namespace for adding a query element.
+ """
+ iq = self.Iq()
+ iq['id'] = str(id)
+ iq['to'] = ito
+ iq['from'] = ifrom
+ iq['type'] = itype
+ iq['query'] = iquery
+ return iq
+
+ def make_iq_get(self, queryxmlns=None, ito=None, ifrom=None, iq=None):
+ """Create an :class:`~sleekxmpp.stanza.iq.Iq` stanza of type ``'get'``.
+
+ Optionally, a query element may be added.
+
+ :param queryxmlns: The namespace of the query to use.
+ :param ito: The destination :class:`~sleekxmpp.xmlstream.jid.JID`
+ for this stanza.
+ :param ifrom: The ``'from'`` :class:`~sleekxmpp.xmlstream.jid.JID`
+ to use for this stanza.
+ :param iq: Optionally use an existing stanza instead
+ of generating a new one.
+ """
+ if not iq:
+ iq = self.Iq()
+ iq['type'] = 'get'
+ iq['query'] = queryxmlns
+ if ito:
+ iq['to'] = ito
+ if ifrom:
+ iq['from'] = ifrom
+ return iq
+
+ def make_iq_result(self, id=None, ito=None, ifrom=None, iq=None):
+ """
+ Create an :class:`~sleekxmpp.stanza.iq.Iq` stanza of type
+ ``'result'`` with the given ID value.
+
+ :param id: An ideally unique ID value. May use :meth:`new_id()`.
+ :param ito: The destination :class:`~sleekxmpp.xmlstream.jid.JID`
+ for this stanza.
+ :param ifrom: The ``'from'`` :class:`~sleekxmpp.xmlstream.jid.JID`
+ to use for this stanza.
+ :param iq: Optionally use an existing stanza instead
+ of generating a new one.
+ """
+ if not iq:
+ iq = self.Iq()
+ if id is None:
+ id = self.new_id()
+ iq['id'] = id
+ iq['type'] = 'result'
+ if ito:
+ iq['to'] = ito
+ if ifrom:
+ iq['from'] = ifrom
+ return iq
+
+ def make_iq_set(self, sub=None, ito=None, ifrom=None, iq=None):
+ """
+ Create an :class:`~sleekxmpp.stanza.iq.Iq` stanza of type ``'set'``.
+
+ Optionally, a substanza may be given to use as the
+ stanza's payload.
+
+ :param sub: Either an
+ :class:`~sleekxmpp.xmlstream.stanzabase.ElementBase`
+ stanza object or an
+ :class:`~xml.etree.ElementTree.Element` XML object
+ to use as the :class:`~sleekxmpp.stanza.iq.Iq`'s payload.
+ :param ito: The destination :class:`~sleekxmpp.xmlstream.jid.JID`
+ for this stanza.
+ :param ifrom: The ``'from'`` :class:`~sleekxmpp.xmlstream.jid.JID`
+ to use for this stanza.
+ :param iq: Optionally use an existing stanza instead
+ of generating a new one.
+ """
+ if not iq:
+ iq = self.Iq()
+ iq['type'] = 'set'
+ if sub != None:
+ iq.append(sub)
+ if ito:
+ iq['to'] = ito
+ if ifrom:
+ iq['from'] = ifrom
+ return iq
+
+ def make_iq_error(self, id, type='cancel',
+ condition='feature-not-implemented',
+ text=None, ito=None, ifrom=None, iq=None):
+ """
+ Create an :class:`~sleekxmpp.stanza.iq.Iq` stanza of type ``'error'``.
+
+ :param id: An ideally unique ID value. May use :meth:`new_id()`.
+ :param type: The type of the error, such as ``'cancel'`` or
+ ``'modify'``. Defaults to ``'cancel'``.
+ :param condition: The error condition. Defaults to
+ ``'feature-not-implemented'``.
+ :param text: A message describing the cause of the error.
+ :param ito: The destination :class:`~sleekxmpp.xmlstream.jid.JID`
+ for this stanza.
+ :param ifrom: The ``'from'`` :class:`~sleekxmpp.xmlstream.jid.JID`
+ to use for this stanza.
+ :param iq: Optionally use an existing stanza instead
+ of generating a new one.
+ """
+ if not iq:
+ iq = self.Iq()
+ iq['id'] = id
+ iq['error']['type'] = type
+ iq['error']['condition'] = condition
+ iq['error']['text'] = text
+ if ito:
+ iq['to'] = ito
+ if ifrom:
+ iq['from'] = ifrom
+ return iq
+
+ def make_iq_query(self, iq=None, xmlns='', ito=None, ifrom=None):
+ """
+ Create or modify an :class:`~sleekxmpp.stanza.iq.Iq` stanza
+ to use the given query namespace.
+
+ :param iq: Optionally use an existing stanza instead
+ of generating a new one.
+ :param xmlns: The query's namespace.
+ :param ito: The destination :class:`~sleekxmpp.xmlstream.jid.JID`
+ for this stanza.
+ :param ifrom: The ``'from'`` :class:`~sleekxmpp.xmlstream.jid.JID`
+ to use for this stanza.
+ """
+ if not iq:
+ iq = self.Iq()
+ iq['query'] = xmlns
+ if ito:
+ iq['to'] = ito
+ if ifrom:
+ iq['from'] = ifrom
+ return iq
+
+ def make_query_roster(self, iq=None):
+ """Create a roster query element.
+
+ :param iq: Optionally use an existing stanza instead
+ of generating a new one.
+ """
+ if iq:
+ iq['query'] = 'jabber:iq:roster'
+ return ET.Element("{jabber:iq:roster}query")
+
+ def make_message(self, mto, mbody=None, msubject=None, mtype=None,
+ mhtml=None, mfrom=None, mnick=None):
+ """
+ Create and initialize a new
+ :class:`~sleekxmpp.stanza.message.Message` stanza.
+
+ :param mto: The recipient of the message.
+ :param mbody: The main contents of the message.
+ :param msubject: Optional subject for the message.
+ :param mtype: The message's type, such as ``'chat'`` or
+ ``'groupchat'``.
+ :param mhtml: Optional HTML body content in the form of a string.
+ :param mfrom: The sender of the message. if sending from a client,
+ be aware that some servers require that the full JID
+ of the sender be used.
+ :param mnick: Optional nickname of the sender.
+ """
+ message = self.Message(sto=mto, stype=mtype, sfrom=mfrom)
+ message['body'] = mbody
+ message['subject'] = msubject
+ if mnick is not None:
+ message['nick'] = mnick
+ if mhtml is not None:
+ message['html']['body'] = mhtml
+ return message
+
+ def make_presence(self, pshow=None, pstatus=None, ppriority=None,
+ pto=None, ptype=None, pfrom=None, pnick=None):
+ """
+ Create and initialize a new
+ :class:`~sleekxmpp.stanza.presence.Presence` stanza.
+
+ :param pshow: The presence's show value.
+ :param pstatus: The presence's status message.
+ :param ppriority: This connection's priority.
+ :param pto: The recipient of a directed presence.
+ :param ptype: The type of presence, such as ``'subscribe'``.
+ :param pfrom: The sender of the presence.
+ :param pnick: Optional nickname of the presence's sender.
+ """
+ presence = self.Presence(stype=ptype, sfrom=pfrom, sto=pto)
+ if pshow is not None:
+ presence['type'] = pshow
+ if pfrom is None and self.is_component:
+ presence['from'] = self.boundjid.full
+ presence['priority'] = ppriority
+ presence['status'] = pstatus
+ presence['nick'] = pnick
+ return presence
+
+ def send_message(self, mto, mbody, msubject=None, mtype=None,
+ mhtml=None, mfrom=None, mnick=None):
+ """
+ Create, initialize, and send a new
+ :class:`~sleekxmpp.stanza.message.Message` stanza.
+
+ :param mto: The recipient of the message.
+ :param mbody: The main contents of the message.
+ :param msubject: Optional subject for the message.
+ :param mtype: The message's type, such as ``'chat'`` or
+ ``'groupchat'``.
+ :param mhtml: Optional HTML body content in the form of a string.
+ :param mfrom: The sender of the message. if sending from a client,
+ be aware that some servers require that the full JID
+ of the sender be used.
+ :param mnick: Optional nickname of the sender.
+ """
+ self.make_message(mto, mbody, msubject, mtype,
+ mhtml, mfrom, mnick).send()
+
+ def send_presence(self, pshow=None, pstatus=None, ppriority=None,
+ pto=None, pfrom=None, ptype=None, pnick=None):
+ """
+ Create, initialize, and send a new
+ :class:`~sleekxmpp.stanza.presence.Presence` stanza.
+
+ :param pshow: The presence's show value.
+ :param pstatus: The presence's status message.
+ :param ppriority: This connection's priority.
+ :param pto: The recipient of a directed presence.
+ :param ptype: The type of presence, such as ``'subscribe'``.
+ :param pfrom: The sender of the presence.
+ :param pnick: Optional nickname of the presence's sender.
+ """
+ # Python2.6 chokes on Unicode strings for dict keys.
+ args = {str('pto'): pto,
+ str('ptype'): ptype,
+ str('pshow'): pshow,
+ str('pstatus'): pstatus,
+ str('ppriority'): ppriority,
+ str('pnick'): pnick}
+
+ if self.is_component:
+ self.roster[pfrom].send_presence(**args)
+ else:
+ self.client_roster.send_presence(**args)
+
+ def send_presence_subscription(self, pto, pfrom=None,
+ ptype='subscribe', pnick=None):
+ """
+ Create, initialize, and send a new
+ :class:`~sleekxmpp.stanza.presence.Presence` stanza of
+ type ``'subscribe'``.
+
+ :param pto: The recipient of a directed presence.
+ :param pfrom: The sender of the presence.
+ :param ptype: The type of presence, such as ``'subscribe'``.
+ :param pnick: Optional nickname of the presence's sender.
+ """
+ presence = self.makePresence(ptype=ptype,
+ pfrom=pfrom,
+ pto=self.getjidbare(pto))
+ if pnick:
+ nick = ET.Element('{http://jabber.org/protocol/nick}nick')
+ nick.text = pnick
+ presence.append(nick)
+ presence.send()
+
+ @property
+ def jid(self):
+ """Attribute accessor for bare jid"""
+ log.warning("jid property deprecated. Use boundjid.bare")
+ return self.boundjid.bare
+
+ @jid.setter
+ def jid(self, value):
+ log.warning("jid property deprecated. Use boundjid.bare")
+ self.boundjid.bare = value
+
+ @property
+ def fulljid(self):
+ """Attribute accessor for full jid"""
+ log.warning("fulljid property deprecated. Use boundjid.full")
+ return self.boundjid.full
+
+ @fulljid.setter
+ def fulljid(self, value):
+ log.warning("fulljid property deprecated. Use boundjid.full")
+ self.boundjid.full = value
+
+ @property
+ def resource(self):
+ """Attribute accessor for jid resource"""
+ log.warning("resource property deprecated. Use boundjid.resource")
+ return self.boundjid.resource
+
+ @resource.setter
+ def resource(self, value):
+ log.warning("fulljid property deprecated. Use boundjid.full")
+ self.boundjid.resource = value
+
+ @property
+ def username(self):
+ """Attribute accessor for jid usernode"""
+ log.warning("username property deprecated. Use boundjid.user")
+ return self.boundjid.user
+
+ @username.setter
+ def username(self, value):
+ log.warning("username property deprecated. Use boundjid.user")
+ self.boundjid.user = value
+
+ @property
+ def server(self):
+ """Attribute accessor for jid host"""
+ log.warning("server property deprecated. Use boundjid.host")
+ return self.boundjid.server
+
+ @server.setter
+ def server(self, value):
+ log.warning("server property deprecated. Use boundjid.host")
+ self.boundjid.server = value
+
+ @property
+ def auto_authorize(self):
+ """Auto accept or deny subscription requests.
+
+ If ``True``, auto accept subscription requests.
+ If ``False``, auto deny subscription requests.
+ If ``None``, don't automatically respond.
+ """
+ return self.roster.auto_authorize
+
+ @auto_authorize.setter
+ def auto_authorize(self, value):
+ self.roster.auto_authorize = value
+
+ @property
+ def auto_subscribe(self):
+ """Auto send requests for mutual subscriptions.
+
+ If ``True``, auto send mutual subscription requests.
+ """
+ return self.roster.auto_subscribe
+
+ @auto_subscribe.setter
+ def auto_subscribe(self, value):
+ self.roster.auto_subscribe = value
+
+ def set_jid(self, jid):
+ """Rip a JID apart and claim it as our own."""
+ log.debug("setting jid to %s", jid)
+ self.boundjid.full = jid
+
+ def getjidresource(self, fulljid):
+ if '/' in fulljid:
+ return fulljid.split('/', 1)[-1]
+ else:
+ return ''
+
+ def getjidbare(self, fulljid):
+ return fulljid.split('/', 1)[0]
+
+ def _handle_disconnected(self, event):
+ """When disconnected, reset the roster"""
+ self.roster.reset()
+
+ def _handle_stream_error(self, error):
+ self.event('stream_error', error)
+
+ def _handle_message(self, msg):
+ """Process incoming message stanzas."""
+ self.event('message', msg)
+
+ def _handle_available(self, presence):
+ pto = presence['to'].bare
+ pfrom = presence['from'].bare
+ self.roster[pto][pfrom].handle_available(presence)
+
+ def _handle_unavailable(self, presence):
+ pto = presence['to'].bare
+ pfrom = presence['from'].bare
+ self.roster[pto][pfrom].handle_unavailable(presence)
+
+ def _handle_new_subscription(self, stanza):
+ """Attempt to automatically handle subscription requests.
+
+ Subscriptions will be approved if the request is from
+ a whitelisted JID, of :attr:`auto_authorize` is True. They
+ will be rejected if :attr:`auto_authorize` is False. Setting
+ :attr:`auto_authorize` to ``None`` will disable automatic
+ subscription handling (except for whitelisted JIDs).
+
+ If a subscription is accepted, a request for a mutual
+ subscription will be sent if :attr:`auto_subscribe` is ``True``.
+ """
+ roster = self.roster[stanza['to'].bare]
+ item = self.roster[stanza['to'].bare][stanza['from'].bare]
+ if item['whitelisted']:
+ item.authorize()
+ elif roster.auto_authorize:
+ item.authorize()
+ if roster.auto_subscribe:
+ item.subscribe()
+ elif roster.auto_authorize == False:
+ item.unauthorize()
+
+ def _handle_removed_subscription(self, presence):
+ pto = presence['to'].bare
+ pfrom = presence['from'].bare
+ self.roster[pto][pfrom].unauthorize()
+
+ def _handle_subscribe(self, presence):
+ pto = presence['to'].bare
+ pfrom = presence['from'].bare
+ self.roster[pto][pfrom].handle_subscribe(presence)
+
+ def _handle_subscribed(self, presence):
+ pto = presence['to'].bare
+ pfrom = presence['from'].bare
+ self.roster[pto][pfrom].handle_subscribed(presence)
+
+ def _handle_unsubscribe(self, presence):
+ pto = presence['to'].bare
+ pfrom = presence['from'].bare
+ self.roster[pto][pfrom].handle_unsubscribe(presence)
+
+ def _handle_unsubscribed(self, presence):
+ pto = presence['to'].bare
+ pfrom = presence['from'].bare
+ self.roster[pto][pfrom].handle_unsubscribed(presence)
+
+ def _handle_presence(self, presence):
+ """Process incoming presence stanzas.
+
+ Update the roster with presence information.
+ """
+ self.event("presence_%s" % presence['type'], presence)
+
+ # Check for changes in subscription state.
+ if presence['type'] in ('subscribe', 'subscribed',
+ 'unsubscribe', 'unsubscribed'):
+ self.event('changed_subscription', presence)
+ return
+ elif not presence['type'] in ('available', 'unavailable') and \
+ not presence['type'] in presence.showtypes:
+ return
+
+ def exception(self, exception):
+ """Process any uncaught exceptions, notably
+ :class:`~sleekxmpp.exceptions.IqError` and
+ :class:`~sleekxmpp.exceptions.IqTimeout` exceptions.
+
+ :param exception: An unhandled :class:`Exception` object.
+ """
+ if isinstance(exception, IqError):
+ iq = exception.iq
+ log.error('%s: %s', iq['error']['condition'],
+ iq['error']['text'])
+ log.warning('You should catch IqError exceptions')
+ elif isinstance(exception, IqTimeout):
+ iq = exception.iq
+ log.error('Request timed out: %s', iq)
+ log.warning('You should catch IqTimeout exceptions')
+ elif isinstance(exception, SyntaxError):
+ # Hide stream parsing errors that occur when the
+ # stream is disconnected (they've been handled, we
+ # don't need to make a mess in the logs).
+ pass
+ else:
+ log.exception(exception)
+
+
+# Restore the old, lowercased name for backwards compatibility.
+basexmpp = BaseXMPP
+
+# To comply with PEP8, method names now use underscores.
+# Deprecated method names are re-mapped for backwards compatibility.
+BaseXMPP.registerPlugin = BaseXMPP.register_plugin
+BaseXMPP.makeIq = BaseXMPP.make_iq
+BaseXMPP.makeIqGet = BaseXMPP.make_iq_get
+BaseXMPP.makeIqResult = BaseXMPP.make_iq_result
+BaseXMPP.makeIqSet = BaseXMPP.make_iq_set
+BaseXMPP.makeIqError = BaseXMPP.make_iq_error
+BaseXMPP.makeIqQuery = BaseXMPP.make_iq_query
+BaseXMPP.makeQueryRoster = BaseXMPP.make_query_roster
+BaseXMPP.makeMessage = BaseXMPP.make_message
+BaseXMPP.makePresence = BaseXMPP.make_presence
+BaseXMPP.sendMessage = BaseXMPP.send_message
+BaseXMPP.sendPresence = BaseXMPP.send_presence
+BaseXMPP.sendPresenceSubscription = BaseXMPP.send_presence_subscription
diff --git a/sleekxmpp/clientxmpp.py b/sleekxmpp/clientxmpp.py
new file mode 100644
index 00000000..69e7db6c
--- /dev/null
+++ b/sleekxmpp/clientxmpp.py
@@ -0,0 +1,310 @@
+# -*- coding: utf-8 -*-
+"""
+ sleekxmpp.clientxmpp
+ ~~~~~~~~~~~~~~~~~~~~
+
+ This module provides XMPP functionality that
+ is specific to client connections.
+
+ Part of SleekXMPP: The Sleek XMPP Library
+
+ :copyright: (c) 2011 Nathanael C. Fritz
+ :license: MIT, see LICENSE for more details
+"""
+
+from __future__ import absolute_import, unicode_literals
+
+import logging
+import base64
+import sys
+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 *
+from sleekxmpp.xmlstream import XMLStream, RestartStream
+from sleekxmpp.xmlstream import StanzaBase, ET, register_stanza_plugin
+from sleekxmpp.xmlstream.matcher import *
+from sleekxmpp.xmlstream.handler import *
+
+# Flag indicating if DNS SRV records are available for use.
+try:
+ import dns.resolver
+except ImportError:
+ DNSPYTHON = False
+else:
+ DNSPYTHON = True
+
+
+log = logging.getLogger(__name__)
+
+
+class ClientXMPP(BaseXMPP):
+
+ """
+ SleekXMPP's client class. (Use only for good, not for evil.)
+
+ Typical use pattern:
+
+ .. code-block:: python
+
+ xmpp = ClientXMPP('user@server.tld/resource', 'password')
+ # ... Register plugins and event handlers ...
+ xmpp.connect()
+ xmpp.process(block=False) # block=True will block the current
+ # thread. By default, block=False
+
+ :param jid: The JID of the XMPP user account.
+ :param password: The password for the XMPP user account.
+ :param ssl: **Deprecated.**
+ :param plugin_config: A dictionary of plugin configurations.
+ :param plugin_whitelist: A list of approved plugins that
+ will be loaded when calling
+ :meth:`~sleekxmpp.basexmpp.BaseXMPP.register_plugins()`.
+ :param escape_quotes: **Deprecated.**
+ """
+
+ def __init__(self, jid, password, ssl=False, plugin_config={},
+ plugin_whitelist=[], escape_quotes=True, sasl_mech=None):
+ BaseXMPP.__init__(self, jid, 'jabber:client')
+
+ self.set_jid(jid)
+ self.password = password
+ self.escape_quotes = escape_quotes
+ self.plugin_config = plugin_config
+ self.plugin_whitelist = plugin_whitelist
+ self.default_port = 5222
+
+ self.stream_header = "<stream:stream to='%s' %s %s version='1.0'>" % (
+ self.boundjid.host,
+ "xmlns:stream='%s'" % self.stream_ns,
+ "xmlns='%s'" % self.default_ns)
+ self.stream_footer = "</stream:stream>"
+
+ 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('session_bind', self._handle_session_bind)
+
+ self.register_stanza(StreamFeatures)
+
+ self.register_handler(
+ Callback('Stream Features',
+ MatchXPath('{%s}features' % self.stream_ns),
+ self._handle_stream_features))
+ self.register_handler(
+ Callback('Roster Update',
+ MatchXPath('{%s}iq/{%s}query' % (
+ self.default_ns,
+ 'jabber:iq:roster')),
+ self._handle_roster))
+
+ # Setup default stream features
+ self.register_plugin('feature_starttls')
+ self.register_plugin('feature_bind')
+ self.register_plugin('feature_session')
+ self.register_plugin('feature_mechanisms',
+ pconfig={'use_mech': sasl_mech} if sasl_mech else None)
+
+ def connect(self, address=tuple(), reattempt=True,
+ use_tls=True, use_ssl=False):
+ """Connect to the XMPP server.
+
+ When no address is given, a SRV lookup for the server will
+ be attempted. If that fails, the server user in the JID
+ will be used.
+
+ :param address -- A tuple containing the server's host and port.
+ :param reattempt: If ``True``, repeat attempting to connect if an
+ error occurs. Defaults to ``True``.
+ :param use_tls: Indicates if TLS should be used for the
+ connection. Defaults to ``True``.
+ :param use_ssl: Indicates if the older SSL connection method
+ should be used. Defaults to ``False``.
+ """
+ self.session_started_event.clear()
+ if not address:
+ address = (self.boundjid.host, 5222)
+
+ return XMLStream.connect(self, address[0], address[1],
+ use_tls=use_tls, use_ssl=use_ssl,
+ reattempt=reattempt)
+
+ def get_dns_records(self, domain, port=None):
+ """Get the DNS records for a domain, including SRV records.
+
+ :param domain: The domain in question.
+ :param port: If the results don't include a port, use this one.
+ """
+ if port is None:
+ port = self.default_port
+ if DNSPYTHON:
+ try:
+ record = "_xmpp-client._tcp.%s" % domain
+ answers = []
+ for answer in dns.resolver.query(record, dns.rdatatype.SRV):
+ address = (answer.target.to_text()[:-1], answer.port)
+ answers.append((address, answer.priority, answer.weight))
+ except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer):
+ log.warning("No SRV records for %s", domain)
+ answers = super(ClientXMPP, self).get_dns_records(domain, port)
+ except dns.exception.Timeout:
+ log.warning("DNS resolution timed out " + \
+ "for SRV record of %s", domain)
+ answers = super(ClientXMPP, self).get_dns_records(domain, port)
+ return answers
+ else:
+ log.warning("dnspython is not installed -- " + \
+ "relying on OS A record resolution")
+ return [((domain, port), 0, 0)]
+
+ def register_feature(self, name, handler, restart=False, order=5000):
+ """Register a stream feature handler.
+
+ :param name: The name of the stream feature.
+ :param handler: The function to execute if the feature is received.
+ :param restart: Indicates if feature processing should halt with
+ this feature. Defaults to ``False``.
+ :param order: The relative ordering in which the feature should
+ be negotiated. Lower values will be attempted
+ earlier when available.
+ """
+ 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):
+ """Add or change a roster item.
+
+ :param jid: The JID of the entry to modify.
+ :param name: The user's nickname for this JID.
+ :param subscription: The subscription status. May be one of
+ ``'to'``, ``'from'``, ``'both'``, or
+ ``'none'``. If set to ``'remove'``,
+ the entry will be deleted.
+ :param groups: The roster groups that contain this item.
+ :param block: Specify if the roster request will block
+ until a response is received, or a timeout
+ occurs. Defaults to ``True``.
+ :param timeout: The length of time (in seconds) to wait
+ for a response before continuing if blocking
+ is used. Defaults to
+ :attr:`~sleekxmpp.xmlstream.xmlstream.XMLStream.response_timeout`.
+ :param callback: Optional reference to a stream handler function.
+ Will be executed when the roster is received.
+ Implies ``block=False``.
+ """
+ return self.client_roster.update(jid, name, subscription, groups,
+ block, timeout, callback)
+
+ def del_roster_item(self, jid):
+ """Remove an item from the roster.
+
+ This is done by setting its subscription status to ``'remove'``.
+
+ :param jid: The JID of the item to remove.
+ """
+ return self.client_roster.remove(jid)
+
+ def get_roster(self, block=True, timeout=None, callback=None):
+ """Request the roster from the server.
+
+ :param block: Specify if the roster request will block until a
+ response is received, or a timeout occurs.
+ Defaults to ``True``.
+ :param timeout: The length of time (in seconds) to wait for a response
+ before continuing if blocking is used.
+ Defaults to
+ :attr:`~sleekxmpp.xmlstream.xmlstream.XMLStream.response_timeout`.
+ :param callback: Optional reference to a stream handler function. Will
+ be executed when the roster is received.
+ Implies ``block=False``.
+ """
+ iq = self.Iq()
+ iq['type'] = 'get'
+ iq.enable('roster')
+
+ if not block and callback is None:
+ callback = lambda resp: self._handle_roster(resp, request=True)
+
+ response = iq.send(block, timeout, callback)
+
+ if block:
+ self._handle_roster(response, request=True)
+ return response
+
+ 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()
+
+ def _handle_stream_features(self, features):
+ """Process the received stream features.
+
+ :param features: The features stanza.
+ """
+ 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):
+ """Update the roster after receiving a roster stanza.
+
+ :param iq: The roster stanza.
+ :param request: Indicates if this stanza is a response
+ to a request for the roster, and not an
+ empty acknowledgement from the server.
+ """
+ if iq['type'] == 'set' or (iq['type'] == 'result' and request):
+ for jid in iq['roster']['items']:
+ item = iq['roster']['items'][jid]
+ roster = self.roster[iq['to'].bare]
+ roster[jid]['name'] = item['name']
+ roster[jid]['groups'] = item['groups']
+ roster[jid]['from'] = item['subscription'] in ['from', 'both']
+ roster[jid]['to'] = item['subscription'] in ['to', 'both']
+ roster[jid]['pending_out'] = (item['ask'] == 'subscribe')
+ self.event('roster_received', iq)
+
+ self.event("roster_update", iq)
+ if iq['type'] == 'set':
+ iq.reply()
+ iq.enable('roster')
+ iq.send()
+
+ def _handle_session_bind(self, jid):
+ """Set the client roster to the JID set by the server.
+
+ :param :class:`sleekxmpp.xmlstream.jid.JID` jid: The bound JID as
+ dictated by the server. The same as :attr:`boundjid`.
+ """
+ self.client_roster = self.roster[jid]
+
+
+# To comply with PEP8, method names now use underscores.
+# Deprecated method names are re-mapped for backwards compatibility.
+ClientXMPP.updateRoster = ClientXMPP.update_roster
+ClientXMPP.delRosterItem = ClientXMPP.del_roster_item
+ClientXMPP.getRoster = ClientXMPP.get_roster
+ClientXMPP.registerFeature = ClientXMPP.register_feature
diff --git a/sleekxmpp/componentxmpp.py b/sleekxmpp/componentxmpp.py
new file mode 100644
index 00000000..5b16c5ef
--- /dev/null
+++ b/sleekxmpp/componentxmpp.py
@@ -0,0 +1,162 @@
+# -*- coding: utf-8 -*-
+"""
+ sleekxmpp.clientxmpp
+ ~~~~~~~~~~~~~~~~~~~~
+
+ This module provides XMPP functionality that
+ is specific to external server component connections.
+
+ Part of SleekXMPP: The Sleek XMPP Library
+
+ :copyright: (c) 2011 Nathanael C. Fritz
+ :license: MIT, see LICENSE for more details
+"""
+
+from __future__ import absolute_import
+
+import logging
+import base64
+import sys
+import hashlib
+
+from sleekxmpp import plugins
+from sleekxmpp import stanza
+from sleekxmpp.basexmpp import BaseXMPP
+from sleekxmpp.xmlstream import XMLStream, RestartStream
+from sleekxmpp.xmlstream import StanzaBase, ET
+from sleekxmpp.xmlstream.matcher import *
+from sleekxmpp.xmlstream.handler import *
+
+
+log = logging.getLogger(__name__)
+
+
+class ComponentXMPP(BaseXMPP):
+
+ """
+ SleekXMPP's basic XMPP server component.
+
+ Use only for good, not for evil.
+
+ :param jid: The JID of the component.
+ :param secret: The secret or password for the component.
+ :param host: The server accepting the component.
+ :param port: The port used to connect to the server.
+ :param plugin_config: A dictionary of plugin configurations.
+ :param plugin_whitelist: A list of approved plugins that
+ will be loaded when calling
+ :meth:`~sleekxmpp.basexmpp.BaseXMPP.register_plugins()`.
+ :param use_jc_ns: Indicates if the ``'jabber:client'`` namespace
+ should be used instead of the standard
+ ``'jabber:component:accept'`` namespace.
+ Defaults to ``False``.
+ """
+
+ def __init__(self, jid, secret, host=None, port=None,
+ plugin_config={}, plugin_whitelist=[], use_jc_ns=False):
+ if use_jc_ns:
+ default_ns = 'jabber:client'
+ else:
+ default_ns = 'jabber:component:accept'
+ BaseXMPP.__init__(self, jid, default_ns)
+
+ self.auto_authorize = None
+ self.stream_header = "<stream:stream %s %s to='%s'>" % (
+ 'xmlns="jabber:component:accept"',
+ 'xmlns:stream="%s"' % self.stream_ns,
+ jid)
+ self.stream_footer = "</stream:stream>"
+ self.server_host = host
+ self.server_port = port
+ self.secret = secret
+
+ self.plugin_config = plugin_config
+ self.plugin_whitelist = plugin_whitelist
+ self.is_component = True
+
+ self.register_handler(
+ Callback('Handshake',
+ MatchXPath('{jabber:component:accept}handshake'),
+ self._handle_handshake))
+ self.add_event_handler('presence_probe',
+ self._handle_probe)
+
+ def connect(self, host=None, port=None, use_ssl=False,
+ use_tls=True, reattempt=True):
+ """Connect to the server.
+
+ Setting ``reattempt`` to ``True`` will cause connection attempts to
+ be made every second until a successful connection is established.
+
+ :param host: The name of the desired server for the connection.
+ Defaults to :attr:`server_host`.
+ :param port: Port to connect to on the server.
+ Defauts to :attr:`server_port`.
+ :param use_ssl: Flag indicating if SSL should be used by connecting
+ directly to a port using SSL.
+ :param use_tls: Flag indicating if TLS should be used, allowing for
+ connecting to a port without using SSL immediately and
+ later upgrading the connection.
+ :param reattempt: Flag indicating if the socket should reconnect
+ after disconnections.
+ """
+ if host is None:
+ host = self.server_host
+ if port is None:
+ port = self.server_port
+ log.debug("Connecting to %s:%s", host, port)
+ return XMLStream.connect(self, host=host, port=port,
+ use_ssl=use_ssl,
+ use_tls=use_tls,
+ reattempt=reattempt)
+
+ def incoming_filter(self, xml):
+ """
+ Pre-process incoming XML stanzas by converting any
+ ``'jabber:client'`` namespaced elements to the component's
+ default namespace.
+
+ :param xml: The XML stanza to pre-process.
+ """
+ if xml.tag.startswith('{jabber:client}'):
+ xml.tag = xml.tag.replace('jabber:client', self.default_ns)
+
+ # The incoming_filter call is only made on top level stanza
+ # elements. So we manually continue filtering on sub-elements.
+ for sub in xml:
+ self.incoming_filter(sub)
+
+ return xml
+
+ def start_stream_handler(self, xml):
+ """
+ Once the streams are established, attempt to handshake
+ with the server to be accepted as a component.
+
+ :param 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)
+ if sys.version_info >= (3, 0):
+ # Handle Unicode byte encoding in Python 3.
+ pre_hash = bytes(pre_hash, 'utf-8')
+
+ handshake = ET.Element('{jabber:component:accept}handshake')
+ handshake.text = hashlib.sha1(pre_hash).hexdigest().lower()
+ self.send_xml(handshake, now=True)
+
+ def _handle_handshake(self, xml):
+ """The handshake has been accepted.
+
+ :param xml: The reply handshake stanza.
+ """
+ self.session_started_event.set()
+ self.event("session_start")
+
+ def _handle_probe(self, presence):
+ pto = presence['to'].bare
+ pfrom = presence['from'].bare
+ self.roster[pto][pfrom].handle_probe(presence)
diff --git a/sleekxmpp/exceptions.py b/sleekxmpp/exceptions.py
new file mode 100644
index 00000000..6bac1e40
--- /dev/null
+++ b/sleekxmpp/exceptions.py
@@ -0,0 +1,90 @@
+# -*- coding: utf-8 -*-
+"""
+ sleekxmpp.exceptions
+ ~~~~~~~~~~~~~~~~~~~~
+
+ Part of SleekXMPP: The Sleek XMPP Library
+
+ :copyright: (c) 2011 Nathanael C. Fritz
+ :license: MIT, see LICENSE for more details
+"""
+
+
+class XMPPError(Exception):
+
+ """
+ A generic exception that may be raised while processing an XMPP stanza
+ to indicate that an error response stanza should be sent.
+
+ The exception method for stanza objects extending
+ :class:`~sleekxmpp.stanza.rootstanza.RootStanza` will create an error
+ stanza and initialize any additional substanzas using the extension
+ information included in the exception.
+
+ Meant for use in SleekXMPP plugins and applications using SleekXMPP.
+
+ Extension information can be included to add additional XML elements
+ to the generated error stanza.
+
+ :param condition: The XMPP defined error condition.
+ Defaults to ``'undefined-condition'``.
+ :param text: Human readable text describing the error.
+ :param etype: The XMPP error type, such as ``'cancel'`` or ``'modify'``.
+ Defaults to ``'cancel'``.
+ :param extension: Tag name of the extension's XML content.
+ :param extension_ns: XML namespace of the extensions' XML content.
+ :param extension_args: Content and attributes for the extension
+ element. Same as the additional arguments to
+ the :class:`~xml.etree.ElementTree.Element`
+ constructor.
+ :param clear: Indicates if the stanza's contents should be
+ removed before replying with an error.
+ Defaults to ``True``.
+ """
+
+ def __init__(self, condition='undefined-condition', text=None,
+ etype='cancel', extension=None, extension_ns=None,
+ extension_args=None, clear=True):
+ if extension_args is None:
+ extension_args = {}
+
+ self.condition = condition
+ self.text = text
+ self.etype = etype
+ self.clear = clear
+ self.extension = extension
+ self.extension_ns = extension_ns
+ self.extension_args = extension_args
+
+
+class IqTimeout(XMPPError):
+
+ """
+ An exception which indicates that an IQ request response has not been
+ received within the alloted time window.
+ """
+
+ def __init__(self, iq):
+ super(IqTimeout, self).__init__(
+ condition='remote-server-timeout',
+ etype='cancel')
+
+ #: The :class:`~sleekxmpp.stanza.iq.Iq` stanza whose response
+ #: did not arrive before the timeout expired.
+ self.iq = iq
+
+class IqError(XMPPError):
+
+ """
+ An exception raised when an Iq stanza of type 'error' is received
+ after making a blocking send call.
+ """
+
+ def __init__(self, iq):
+ super(IqError, self).__init__(
+ condition=iq['error']['condition'],
+ text=iq['error']['text'],
+ etype=iq['error']['type'])
+
+ #: The :class:`~sleekxmpp.stanza.iq.Iq` error result stanza.
+ self.iq = iq
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..72897131
--- /dev/null
+++ b/sleekxmpp/features/feature_bind/bind.py
@@ -0,0 +1,65 @@
+"""
+ 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.event('session_bind', self.xmpp.boundjid, direct=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..deff5d30
--- /dev/null
+++ b/sleekxmpp/features/feature_mechanisms/mechanisms.py
@@ -0,0 +1,131 @@
+"""
+ 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
+ if 'access_token' in values:
+ values['access_token'] = 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..d2a981f9
--- /dev/null
+++ b/sleekxmpp/features/feature_mechanisms/stanza/auth.py
@@ -0,0 +1,49 @@
+"""
+ 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
+
+ #: Some SASL mechs require sending values as is,
+ #: without converting base64.
+ plain_mechs = set(['X-MESSENGER-OAUTH2'])
+
+ def setup(self, xml):
+ StanzaBase.setup(self, xml)
+ self.xml.tag = self.tag_name()
+
+ def get_value(self):
+ if not self['mechanism'] in self.plain_mechs:
+ return base64.b64decode(bytes(self.xml.text))
+ else:
+ return self.xml.text
+
+ def set_value(self, values):
+ if not self['mechanism'] in self.plain_mechs:
+ self.xml.text = bytes(base64.b64encode(values)).decode('utf-8')
+ else:
+ self.xml.text = bytes(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..4e2b6621
--- /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
new file mode 100644
index 00000000..0b2fa119
--- /dev/null
+++ b/sleekxmpp/plugins/__init__.py
@@ -0,0 +1,36 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+__all__ = [
+ # Non-standard
+ 'gmail_notify', # Gmail searching and notifications
+
+ # XEPS
+ 'xep_0004', # Data Forms
+ 'xep_0009', # Jabber-RPC
+ 'xep_0012', # Last Activity
+ 'xep_0030', # Service Discovery
+ 'xep_0033', # Extended Stanza Addresses
+ 'xep_0045', # Multi-User Chat (Client)
+ 'xep_0050', # Ad-hoc Commands
+ 'xep_0059', # Result Set Management
+ 'xep_0060', # Pubsub (Client)
+ 'xep_0066', # Out-of-band Transfer
+# 'xep_0078', # Non-SASL auth. Don't automatically load
+ 'xep_0082', # XMPP Date and Time Profiles
+ 'xep_0085', # Chat State Notifications
+ 'xep_0086', # Legacy Error Codes
+ 'xep_0092', # Software Version
+ 'xep_0115', # Entity Capabilities
+ 'xep_0128', # Extended Service Discovery
+ 'xep_0199', # Ping
+ 'xep_0202', # Entity Time
+ 'xep_0203', # Delayed Delivery
+ 'xep_0224', # Attention
+ 'xep_0249', # Direct MUC Invitations
+]
diff --git a/sleekxmpp/plugins/base.py b/sleekxmpp/plugins/base.py
new file mode 100644
index 00000000..561421d8
--- /dev/null
+++ b/sleekxmpp/plugins/base.py
@@ -0,0 +1,91 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+
+class base_plugin(object):
+
+ """
+ The base_plugin class serves as a base for user created plugins
+ that provide support for existing or experimental XEPS.
+
+ Each plugin has a dictionary for configuration options, as well
+ as a name and description.
+
+ The lifecycle of a plugin is:
+ 1. The plugin is instantiated during registration.
+ 2. Once the XML stream begins processing, the method
+ plugin_init() is called (if the plugin is configured
+ as enabled with {'enable': True}).
+ 3. After all plugins have been initialized, the
+ method post_init() is called.
+
+ Recommended event handlers:
+ session_start -- Plugins which require the use of the current
+ bound JID SHOULD wait for the session_start
+ event to perform any initialization (or
+ resetting). This is a transitive recommendation,
+ plugins that use other plugins which use the
+ bound JID should also wait for session_start
+ before making such calls.
+ session_end -- If the plugin keeps any per-session state,
+ such as joined MUC rooms, such state SHOULD
+ be cleared when the session_end event is raised.
+
+ Attributes:
+ xep -- The XEP number the plugin implements, if any.
+ description -- A short description of the plugin, typically
+ the long name of the implemented XEP.
+ xmpp -- The main SleekXMPP instance.
+ config -- A dictionary of custom configuration values.
+ The value 'enable' is special and controls
+ whether or not the plugin is initialized
+ after registration.
+ post_initted -- Executed after all plugins have been initialized
+ to handle any cross-plugin interactions, such as
+ registering service discovery items.
+ enable -- Indicates that the plugin is enabled for use and
+ will be initialized after registration.
+
+ Methods:
+ plugin_init -- Initialize the plugin state.
+ post_init -- Handle any cross-plugin concerns.
+ """
+
+ def __init__(self, xmpp, config=None):
+ """
+ Instantiate a new plugin and store the given configuration.
+
+ Arguments:
+ xmpp -- The main SleekXMPP instance.
+ config -- A dictionary of configuration values.
+ """
+ if config is None:
+ config = {}
+ self.xep = None
+ self.rfc = None
+ self.description = 'Base Plugin'
+ self.xmpp = xmpp
+ self.config = config
+ self.post_inited = False
+ self.enable = config.get('enable', True)
+ if self.enable:
+ self.plugin_init()
+
+ def plugin_init(self):
+ """
+ Initialize plugin state, such as registering any stream or
+ event handlers, or new stanza types.
+ """
+ pass
+
+ def post_init(self):
+ """
+ Perform any cross-plugin interactions, such as registering
+ service discovery identities or items.
+ """
+ self.post_inited = True
diff --git a/sleekxmpp/plugins/gmail_notify.py b/sleekxmpp/plugins/gmail_notify.py
new file mode 100644
index 00000000..fc97a2ab
--- /dev/null
+++ b/sleekxmpp/plugins/gmail_notify.py
@@ -0,0 +1,149 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+import logging
+from . import base
+from .. xmlstream.handler.callback import Callback
+from .. xmlstream.matcher.xpath import MatchXPath
+from .. xmlstream.stanzabase import registerStanzaPlugin, ElementBase, ET, JID
+from .. stanza.iq import Iq
+
+
+log = logging.getLogger(__name__)
+
+
+class GmailQuery(ElementBase):
+ namespace = 'google:mail:notify'
+ name = 'query'
+ plugin_attrib = 'gmail'
+ interfaces = set(('newer-than-time', 'newer-than-tid', 'q', 'search'))
+
+ def getSearch(self):
+ return self['q']
+
+ def setSearch(self, search):
+ self['q'] = search
+
+ def delSearch(self):
+ del self['q']
+
+
+class MailBox(ElementBase):
+ namespace = 'google:mail:notify'
+ name = 'mailbox'
+ plugin_attrib = 'mailbox'
+ interfaces = set(('result-time', 'total-matched', 'total-estimate',
+ 'url', 'threads', 'matched', 'estimate'))
+
+ def getThreads(self):
+ threads = []
+ for threadXML in self.xml.findall('{%s}%s' % (MailThread.namespace,
+ MailThread.name)):
+ threads.append(MailThread(xml=threadXML, parent=None))
+ return threads
+
+ def getMatched(self):
+ return self['total-matched']
+
+ def getEstimate(self):
+ return self['total-estimate'] == '1'
+
+
+class MailThread(ElementBase):
+ namespace = 'google:mail:notify'
+ name = 'mail-thread-info'
+ plugin_attrib = 'thread'
+ interfaces = set(('tid', 'participation', 'messages', 'date',
+ 'senders', 'url', 'labels', 'subject', 'snippet'))
+ sub_interfaces = set(('labels', 'subject', 'snippet'))
+
+ def getSenders(self):
+ senders = []
+ sendersXML = self.xml.find('{%s}senders' % self.namespace)
+ if sendersXML is not None:
+ for senderXML in sendersXML.findall('{%s}sender' % self.namespace):
+ senders.append(MailSender(xml=senderXML, parent=None))
+ return senders
+
+
+class MailSender(ElementBase):
+ namespace = 'google:mail:notify'
+ name = 'sender'
+ plugin_attrib = 'sender'
+ interfaces = set(('address', 'name', 'originator', 'unread'))
+
+ def getOriginator(self):
+ return self.xml.attrib.get('originator', '0') == '1'
+
+ def getUnread(self):
+ return self.xml.attrib.get('unread', '0') == '1'
+
+
+class NewMail(ElementBase):
+ namespace = 'google:mail:notify'
+ name = 'new-mail'
+ plugin_attrib = 'new-mail'
+
+
+class gmail_notify(base.base_plugin):
+ """
+ Google Talk: Gmail Notifications
+ """
+
+ def plugin_init(self):
+ self.description = 'Google Talk: Gmail Notifications'
+
+ self.xmpp.registerHandler(
+ Callback('Gmail Result',
+ MatchXPath('{%s}iq/{%s}%s' % (self.xmpp.default_ns,
+ MailBox.namespace,
+ MailBox.name)),
+ self.handle_gmail))
+
+ self.xmpp.registerHandler(
+ Callback('Gmail New Mail',
+ MatchXPath('{%s}iq/{%s}%s' % (self.xmpp.default_ns,
+ NewMail.namespace,
+ NewMail.name)),
+ self.handle_new_mail))
+
+ registerStanzaPlugin(Iq, GmailQuery)
+ registerStanzaPlugin(Iq, MailBox)
+ registerStanzaPlugin(Iq, NewMail)
+
+ self.last_result_time = None
+
+ def handle_gmail(self, iq):
+ mailbox = iq['mailbox']
+ approx = ' approximately' if mailbox['estimated'] else ''
+ log.info('Gmail: Received%s %s emails', approx, mailbox['total-matched'])
+ self.last_result_time = mailbox['result-time']
+ self.xmpp.event('gmail_messages', iq)
+
+ def handle_new_mail(self, iq):
+ log.info("Gmail: New emails received!")
+ self.xmpp.event('gmail_notify')
+ self.checkEmail()
+
+ def getEmail(self, query=None):
+ return self.search(query)
+
+ def checkEmail(self):
+ return self.search(newer=self.last_result_time)
+
+ def search(self, query=None, newer=None):
+ if query is None:
+ log.info("Gmail: Checking for new emails")
+ else:
+ log.info('Gmail: Searching for emails matching: "%s"', query)
+ iq = self.xmpp.Iq()
+ iq['type'] = 'get'
+ iq['to'] = self.xmpp.boundjid.bare
+ iq['gmail']['q'] = query
+ iq['gmail']['newer-than-time'] = newer
+ return iq.send()
diff --git a/sleekxmpp/plugins/jobs.py b/sleekxmpp/plugins/jobs.py
new file mode 100644
index 00000000..cb9deba8
--- /dev/null
+++ b/sleekxmpp/plugins/jobs.py
@@ -0,0 +1,49 @@
+from . import base
+import logging
+from xml.etree import cElementTree as ET
+
+
+log = logging.getLogger(__name__)
+
+
+class jobs(base.base_plugin):
+ def plugin_init(self):
+ self.xep = 'pubsubjob'
+ self.description = "Job distribution over Pubsub"
+
+ def post_init(self):
+ pass
+ #TODO add event
+
+ def createJobNode(self, host, jid, node, config=None):
+ pass
+
+ def createJob(self, host, node, jobid=None, payload=None):
+ return self.xmpp.plugin['xep_0060'].setItem(host, node, ((jobid, payload),))
+
+ def claimJob(self, host, node, jobid, ifrom=None):
+ return self._setState(host, node, jobid, ET.Element('{http://andyet.net/protocol/pubsubjob}claimed'))
+
+ def unclaimJob(self, host, node, jobid):
+ return self._setState(host, node, jobid, ET.Element('{http://andyet.net/protocol/pubsubjob}unclaimed'))
+
+ def finishJob(self, host, node, jobid, payload=None):
+ finished = ET.Element('{http://andyet.net/protocol/pubsubjob}finished')
+ if payload is not None:
+ finished.append(payload)
+ return self._setState(host, node, jobid, finished)
+
+ def _setState(self, host, node, jobid, state, ifrom=None):
+ iq = self.xmpp.Iq()
+ iq['to'] = host
+ if ifrom: iq['from'] = ifrom
+ iq['type'] = 'set'
+ iq['psstate']['node'] = node
+ iq['psstate']['item'] = jobid
+ iq['psstate']['payload'] = state
+ result = iq.send()
+ if result is None or type(result) == bool or result['type'] != 'result':
+ log.error("Unable to change %s:%s to %s", node, jobid, state)
+ return False
+ return True
+
diff --git a/sleekxmpp/plugins/old_0004.py b/sleekxmpp/plugins/old_0004.py
new file mode 100644
index 00000000..7f086866
--- /dev/null
+++ b/sleekxmpp/plugins/old_0004.py
@@ -0,0 +1,421 @@
+"""
+ 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 . import base
+import logging
+from xml.etree import cElementTree as ET
+import copy
+import logging
+#TODO support item groups and results
+
+
+log = logging.getLogger(__name__)
+
+
+class old_0004(base.base_plugin):
+
+ def plugin_init(self):
+ self.xep = '0004'
+ self.description = '*Deprecated Data Forms'
+ self.xmpp.add_handler("<message><x xmlns='jabber:x:data' /></message>", self.handler_message_xform, name='Old Message Form')
+
+ def post_init(self):
+ base.base_plugin.post_init(self)
+ self.xmpp.plugin['xep_0030'].add_feature('jabber:x:data')
+ log.warning("This implementation of XEP-0004 is deprecated.")
+
+ def handler_message_xform(self, xml):
+ object = self.handle_form(xml)
+ self.xmpp.event("message_form", object)
+
+ def handler_presence_xform(self, xml):
+ object = self.handle_form(xml)
+ self.xmpp.event("presence_form", object)
+
+ def handle_form(self, xml):
+ xmlform = xml.find('{jabber:x:data}x')
+ object = self.buildForm(xmlform)
+ self.xmpp.event("message_xform", object)
+ return object
+
+ def buildForm(self, xml):
+ form = Form(ftype=xml.attrib['type'])
+ form.fromXML(xml)
+ return form
+
+ def makeForm(self, ftype='form', title='', instructions=''):
+ return Form(self.xmpp, ftype, title, instructions)
+
+class FieldContainer(object):
+ def __init__(self, stanza = 'form'):
+ self.fields = []
+ self.field = {}
+ self.stanza = stanza
+
+ def addField(self, var, ftype='text-single', label='', desc='', required=False, value=None):
+ self.field[var] = FormField(var, ftype, label, desc, required, value)
+ self.fields.append(self.field[var])
+ return self.field[var]
+
+ def buildField(self, xml):
+ self.field[xml.get('var', '__unnamed__')] = FormField(xml.get('var', '__unnamed__'), xml.get('type', 'text-single'))
+ self.fields.append(self.field[xml.get('var', '__unnamed__')])
+ self.field[xml.get('var', '__unnamed__')].buildField(xml)
+
+ def buildContainer(self, xml):
+ self.stanza = xml.tag
+ for field in xml.findall('{jabber:x:data}field'):
+ self.buildField(field)
+
+ def getXML(self, ftype):
+ container = ET.Element(self.stanza)
+ for field in self.fields:
+ container.append(field.getXML(ftype))
+ return container
+
+class Form(FieldContainer):
+ types = ('form', 'submit', 'cancel', 'result')
+ def __init__(self, xmpp=None, ftype='form', title='', instructions=''):
+ if not ftype in self.types:
+ raise ValueError("Invalid Form Type")
+ FieldContainer.__init__(self)
+ self.xmpp = xmpp
+ self.type = ftype
+ self.title = title
+ self.instructions = instructions
+ self.reported = []
+ self.items = []
+
+ def merge(self, form2):
+ form1 = Form(ftype=self.type)
+ form1.fromXML(self.getXML(self.type))
+ for field in form2.fields:
+ if not field.var in form1.field:
+ form1.addField(field.var, field.type, field.label, field.desc, field.required, field.value)
+ else:
+ form1.field[field.var].value = field.value
+ for option, label in field.options:
+ if (option, label) not in form1.field[field.var].options:
+ form1.fields[field.var].addOption(option, label)
+ return form1
+
+ def copy(self):
+ newform = Form(ftype=self.type)
+ newform.fromXML(self.getXML(self.type))
+ return newform
+
+ def update(self, form):
+ values = form.getValues()
+ for var in values:
+ if var in self.fields:
+ self.fields[var].setValue(self.fields[var])
+
+ def getValues(self):
+ result = {}
+ for field in self.fields:
+ value = field.value
+ if len(value) == 1:
+ value = value[0]
+ result[field.var] = value
+ return result
+
+ def setValues(self, values={}):
+ for field in values:
+ if field in self.field:
+ if isinstance(values[field], list) or isinstance(values[field], tuple):
+ for value in values[field]:
+ self.field[field].setValue(value)
+ else:
+ self.field[field].setValue(values[field])
+
+ def fromXML(self, xml):
+ self.buildForm(xml)
+
+ def addItem(self):
+ newitem = FieldContainer('item')
+ self.items.append(newitem)
+ return newitem
+
+ def buildItem(self, xml):
+ newitem = self.addItem()
+ newitem.buildContainer(xml)
+
+ def addReported(self):
+ reported = FieldContainer('reported')
+ self.reported.append(reported)
+ return reported
+
+ def buildReported(self, xml):
+ reported = self.addReported()
+ reported.buildContainer(xml)
+
+ def setTitle(self, title):
+ self.title = title
+
+ def setInstructions(self, instructions):
+ self.instructions = instructions
+
+ def setType(self, ftype):
+ self.type = ftype
+
+ def getXMLMessage(self, to):
+ msg = self.xmpp.makeMessage(to)
+ msg.append(self.getXML())
+ return msg
+
+ def buildForm(self, xml):
+ self.type = xml.get('type', 'form')
+ if xml.find('{jabber:x:data}title') is not None:
+ self.setTitle(xml.find('{jabber:x:data}title').text)
+ if xml.find('{jabber:x:data}instructions') is not None:
+ self.setInstructions(xml.find('{jabber:x:data}instructions').text)
+ for field in xml.findall('{jabber:x:data}field'):
+ self.buildField(field)
+ for reported in xml.findall('{jabber:x:data}reported'):
+ self.buildReported(reported)
+ for item in xml.findall('{jabber:x:data}item'):
+ self.buildItem(item)
+
+ #def getXML(self, tostring = False):
+ def getXML(self, ftype=None):
+ if ftype:
+ self.type = ftype
+ form = ET.Element('{jabber:x:data}x')
+ form.attrib['type'] = self.type
+ if self.title and self.type in ('form', 'result'):
+ title = ET.Element('{jabber:x:data}title')
+ title.text = self.title
+ form.append(title)
+ if self.instructions and self.type == 'form':
+ instructions = ET.Element('{jabber:x:data}instructions')
+ instructions.text = self.instructions
+ form.append(instructions)
+ for field in self.fields:
+ form.append(field.getXML(self.type))
+ for reported in self.reported:
+ form.append(reported.getXML('{jabber:x:data}reported'))
+ for item in self.items:
+ form.append(item.getXML(self.type))
+ #if tostring:
+ # form = self.xmpp.tostring(form)
+ return form
+
+ def getXHTML(self):
+ form = ET.Element('{http://www.w3.org/1999/xhtml}form')
+ if self.title:
+ title = ET.Element('h2')
+ title.text = self.title
+ form.append(title)
+ if self.instructions:
+ instructions = ET.Element('p')
+ instructions.text = self.instructions
+ form.append(instructions)
+ for field in self.fields:
+ form.append(field.getXHTML())
+ for field in self.reported:
+ form.append(field.getXHTML())
+ for field in self.items:
+ form.append(field.getXHTML())
+ return form
+
+
+ def makeSubmit(self):
+ self.setType('submit')
+
+class FormField(object):
+ types = ('boolean', 'fixed', 'hidden', 'jid-multi', 'jid-single', 'list-multi', 'list-single', 'text-multi', 'text-private', 'text-single')
+ listtypes = ('jid-multi', 'jid-single', 'list-multi', 'list-single')
+ lbtypes = ('fixed', 'text-multi')
+ def __init__(self, var, ftype='text-single', label='', desc='', required=False, value=None):
+ if not ftype in self.types:
+ raise ValueError("Invalid Field Type")
+ self.type = ftype
+ self.var = var
+ self.label = label
+ self.desc = desc
+ self.options = []
+ self.required = False
+ self.value = []
+ if self.type in self.listtypes:
+ self.islist = True
+ else:
+ self.islist = False
+ if self.type in self.lbtypes:
+ self.islinebreak = True
+ else:
+ self.islinebreak = False
+ if value:
+ self.setValue(value)
+
+ def addOption(self, value, label):
+ if self.islist:
+ self.options.append((value, label))
+ else:
+ raise ValueError("Cannot add options to non-list type field.")
+
+ def setTrue(self):
+ if self.type == 'boolean':
+ self.value = [True]
+
+ def setFalse(self):
+ if self.type == 'boolean':
+ self.value = [False]
+
+ def require(self):
+ self.required = True
+
+ def setDescription(self, desc):
+ self.desc = desc
+
+ def setValue(self, value):
+ if self.type == 'boolean':
+ if value in ('1', 1, True, 'true', 'True', 'yes'):
+ value = True
+ else:
+ value = False
+ if self.islinebreak and value is not None:
+ self.value += value.split('\n')
+ else:
+ if len(self.value) and (not self.islist or self.type == 'list-single'):
+ self.value = [value]
+ else:
+ self.value.append(value)
+
+ def delValue(self, value):
+ if type(self.value) == type([]):
+ try:
+ idx = self.value.index(value)
+ if idx != -1:
+ self.value.pop(idx)
+ except ValueError:
+ pass
+ else:
+ self.value = ''
+
+ def setAnswer(self, value):
+ self.setValue(value)
+
+ def buildField(self, xml):
+ self.type = xml.get('type', 'text-single')
+ self.label = xml.get('label', '')
+ for option in xml.findall('{jabber:x:data}option'):
+ self.addOption(option.find('{jabber:x:data}value').text, option.get('label', ''))
+ for value in xml.findall('{jabber:x:data}value'):
+ self.setValue(value.text)
+ if xml.find('{jabber:x:data}required') is not None:
+ self.require()
+ if xml.find('{jabber:x:data}desc') is not None:
+ self.setDescription(xml.find('{jabber:x:data}desc').text)
+
+ def getXML(self, ftype):
+ field = ET.Element('{jabber:x:data}field')
+ if ftype != 'result':
+ field.attrib['type'] = self.type
+ if self.type != 'fixed':
+ if self.var:
+ field.attrib['var'] = self.var
+ if self.label:
+ field.attrib['label'] = self.label
+ if ftype == 'form':
+ for option in self.options:
+ optionxml = ET.Element('{jabber:x:data}option')
+ optionxml.attrib['label'] = option[1]
+ optionval = ET.Element('{jabber:x:data}value')
+ optionval.text = option[0]
+ optionxml.append(optionval)
+ field.append(optionxml)
+ if self.required:
+ required = ET.Element('{jabber:x:data}required')
+ field.append(required)
+ if self.desc:
+ desc = ET.Element('{jabber:x:data}desc')
+ desc.text = self.desc
+ field.append(desc)
+ for value in self.value:
+ valuexml = ET.Element('{jabber:x:data}value')
+ if value is True or value is False:
+ if value:
+ valuexml.text = '1'
+ else:
+ valuexml.text = '0'
+ else:
+ valuexml.text = value
+ field.append(valuexml)
+ return field
+
+ def getXHTML(self):
+ field = ET.Element('div', {'class': 'xmpp-xforms-%s' % self.type})
+ if self.label:
+ label = ET.Element('p')
+ label.text = "%s: " % self.label
+ else:
+ label = ET.Element('p')
+ label.text = "%s: " % self.var
+ field.append(label)
+ if self.type == 'boolean':
+ formf = ET.Element('input', {'type': 'checkbox', 'name': self.var})
+ if len(self.value) and self.value[0] in (True, 'true', '1'):
+ formf.attrib['checked'] = 'checked'
+ elif self.type == 'fixed':
+ formf = ET.Element('p')
+ try:
+ formf.text = ', '.join(self.value)
+ except:
+ pass
+ field.append(formf)
+ formf = ET.Element('input', {'type': 'hidden', 'name': self.var})
+ try:
+ formf.text = ', '.join(self.value)
+ except:
+ pass
+ elif self.type == 'hidden':
+ formf = ET.Element('input', {'type': 'hidden', 'name': self.var})
+ try:
+ formf.text = ', '.join(self.value)
+ except:
+ pass
+ elif self.type in ('jid-multi', 'list-multi'):
+ formf = ET.Element('select', {'name': self.var})
+ for option in self.options:
+ optf = ET.Element('option', {'value': option[0], 'multiple': 'multiple'})
+ optf.text = option[1]
+ if option[1] in self.value:
+ optf.attrib['selected'] = 'selected'
+ formf.append(option)
+ elif self.type in ('jid-single', 'text-single'):
+ formf = ET.Element('input', {'type': 'text', 'name': self.var})
+ try:
+ formf.attrib['value'] = ', '.join(self.value)
+ except:
+ pass
+ elif self.type == 'list-single':
+ formf = ET.Element('select', {'name': self.var})
+ for option in self.options:
+ optf = ET.Element('option', {'value': option[0]})
+ optf.text = option[1]
+ if not optf.text:
+ optf.text = option[0]
+ if option[1] in self.value:
+ optf.attrib['selected'] = 'selected'
+ formf.append(optf)
+ elif self.type == 'text-multi':
+ formf = ET.Element('textarea', {'name': self.var})
+ try:
+ formf.text = ', '.join(self.value)
+ except:
+ pass
+ if not formf.text:
+ formf.text = ' '
+ elif self.type == 'text-private':
+ formf = ET.Element('input', {'type': 'password', 'name': self.var})
+ try:
+ formf.attrib['value'] = ', '.join(self.value)
+ except:
+ pass
+ label.append(formf)
+ return field
+
diff --git a/sleekxmpp/plugins/old_0009.py b/sleekxmpp/plugins/old_0009.py
new file mode 100644
index 00000000..625b03fb
--- /dev/null
+++ b/sleekxmpp/plugins/old_0009.py
@@ -0,0 +1,277 @@
+"""
+XEP-0009 XMPP Remote Procedure Calls
+"""
+from __future__ import with_statement
+from . import base
+import logging
+from xml.etree import cElementTree as ET
+import copy
+import time
+import base64
+
+def py2xml(*args):
+ params = ET.Element("params")
+ for x in args:
+ param = ET.Element("param")
+ param.append(_py2xml(x))
+ params.append(param) #<params><param>...
+ return params
+
+def _py2xml(*args):
+ for x in args:
+ val = ET.Element("value")
+ if type(x) is int:
+ i4 = ET.Element("i4")
+ i4.text = str(x)
+ val.append(i4)
+ if type(x) is bool:
+ boolean = ET.Element("boolean")
+ boolean.text = str(int(x))
+ val.append(boolean)
+ elif type(x) is str:
+ string = ET.Element("string")
+ string.text = x
+ val.append(string)
+ elif type(x) is float:
+ double = ET.Element("double")
+ double.text = str(x)
+ val.append(double)
+ elif type(x) is rpcbase64:
+ b64 = ET.Element("Base64")
+ b64.text = x.encoded()
+ val.append(b64)
+ elif type(x) is rpctime:
+ iso = ET.Element("dateTime.iso8601")
+ iso.text = str(x)
+ val.append(iso)
+ elif type(x) is list:
+ array = ET.Element("array")
+ data = ET.Element("data")
+ for y in x:
+ data.append(_py2xml(y))
+ array.append(data)
+ val.append(array)
+ elif type(x) is dict:
+ struct = ET.Element("struct")
+ for y in x.keys():
+ member = ET.Element("member")
+ name = ET.Element("name")
+ name.text = y
+ member.append(name)
+ member.append(_py2xml(x[y]))
+ struct.append(member)
+ val.append(struct)
+ return val
+
+def xml2py(params):
+ vals = []
+ for param in params.findall('param'):
+ vals.append(_xml2py(param.find('value')))
+ return vals
+
+def _xml2py(value):
+ if value.find('i4') is not None:
+ return int(value.find('i4').text)
+ if value.find('int') is not None:
+ return int(value.find('int').text)
+ if value.find('boolean') is not None:
+ return bool(value.find('boolean').text)
+ if value.find('string') is not None:
+ return value.find('string').text
+ if value.find('double') is not None:
+ return float(value.find('double').text)
+ if value.find('Base64') is not None:
+ return rpcbase64(value.find('Base64').text)
+ if value.find('dateTime.iso8601') is not None:
+ return rpctime(value.find('dateTime.iso8601'))
+ if value.find('struct') is not None:
+ struct = {}
+ for member in value.find('struct').findall('member'):
+ struct[member.find('name').text] = _xml2py(member.find('value'))
+ return struct
+ if value.find('array') is not None:
+ array = []
+ for val in value.find('array').find('data').findall('value'):
+ array.append(_xml2py(val))
+ return array
+ raise ValueError()
+
+class rpcbase64(object):
+ def __init__(self, data):
+ #base 64 encoded string
+ self.data = data
+
+ def decode(self):
+ return base64.decodestring(data)
+
+ def __str__(self):
+ return self.decode()
+
+ def encoded(self):
+ return self.data
+
+class rpctime(object):
+ def __init__(self,data=None):
+ #assume string data is in iso format YYYYMMDDTHH:MM:SS
+ if type(data) is str:
+ self.timestamp = time.strptime(data,"%Y%m%dT%H:%M:%S")
+ elif type(data) is time.struct_time:
+ self.timestamp = data
+ elif data is None:
+ self.timestamp = time.gmtime()
+ else:
+ raise ValueError()
+
+ def iso8601(self):
+ #return a iso8601 string
+ return time.strftime("%Y%m%dT%H:%M:%S",self.timestamp)
+
+ def __str__(self):
+ return self.iso8601()
+
+class JabberRPCEntry(object):
+ def __init__(self,call):
+ self.call = call
+ self.result = None
+ self.error = None
+ self.allow = {} #{'<jid>':['<resource1>',...],...}
+ self.deny = {}
+
+ def check_acl(self, jid, resource):
+ #Check for deny
+ if jid in self.deny.keys():
+ if self.deny[jid] == None or resource in self.deny[jid]:
+ return False
+ #Check for allow
+ if allow == None:
+ return True
+ if jid in self.allow.keys():
+ if self.allow[jid] == None or resource in self.allow[jid]:
+ return True
+ return False
+
+ def acl_allow(self, jid, resource):
+ if jid == None:
+ self.allow = None
+ elif resource == None:
+ self.allow[jid] = None
+ elif jid in self.allow.keys():
+ self.allow[jid].append(resource)
+ else:
+ self.allow[jid] = [resource]
+
+ def acl_deny(self, jid, resource):
+ if jid == None:
+ self.deny = None
+ elif resource == None:
+ self.deny[jid] = None
+ elif jid in self.deny.keys():
+ self.deny[jid].append(resource)
+ else:
+ self.deny[jid] = [resource]
+
+ def call_method(self, args):
+ ret = self.call(*args)
+
+class xep_0009(base.base_plugin):
+
+ def plugin_init(self):
+ self.xep = '0009'
+ self.description = 'Jabber-RPC'
+ self.xmpp.add_handler("<iq type='set'><query xmlns='jabber:iq:rpc' /></iq>",
+ self._callMethod, name='Jabber RPC Call')
+ self.xmpp.add_handler("<iq type='result'><query xmlns='jabber:iq:rpc' /></iq>",
+ self._callResult, name='Jabber RPC Result')
+ self.xmpp.add_handler("<iq type='error'><query xmlns='jabber:iq:rpc' /></iq>",
+ self._callError, name='Jabber RPC Error')
+ self.entries = {}
+ self.activeCalls = []
+
+ def post_init(self):
+ base.base_plugin.post_init(self)
+ self.xmpp.plugin['xep_0030'].add_feature('jabber:iq:rpc')
+ self.xmpp.plugin['xep_0030'].add_identity('automatition','rpc')
+
+ def register_call(self, method, name=None):
+ #@returns an string that can be used in acl commands.
+ with self.lock:
+ if name is None:
+ self.entries[method.__name__] = JabberRPCEntry(method)
+ return method.__name__
+ else:
+ self.entries[name] = JabberRPCEntry(method)
+ return name
+
+ def acl_allow(self, entry, jid=None, resource=None):
+ #allow the method entry to be called by the given jid and resource.
+ #if jid is None it will allow any jid/resource.
+ #if resource is None it will allow any resource belonging to the jid.
+ with self.lock:
+ if self.entries[entry]:
+ self.entries[entry].acl_allow(jid,resource)
+ else:
+ raise ValueError()
+
+ def acl_deny(self, entry, jid=None, resource=None):
+ #Note: by default all requests are denied unless allowed with acl_allow.
+ #If you deny an entry it will not be allowed regardless of acl_allow
+ with self.lock:
+ if self.entries[entry]:
+ self.entries[entry].acl_deny(jid,resource)
+ else:
+ raise ValueError()
+
+ def unregister_call(self, entry):
+ #removes the registered call
+ with self.lock:
+ if self.entries[entry]:
+ del self.entries[entry]
+ else:
+ raise ValueError()
+
+ def makeMethodCallQuery(self,pmethod,params):
+ query = self.xmpp.makeIqQuery(iq,"jabber:iq:rpc")
+ methodCall = ET.Element('methodCall')
+ methodName = ET.Element('methodName')
+ methodName.text = pmethod
+ methodCall.append(methodName)
+ methodCall.append(params)
+ query.append(methodCall)
+ return query
+
+ def makeIqMethodCall(self,pto,pmethod,params):
+ iq = self.xmpp.makeIqSet()
+ iq.set('to',pto)
+ iq.append(self.makeMethodCallQuery(pmethod,params))
+ return iq
+
+ def makeIqMethodResponse(self,pto,pid,params):
+ iq = self.xmpp.makeIqResult(pid)
+ iq.set('to',pto)
+ query = self.xmpp.makeIqQuery(iq,"jabber:iq:rpc")
+ methodResponse = ET.Element('methodResponse')
+ methodResponse.append(params)
+ query.append(methodResponse)
+ return iq
+
+ def makeIqMethodError(self,pto,id,pmethod,params,condition):
+ iq = self.xmpp.makeIqError(id)
+ iq.set('to',pto)
+ iq.append(self.makeMethodCallQuery(pmethod,params))
+ iq.append(self.xmpp['xep_0086'].makeError(condition))
+ return iq
+
+
+
+ def call_remote(self, pto, pmethod, *args):
+ #calls a remote method. Returns the id of the Iq.
+ pass
+
+ def _callMethod(self,xml):
+ pass
+
+ def _callResult(self,xml):
+ pass
+
+ def _callError(self,xml):
+ pass
diff --git a/sleekxmpp/plugins/old_0050.py b/sleekxmpp/plugins/old_0050.py
new file mode 100644
index 00000000..6e969a51
--- /dev/null
+++ b/sleekxmpp/plugins/old_0050.py
@@ -0,0 +1,133 @@
+"""
+ 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 . import base
+import logging
+from xml.etree import cElementTree as ET
+import time
+
+class old_0050(base.base_plugin):
+ """
+ XEP-0050 Ad-Hoc Commands
+ """
+
+ def plugin_init(self):
+ self.xep = '0050'
+ self.description = 'Ad-Hoc Commands'
+ self.xmpp.add_handler("<iq type='set' xmlns='%s'><command xmlns='http://jabber.org/protocol/commands' action='__None__'/></iq>" % self.xmpp.default_ns, self.handler_command, name='Ad-Hoc None')
+ self.xmpp.add_handler("<iq type='set' xmlns='%s'><command xmlns='http://jabber.org/protocol/commands' action='execute'/></iq>" % self.xmpp.default_ns, self.handler_command, name='Ad-Hoc Execute')
+ self.xmpp.add_handler("<iq type='set' xmlns='%s'><command xmlns='http://jabber.org/protocol/commands' action='next'/></iq>" % self.xmpp.default_ns, self.handler_command_next, name='Ad-Hoc Next', threaded=True)
+ self.xmpp.add_handler("<iq type='set' xmlns='%s'><command xmlns='http://jabber.org/protocol/commands' action='cancel'/></iq>" % self.xmpp.default_ns, self.handler_command_cancel, name='Ad-Hoc Cancel')
+ self.xmpp.add_handler("<iq type='set' xmlns='%s'><command xmlns='http://jabber.org/protocol/commands' action='complete'/></iq>" % self.xmpp.default_ns, self.handler_command_complete, name='Ad-Hoc Complete')
+ self.commands = {}
+ self.sessions = {}
+ self.sd = self.xmpp.plugin['xep_0030']
+
+ def post_init(self):
+ base.base_plugin.post_init(self)
+ self.sd.add_feature('http://jabber.org/protocol/commands')
+
+ def addCommand(self, node, name, form, pointer=None, multi=False):
+ self.sd.add_item(None, name, 'http://jabber.org/protocol/commands', node)
+ self.sd.add_identity('automation', 'command-node', name, node)
+ self.sd.add_feature('http://jabber.org/protocol/commands', node)
+ self.sd.add_feature('jabber:x:data', node)
+ self.commands[node] = (name, form, pointer, multi)
+
+ def getNewSession(self):
+ return str(time.time()) + '-' + self.xmpp.getNewId()
+
+ def handler_command(self, xml):
+ in_command = xml.find('{http://jabber.org/protocol/commands}command')
+ sessionid = in_command.get('sessionid', None)
+ node = in_command.get('node')
+ sessionid = self.getNewSession()
+ name, form, pointer, multi = self.commands[node]
+ self.sessions[sessionid] = {}
+ self.sessions[sessionid]['jid'] = xml.get('from')
+ self.sessions[sessionid]['to'] = xml.get('to')
+ self.sessions[sessionid]['past'] = [(form, None)]
+ self.sessions[sessionid]['next'] = pointer
+ npointer = pointer
+ if multi:
+ actions = ['next']
+ status = 'executing'
+ else:
+ if pointer is None:
+ status = 'completed'
+ actions = []
+ else:
+ status = 'executing'
+ actions = ['complete']
+ self.xmpp.send(self.makeCommand(xml.attrib['from'], in_command.attrib['node'], form=form, id=xml.attrib['id'], sessionid=sessionid, status=status, actions=actions))
+
+ def handler_command_complete(self, xml):
+ in_command = xml.find('{http://jabber.org/protocol/commands}command')
+ sessionid = in_command.get('sessionid', None)
+ pointer = self.sessions[sessionid]['next']
+ results = self.xmpp.plugin['old_0004'].makeForm('result')
+ results.fromXML(in_command.find('{jabber:x:data}x'))
+ pointer(results,sessionid)
+ self.xmpp.send(self.makeCommand(xml.attrib['from'], in_command.attrib['node'], form=None, id=xml.attrib['id'], sessionid=sessionid, status='completed', actions=[]))
+ del self.sessions[in_command.get('sessionid')]
+
+
+ def handler_command_next(self, xml):
+ in_command = xml.find('{http://jabber.org/protocol/commands}command')
+ sessionid = in_command.get('sessionid', None)
+ pointer = self.sessions[sessionid]['next']
+ results = self.xmpp.plugin['old_0004'].makeForm('result')
+ results.fromXML(in_command.find('{jabber:x:data}x'))
+ form, npointer, next = pointer(results,sessionid)
+ self.sessions[sessionid]['next'] = npointer
+ self.sessions[sessionid]['past'].append((form, pointer))
+ actions = []
+ actions.append('prev')
+ if npointer is None:
+ status = 'completed'
+ else:
+ status = 'executing'
+ if next:
+ actions.append('next')
+ else:
+ actions.append('complete')
+ self.xmpp.send(self.makeCommand(xml.attrib['from'], in_command.attrib['node'], form=form, id=xml.attrib['id'], sessionid=sessionid, status=status, actions=actions))
+
+ def handler_command_cancel(self, xml):
+ command = xml.find('{http://jabber.org/protocol/commands}command')
+ try:
+ del self.sessions[command.get('sessionid')]
+ except:
+ pass
+ self.xmpp.send(self.makeCommand(xml.attrib['from'], command.attrib['node'], id=xml.attrib['id'], sessionid=command.attrib['sessionid'], status='canceled'))
+
+ def makeCommand(self, to, node, id=None, form=None, sessionid=None, status='executing', actions=[]):
+ if not id:
+ id = self.xmpp.getNewId()
+ iq = self.xmpp.makeIqResult(id)
+ iq.attrib['from'] = self.xmpp.boundjid.full
+ iq.attrib['to'] = to
+ command = ET.Element('{http://jabber.org/protocol/commands}command')
+ command.attrib['node'] = node
+ command.attrib['status'] = status
+ xmlactions = ET.Element('actions')
+ for action in actions:
+ xmlactions.append(ET.Element(action))
+ if xmlactions:
+ command.append(xmlactions)
+ if not sessionid:
+ sessionid = self.getNewSession()
+ else:
+ iq.attrib['from'] = self.sessions[sessionid]['to']
+ command.attrib['sessionid'] = sessionid
+ if form is not None:
+ if hasattr(form,'getXML'):
+ form = form.getXML()
+ command.append(form)
+ iq.append(command)
+ return iq
diff --git a/sleekxmpp/plugins/old_0060.py b/sleekxmpp/plugins/old_0060.py
new file mode 100644
index 00000000..93124fca
--- /dev/null
+++ b/sleekxmpp/plugins/old_0060.py
@@ -0,0 +1,313 @@
+from __future__ import with_statement
+from . import base
+import logging
+#from xml.etree import cElementTree as ET
+from .. xmlstream.stanzabase import registerStanzaPlugin, ElementBase, ET
+from . import stanza_pubsub
+from . 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_0004/__init__.py b/sleekxmpp/plugins/xep_0004/__init__.py
new file mode 100644
index 00000000..aad4e15f
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0004/__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_0004.stanza import Form
+from sleekxmpp.plugins.xep_0004.stanza import FormField, FieldOption
+from sleekxmpp.plugins.xep_0004.dataforms import xep_0004
diff --git a/sleekxmpp/plugins/xep_0004/dataforms.py b/sleekxmpp/plugins/xep_0004/dataforms.py
new file mode 100644
index 00000000..5414be5c
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0004/dataforms.py
@@ -0,0 +1,60 @@
+"""
+ 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 copy
+
+from sleekxmpp.thirdparty import OrderedDict
+
+from sleekxmpp import Message
+from sleekxmpp.xmlstream import register_stanza_plugin, ElementBase, ET
+from sleekxmpp.xmlstream.handler import Callback
+from sleekxmpp.xmlstream.matcher import StanzaPath
+from sleekxmpp.plugins.base import base_plugin
+from sleekxmpp.plugins.xep_0004 import stanza
+from sleekxmpp.plugins.xep_0004.stanza import Form, FormField, FieldOption
+
+
+class xep_0004(base_plugin):
+ """
+ XEP-0004: Data Forms
+ """
+
+ def plugin_init(self):
+ self.xep = '0004'
+ self.description = 'Data Forms'
+ self.stanza = stanza
+
+ self.xmpp.registerHandler(
+ Callback('Data Form',
+ StanzaPath('message/form'),
+ self.handle_form))
+
+ register_stanza_plugin(FormField, FieldOption, iterable=True)
+ register_stanza_plugin(Form, FormField, iterable=True)
+ register_stanza_plugin(Message, Form)
+
+ def make_form(self, ftype='form', title='', instructions=''):
+ f = Form()
+ f['type'] = ftype
+ f['title'] = title
+ f['instructions'] = instructions
+ return f
+
+ def post_init(self):
+ base_plugin.post_init(self)
+ self.xmpp.plugin['xep_0030'].add_feature('jabber:x:data')
+
+ def handle_form(self, message):
+ self.xmpp.event("message_xform", message)
+
+ def build_form(self, xml):
+ return Form(xml=xml)
+
+
+xep_0004.makeForm = xep_0004.make_form
+xep_0004.buildForm = xep_0004.build_form
diff --git a/sleekxmpp/plugins/xep_0004/stanza/__init__.py b/sleekxmpp/plugins/xep_0004/stanza/__init__.py
new file mode 100644
index 00000000..6ad35298
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0004/stanza/__init__.py
@@ -0,0 +1,10 @@
+"""
+ 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_0004.stanza.field import FormField, FieldOption
+from sleekxmpp.plugins.xep_0004.stanza.form import Form
diff --git a/sleekxmpp/plugins/xep_0004/stanza/field.py b/sleekxmpp/plugins/xep_0004/stanza/field.py
new file mode 100644
index 00000000..8156997c
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0004/stanza/field.py
@@ -0,0 +1,180 @@
+"""
+ 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 FormField(ElementBase):
+ namespace = 'jabber:x:data'
+ name = 'field'
+ plugin_attrib = 'field'
+ interfaces = set(('answer', 'desc', 'required', 'value',
+ 'options', 'label', 'type', 'var'))
+ sub_interfaces = set(('desc',))
+ plugin_tag_map = {}
+ plugin_attrib_map = {}
+
+ field_types = set(('boolean', 'fixed', 'hidden', 'jid-multi',
+ 'jid-single', 'list-multi', 'list-single',
+ 'text-multi', 'text-private', 'text-single'))
+
+ true_values = set((True, '1', 'true'))
+ option_types = set(('list-multi', 'list-single'))
+ multi_line_types = set(('hidden', 'text-multi'))
+ multi_value_types = set(('hidden', 'jid-multi',
+ 'list-multi', 'text-multi'))
+
+ def setup(self, xml=None):
+ if ElementBase.setup(self, xml):
+ self._type = None
+ else:
+ self._type = self['type']
+
+ def set_type(self, value):
+ self._set_attr('type', value)
+ if value:
+ self._type = value
+
+ def add_option(self, label='', value=''):
+ if self._type in self.option_types:
+ opt = FieldOption(parent=self)
+ opt['label'] = label
+ opt['value'] = value
+ else:
+ raise ValueError("Cannot add options to " + \
+ "a %s field." % self['type'])
+
+ def del_options(self):
+ optsXML = self.xml.findall('{%s}option' % self.namespace)
+ for optXML in optsXML:
+ self.xml.remove(optXML)
+
+ def del_required(self):
+ reqXML = self.xml.find('{%s}required' % self.namespace)
+ if reqXML is not None:
+ self.xml.remove(reqXML)
+
+ def del_value(self):
+ valsXML = self.xml.findall('{%s}value' % self.namespace)
+ for valXML in valsXML:
+ self.xml.remove(valXML)
+
+ def get_answer(self):
+ return self['value']
+
+ def get_options(self):
+ options = []
+ optsXML = self.xml.findall('{%s}option' % self.namespace)
+ for optXML in optsXML:
+ opt = FieldOption(xml=optXML)
+ options.append({'label': opt['label'], 'value': opt['value']})
+ return options
+
+ def get_required(self):
+ reqXML = self.xml.find('{%s}required' % self.namespace)
+ return reqXML is not None
+
+ def get_value(self, convert=True):
+ valsXML = self.xml.findall('{%s}value' % self.namespace)
+ if len(valsXML) == 0:
+ return None
+ elif self._type == 'boolean':
+ if convert:
+ return valsXML[0].text in self.true_values
+ return valsXML[0].text
+ elif self._type in self.multi_value_types or len(valsXML) > 1:
+ values = []
+ for valXML in valsXML:
+ if valXML.text is None:
+ valXML.text = ''
+ values.append(valXML.text)
+ if self._type == 'text-multi' and convert:
+ values = "\n".join(values)
+ return values
+ else:
+ if valsXML[0].text is None:
+ return ''
+ return valsXML[0].text
+
+ def set_answer(self, answer):
+ self['value'] = answer
+
+ def set_false(self):
+ self['value'] = False
+
+ def set_options(self, options):
+ for value in options:
+ if isinstance(value, dict):
+ self.add_option(**value)
+ else:
+ self.add_option(value=value)
+
+ def set_required(self, required):
+ exists = self['required']
+ if not exists and required:
+ self.xml.append(ET.Element('{%s}required' % self.namespace))
+ elif exists and not required:
+ del self['required']
+
+ def set_true(self):
+ self['value'] = True
+
+ def set_value(self, value):
+ del self['value']
+ valXMLName = '{%s}value' % self.namespace
+
+ if self._type == 'boolean':
+ if value in self.true_values:
+ valXML = ET.Element(valXMLName)
+ valXML.text = '1'
+ self.xml.append(valXML)
+ else:
+ valXML = ET.Element(valXMLName)
+ valXML.text = '0'
+ self.xml.append(valXML)
+ elif self._type in self.multi_value_types or self._type in ('', None):
+ if not isinstance(value, list):
+ value = value.replace('\r', '')
+ value = value.split('\n')
+ for val in value:
+ if self._type in ('', None) and val in self.true_values:
+ val = '1'
+ valXML = ET.Element(valXMLName)
+ valXML.text = val
+ self.xml.append(valXML)
+ else:
+ if isinstance(value, list):
+ raise ValueError("Cannot add multiple values " + \
+ "to a %s field." % self._type)
+ valXML = ET.Element(valXMLName)
+ valXML.text = value
+ self.xml.append(valXML)
+
+
+class FieldOption(ElementBase):
+ namespace = 'jabber:x:data'
+ name = 'option'
+ plugin_attrib = 'option'
+ interfaces = set(('label', 'value'))
+ sub_interfaces = set(('value',))
+
+
+FormField.addOption = FormField.add_option
+FormField.delOptions = FormField.del_options
+FormField.delRequired = FormField.del_required
+FormField.delValue = FormField.del_value
+FormField.getAnswer = FormField.get_answer
+FormField.getOptions = FormField.get_options
+FormField.getRequired = FormField.get_required
+FormField.getValue = FormField.get_value
+FormField.setAnswer = FormField.set_answer
+FormField.setFalse = FormField.set_false
+FormField.setOptions = FormField.set_options
+FormField.setRequired = FormField.set_required
+FormField.setTrue = FormField.set_true
+FormField.setValue = FormField.set_value
diff --git a/sleekxmpp/plugins/xep_0004/stanza/form.py b/sleekxmpp/plugins/xep_0004/stanza/form.py
new file mode 100644
index 00000000..bbf0ee7d
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0004/stanza/form.py
@@ -0,0 +1,254 @@
+"""
+ 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 copy
+import logging
+
+from sleekxmpp.thirdparty import OrderedDict
+
+from sleekxmpp.xmlstream import ElementBase, ET
+from sleekxmpp.plugins.xep_0004.stanza import FormField
+
+
+log = logging.getLogger(__name__)
+
+
+class Form(ElementBase):
+ namespace = 'jabber:x:data'
+ name = 'x'
+ plugin_attrib = 'form'
+ interfaces = set(('fields', 'instructions', 'items',
+ 'reported', 'title', 'type', 'values'))
+ sub_interfaces = set(('title',))
+ form_types = set(('cancel', 'form', 'result', 'submit'))
+
+ def __init__(self, *args, **kwargs):
+ title = None
+ if 'title' in kwargs:
+ title = kwargs['title']
+ del kwargs['title']
+ ElementBase.__init__(self, *args, **kwargs)
+ if title is not None:
+ self['title'] = title
+
+ def setup(self, xml=None):
+ if ElementBase.setup(self, xml):
+ # If we had to generate xml
+ self['type'] = 'form'
+
+ @property
+ def field(self):
+ return self['fields']
+
+ def set_type(self, ftype):
+ self._set_attr('type', ftype)
+ if ftype == 'submit':
+ fields = self['fields']
+ for var in fields:
+ field = fields[var]
+ del field['type']
+ del field['label']
+ del field['desc']
+ del field['required']
+ del field['options']
+ elif ftype == 'cancel':
+ del self['fields']
+
+ def add_field(self, var='', ftype=None, label='', desc='',
+ required=False, value=None, options=None, **kwargs):
+ kwtype = kwargs.get('type', None)
+ if kwtype is None:
+ kwtype = ftype
+
+ field = FormField(parent=self)
+ field['var'] = var
+ field['type'] = kwtype
+ field['value'] = value
+ if self['type'] in ('form', 'result'):
+ field['label'] = label
+ field['desc'] = desc
+ field['required'] = required
+ if options is not None:
+ field['options'] = options
+ else:
+ del field['type']
+ return field
+
+ def getXML(self, type='submit'):
+ self['type'] = type
+ log.warning("Form.getXML() is deprecated API compatibility " + \
+ "with plugins/old_0004.py")
+ return self.xml
+
+ def fromXML(self, xml):
+ log.warning("Form.fromXML() is deprecated API compatibility " + \
+ "with plugins/old_0004.py")
+ n = Form(xml=xml)
+ return n
+
+ def add_item(self, values):
+ itemXML = ET.Element('{%s}item' % self.namespace)
+ self.xml.append(itemXML)
+ reported_vars = self['reported'].keys()
+ for var in reported_vars:
+ field = FormField()
+ field._type = self['reported'][var]['type']
+ field['var'] = var
+ field['value'] = values.get(var, None)
+ itemXML.append(field.xml)
+
+ def add_reported(self, var, ftype=None, label='', desc='', **kwargs):
+ kwtype = kwargs.get('type', None)
+ if kwtype is None:
+ kwtype = ftype
+ reported = self.xml.find('{%s}reported' % self.namespace)
+ if reported is None:
+ reported = ET.Element('{%s}reported' % self.namespace)
+ self.xml.append(reported)
+ fieldXML = ET.Element('{%s}field' % FormField.namespace)
+ reported.append(fieldXML)
+ field = FormField(xml=fieldXML)
+ field['var'] = var
+ field['type'] = kwtype
+ field['label'] = label
+ field['desc'] = desc
+ return field
+
+ def cancel(self):
+ self['type'] = 'cancel'
+
+ def del_fields(self):
+ fieldsXML = self.xml.findall('{%s}field' % FormField.namespace)
+ for fieldXML in fieldsXML:
+ self.xml.remove(fieldXML)
+
+ def del_instructions(self):
+ instsXML = self.xml.findall('{%s}instructions')
+ for instXML in instsXML:
+ self.xml.remove(instXML)
+
+ def del_items(self):
+ itemsXML = self.xml.find('{%s}item' % self.namespace)
+ for itemXML in itemsXML:
+ self.xml.remove(itemXML)
+
+ def del_reported(self):
+ reportedXML = self.xml.find('{%s}reported' % self.namespace)
+ if reportedXML is not None:
+ self.xml.remove(reportedXML)
+
+ def get_fields(self, use_dict=False):
+ fields = OrderedDict()
+ fieldsXML = self.xml.findall('{%s}field' % FormField.namespace)
+ for fieldXML in fieldsXML:
+ field = FormField(xml=fieldXML)
+ fields[field['var']] = field
+ return fields
+
+ def get_instructions(self):
+ instructions = ''
+ instsXML = self.xml.findall('{%s}instructions' % self.namespace)
+ return "\n".join([instXML.text for instXML in instsXML])
+
+ def get_items(self):
+ items = []
+ itemsXML = self.xml.findall('{%s}item' % self.namespace)
+ for itemXML in itemsXML:
+ item = OrderedDict()
+ fieldsXML = itemXML.findall('{%s}field' % FormField.namespace)
+ for fieldXML in fieldsXML:
+ field = FormField(xml=fieldXML)
+ item[field['var']] = field['value']
+ items.append(item)
+ return items
+
+ def get_reported(self):
+ fields = OrderedDict()
+ xml = self.xml.findall('{%s}reported/{%s}field' % (self.namespace,
+ FormField.namespace))
+ for field in xml:
+ field = FormField(xml=field)
+ fields[field['var']] = field
+ return fields
+
+ def get_values(self):
+ values = OrderedDict()
+ fields = self['fields']
+ for var in fields:
+ values[var] = fields[var]['value']
+ return values
+
+ def reply(self):
+ if self['type'] == 'form':
+ self['type'] = 'submit'
+ elif self['type'] == 'submit':
+ self['type'] = 'result'
+
+ def set_fields(self, fields):
+ del self['fields']
+ if not isinstance(fields, list):
+ fields = fields.items()
+ for var, field in fields:
+ field['var'] = var
+ self.add_field(**field)
+
+ def set_instructions(self, instructions):
+ del self['instructions']
+ if instructions in [None, '']:
+ return
+ instructions = instructions.split('\n')
+ for instruction in instructions:
+ inst = ET.Element('{%s}instructions' % self.namespace)
+ inst.text = instruction
+ self.xml.append(inst)
+
+ def set_items(self, items):
+ for item in items:
+ self.add_item(item)
+
+ def set_reported(self, reported):
+ for var in reported:
+ field = reported[var]
+ field['var'] = var
+ self.add_reported(var, **field)
+
+ def set_values(self, values):
+ fields = self['fields']
+ for field in values:
+ fields[field]['value'] = values[field]
+
+ def merge(self, other):
+ new = copy.copy(self)
+ if type(other) == dict:
+ new['values'] = other
+ return new
+ nfields = new['fields']
+ ofields = other['fields']
+ nfields.update(ofields)
+ new['fields'] = nfields
+ return new
+
+
+Form.setType = Form.set_type
+Form.addField = Form.add_field
+Form.addItem = Form.add_item
+Form.addReported = Form.add_reported
+Form.delFields = Form.del_fields
+Form.delInstructions = Form.del_instructions
+Form.delItems = Form.del_items
+Form.delReported = Form.del_reported
+Form.getFields = Form.get_fields
+Form.getInstructions = Form.get_instructions
+Form.getItems = Form.get_items
+Form.getReported = Form.get_reported
+Form.getValues = Form.get_values
+Form.setFields = Form.set_fields
+Form.setInstructions = Form.set_instructions
+Form.setItems = Form.set_items
+Form.setReported = Form.set_reported
+Form.setValues = Form.set_values
diff --git a/sleekxmpp/plugins/xep_0009/__init__.py b/sleekxmpp/plugins/xep_0009/__init__.py
new file mode 100644
index 00000000..2cd14170
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0009/__init__.py
@@ -0,0 +1,11 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2011 Nathanael C. Fritz, Dann Martens (TOMOTON).
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+from sleekxmpp.plugins.xep_0009 import stanza
+from sleekxmpp.plugins.xep_0009.rpc import xep_0009
+from sleekxmpp.plugins.xep_0009.stanza import RPCQuery, MethodCall, MethodResponse
diff --git a/sleekxmpp/plugins/xep_0009/binding.py b/sleekxmpp/plugins/xep_0009/binding.py
new file mode 100644
index 00000000..b4395707
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0009/binding.py
@@ -0,0 +1,169 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2011 Nathanael C. Fritz, Dann Martens (TOMOTON).
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+from xml.etree import cElementTree as ET
+import base64
+import logging
+import time
+
+log = logging.getLogger(__name__)
+
+_namespace = 'jabber:iq:rpc'
+
+def fault2xml(fault):
+ value = dict()
+ value['faultCode'] = fault['code']
+ value['faultString'] = fault['string']
+ fault = ET.Element("fault", {'xmlns': _namespace})
+ fault.append(_py2xml((value)))
+ return fault
+
+def xml2fault(params):
+ vals = []
+ for value in params.findall('{%s}value' % _namespace):
+ vals.append(_xml2py(value))
+ fault = dict()
+ fault['code'] = vals[0]['faultCode']
+ fault['string'] = vals[0]['faultString']
+ return fault
+
+def py2xml(*args):
+ params = ET.Element("{%s}params" % _namespace)
+ for x in args:
+ param = ET.Element("{%s}param" % _namespace)
+ param.append(_py2xml(x))
+ params.append(param) #<params><param>...
+ return params
+
+def _py2xml(*args):
+ for x in args:
+ val = ET.Element("{%s}value" % _namespace)
+ if x is None:
+ nil = ET.Element("{%s}nil" % _namespace)
+ val.append(nil)
+ elif type(x) is int:
+ i4 = ET.Element("{%s}i4" % _namespace)
+ i4.text = str(x)
+ val.append(i4)
+ elif type(x) is bool:
+ boolean = ET.Element("{%s}boolean" % _namespace)
+ boolean.text = str(int(x))
+ val.append(boolean)
+ elif type(x) is str:
+ string = ET.Element("{%s}string" % _namespace)
+ string.text = x
+ val.append(string)
+ elif type(x) is float:
+ double = ET.Element("{%s}double" % _namespace)
+ double.text = str(x)
+ val.append(double)
+ elif type(x) is rpcbase64:
+ b64 = ET.Element("{%s}base64" % _namespace)
+ b64.text = x.encoded()
+ val.append(b64)
+ elif type(x) is rpctime:
+ iso = ET.Element("{%s}dateTime.iso8601" % _namespace)
+ iso.text = str(x)
+ val.append(iso)
+ elif type(x) in (list, tuple):
+ array = ET.Element("{%s}array" % _namespace)
+ data = ET.Element("{%s}data" % _namespace)
+ for y in x:
+ data.append(_py2xml(y))
+ array.append(data)
+ val.append(array)
+ elif type(x) is dict:
+ struct = ET.Element("{%s}struct" % _namespace)
+ for y in x.keys():
+ member = ET.Element("{%s}member" % _namespace)
+ name = ET.Element("{%s}name" % _namespace)
+ name.text = y
+ member.append(name)
+ member.append(_py2xml(x[y]))
+ struct.append(member)
+ val.append(struct)
+ return val
+
+def xml2py(params):
+ namespace = 'jabber:iq:rpc'
+ vals = []
+ for param in params.findall('{%s}param' % namespace):
+ vals.append(_xml2py(param.find('{%s}value' % namespace)))
+ return vals
+
+def _xml2py(value):
+ namespace = 'jabber:iq:rpc'
+ if value.find('{%s}nil' % namespace) is not None:
+ return None
+ if value.find('{%s}i4' % namespace) is not None:
+ return int(value.find('{%s}i4' % namespace).text)
+ if value.find('{%s}int' % namespace) is not None:
+ return int(value.find('{%s}int' % namespace).text)
+ if value.find('{%s}boolean' % namespace) is not None:
+ return bool(int(value.find('{%s}boolean' % namespace).text))
+ if value.find('{%s}string' % namespace) is not None:
+ return value.find('{%s}string' % namespace).text
+ if value.find('{%s}double' % namespace) is not None:
+ return float(value.find('{%s}double' % namespace).text)
+ if value.find('{%s}base64' % namespace) is not None:
+ return rpcbase64(value.find('{%s}base64' % namespace).text.encode())
+ if value.find('{%s}Base64' % namespace) is not None:
+ # Older versions of XEP-0009 used Base64
+ return rpcbase64(value.find('{%s}Base64' % namespace).text.encode())
+ if value.find('{%s}dateTime.iso8601' % namespace) is not None:
+ return rpctime(value.find('{%s}dateTime.iso8601' % namespace).text)
+ if value.find('{%s}struct' % namespace) is not None:
+ struct = {}
+ for member in value.find('{%s}struct' % namespace).findall('{%s}member' % namespace):
+ struct[member.find('{%s}name' % namespace).text] = _xml2py(member.find('{%s}value' % namespace))
+ return struct
+ if value.find('{%s}array' % namespace) is not None:
+ array = []
+ for val in value.find('{%s}array' % namespace).find('{%s}data' % namespace).findall('{%s}value' % namespace):
+ array.append(_xml2py(val))
+ return array
+ raise ValueError()
+
+
+
+class rpcbase64(object):
+
+ def __init__(self, data):
+ #base 64 encoded string
+ self.data = data
+
+ def decode(self):
+ return base64.b64decode(self.data)
+
+ def __str__(self):
+ return self.decode().decode()
+
+ def encoded(self):
+ return self.data.decode()
+
+
+
+class rpctime(object):
+
+ def __init__(self,data=None):
+ #assume string data is in iso format YYYYMMDDTHH:MM:SS
+ if type(data) is str:
+ self.timestamp = time.strptime(data,"%Y%m%dT%H:%M:%S")
+ elif type(data) is time.struct_time:
+ self.timestamp = data
+ elif data is None:
+ self.timestamp = time.gmtime()
+ else:
+ raise ValueError()
+
+ def iso8601(self):
+ #return a iso8601 string
+ return time.strftime("%Y%m%dT%H:%M:%S",self.timestamp)
+
+ def __str__(self):
+ return self.iso8601()
diff --git a/sleekxmpp/plugins/xep_0009/remote.py b/sleekxmpp/plugins/xep_0009/remote.py
new file mode 100644
index 00000000..8c08e8f3
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0009/remote.py
@@ -0,0 +1,742 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2011 Nathanael C. Fritz, Dann Martens (TOMOTON).
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+from binding import py2xml, xml2py, xml2fault, fault2xml
+from threading import RLock
+import abc
+import inspect
+import logging
+import sleekxmpp
+import sys
+import threading
+import traceback
+
+log = logging.getLogger(__name__)
+
+def _intercept(method, name, public):
+ def _resolver(instance, *args, **kwargs):
+ log.debug("Locally calling %s.%s with arguments %s.", instance.FQN(), method.__name__, args)
+ try:
+ value = method(instance, *args, **kwargs)
+ if value == NotImplemented:
+ raise InvocationException("Local handler does not implement %s.%s!" % (instance.FQN(), method.__name__))
+ return value
+ except InvocationException:
+ raise
+ except Exception as e:
+ raise InvocationException("A problem occured calling %s.%s!" % (instance.FQN(), method.__name__), e)
+ _resolver._rpc = public
+ _resolver._rpc_name = method.__name__ if name is None else name
+ return _resolver
+
+def remote(function_argument, public = True):
+ '''
+ Decorator for methods which are remotely callable. This decorator
+ works in conjunction with classes which extend ABC Endpoint.
+ Example:
+
+ @remote
+ def remote_method(arg1, arg2)
+
+ Arguments:
+ function_argument -- a stand-in for either the actual method
+ OR a new name (string) for the method. In that case the
+ method is considered mapped:
+ Example:
+
+ @remote("new_name")
+ def remote_method(arg1, arg2)
+
+ public -- A flag which indicates if this method should be part
+ of the known dictionary of remote methods. Defaults to True.
+ Example:
+
+ @remote(False)
+ def remote_method(arg1, arg2)
+
+ Note: renaming and revising (public vs. private) can be combined.
+ Example:
+
+ @remote("new_name", False)
+ def remote_method(arg1, arg2)
+ '''
+ if hasattr(function_argument, '__call__'):
+ return _intercept(function_argument, None, public)
+ else:
+ if not isinstance(function_argument, basestring):
+ if not isinstance(function_argument, bool):
+ raise Exception('Expected an RPC method name or visibility modifier!')
+ else:
+ def _wrap_revised(function):
+ function = _intercept(function, None, function_argument)
+ return function
+ return _wrap_revised
+ def _wrap_remapped(function):
+ function = _intercept(function, function_argument, public)
+ return function
+ return _wrap_remapped
+
+
+class ACL:
+ '''
+ An Access Control List (ACL) is a list of rules, which are evaluated
+ in order until a match is found. The policy of the matching rule
+ is then applied.
+
+ Rules are 3-tuples, consisting of a policy enumerated type, a JID
+ expression and a RCP resource expression.
+
+ Examples:
+ [ (ACL.ALLOW, '*', '*') ] allow everyone everything, no restrictions
+ [ (ACL.DENY, '*', '*') ] deny everyone everything, no restrictions
+ [ (ACL.ALLOW, 'test@xmpp.org/unit', 'test.*'),
+ (ACL.DENY, '*', '*') ] deny everyone everything, except named
+ JID, which is allowed access to endpoint 'test' only.
+
+ The use of wildcards is allowed in expressions, as follows:
+ '*' everyone, or everything (= all endpoints and methods)
+ 'test@xmpp.org/*' every JID regardless of JID resource
+ '*@xmpp.org/rpc' every JID from domain xmpp.org with JID res 'rpc'
+ 'frank@*' every 'frank', regardless of domain or JID res
+ 'system.*' all methods of endpoint 'system'
+ '*.reboot' all methods reboot regardless of endpoint
+ '''
+ ALLOW = True
+ DENY = False
+
+ @classmethod
+ def check(cls, rules, jid, resource):
+ if rules is None:
+ return cls.DENY # No rules means no access!
+ jid = str(jid) # Check the string representation of the JID.
+ if not jid:
+ return cls.DENY # Can't check an empty JID.
+ for rule in rules:
+ policy = cls._check(rule, jid, resource)
+ if policy is not None:
+ return policy
+ return cls.DENY # By default if not rule matches, deny access.
+
+ @classmethod
+ def _check(cls, rule, jid, resource):
+ if cls._match(jid, rule[1]) and cls._match(resource, rule[2]):
+ return rule[0]
+ else:
+ return None
+
+ @classmethod
+ def _next_token(cls, expression, index):
+ new_index = expression.find('*', index)
+ if new_index == 0:
+ return ''
+ else:
+ if new_index == -1:
+ return expression[index : ]
+ else:
+ return expression[index : new_index]
+
+ @classmethod
+ def _match(cls, value, expression):
+ #! print "_match [VALUE] %s [EXPR] %s" % (value, expression)
+ index = 0
+ position = 0
+ while index < len(expression):
+ token = cls._next_token(expression, index)
+ #! print "[TOKEN] '%s'" % token
+ size = len(token)
+ if size > 0:
+ token_index = value.find(token, position)
+ if token_index == -1:
+ return False
+ else:
+ #! print "[INDEX-OF] %s" % token_index
+ position = token_index + len(token)
+ pass
+ if size == 0:
+ index += 1
+ else:
+ index += size
+ #! print "index %s position %s" % (index, position)
+ return True
+
+ANY_ALL = [ (ACL.ALLOW, '*', '*') ]
+
+
+class RemoteException(Exception):
+ '''
+ Base exception for RPC. This exception is raised when a problem
+ occurs in the network layer.
+ '''
+
+ def __init__(self, message="", cause=None):
+ '''
+ Initializes a new RemoteException.
+
+ Arguments:
+ message -- The message accompanying this exception.
+ cause -- The underlying cause of this exception.
+ '''
+ self._message = message
+ self._cause = cause
+ pass
+
+ def __str__(self):
+ return repr(self._message)
+
+ def get_message(self):
+ return self._message
+
+ def get_cause(self):
+ return self._cause
+
+
+
+class InvocationException(RemoteException):
+ '''
+ Exception raised when a problem occurs during the remote invocation
+ of a method.
+ '''
+ pass
+
+
+
+class AuthorizationException(RemoteException):
+ '''
+ Exception raised when the caller is not authorized to invoke the
+ remote method.
+ '''
+ pass
+
+
+class TimeoutException(Exception):
+ '''
+ Exception raised when the synchronous execution of a method takes
+ longer than the given threshold because an underlying asynchronous
+ reply did not arrive in time.
+ '''
+ pass
+
+
+class Callback(object):
+ '''
+ A base class for callback handlers.
+ '''
+ __metaclass__ = abc.ABCMeta
+
+
+ @abc.abstractproperty
+ def set_value(self, value):
+ return NotImplemented
+
+ @abc.abstractproperty
+ def cancel_with_error(self, exception):
+ return NotImplemented
+
+
+class Future(Callback):
+ '''
+ Represents the result of an asynchronous computation.
+ '''
+
+ def __init__(self):
+ '''
+ Initializes a new Future.
+ '''
+ self._value = None
+ self._exception = None
+ self._event = threading.Event()
+ pass
+
+ def set_value(self, value):
+ '''
+ Sets the value of this Future. Once the value is set, a caller
+ blocked on get_value will be able to continue.
+ '''
+ self._value = value
+ self._event.set()
+
+ def get_value(self, timeout=None):
+ '''
+ Gets the value of this Future. This call will block until
+ the result is available, or until an optional timeout expires.
+ When this Future is cancelled with an error,
+
+ Arguments:
+ timeout -- The maximum waiting time to obtain the value.
+ '''
+ self._event.wait(timeout)
+ if self._exception:
+ raise self._exception
+ if not self._event.is_set():
+ raise TimeoutException
+ return self._value
+
+ def is_done(self):
+ '''
+ Returns true if a value has been returned.
+ '''
+ return self._event.is_set()
+
+ def cancel_with_error(self, exception):
+ '''
+ Cancels the Future because of an error. Once cancelled, a
+ caller blocked on get_value will be able to continue.
+ '''
+ self._exception = exception
+ self._event.set()
+
+
+
+class Endpoint(object):
+ '''
+ The Endpoint class is an abstract base class for all objects
+ participating in an RPC-enabled XMPP network.
+
+ A user subclassing this class is required to implement the method:
+ FQN(self)
+ where FQN stands for Fully Qualified Name, an unambiguous name
+ which specifies which object an RPC call refers to. It is the
+ first part in a RPC method name '<fqn>.<method>'.
+ '''
+ __metaclass__ = abc.ABCMeta
+
+
+ def __init__(self, session, target_jid):
+ '''
+ Initialize a new Endpoint. This constructor should never be
+ invoked by a user, instead it will be called by the factories
+ which instantiate the RPC-enabled objects, of which only
+ the classes are provided by the user.
+
+ Arguments:
+ session -- An RPC session instance.
+ target_jid -- the identity of the remote XMPP entity.
+ '''
+ self.session = session
+ self.target_jid = target_jid
+
+ @abc.abstractproperty
+ def FQN(self):
+ return NotImplemented
+
+ def get_methods(self):
+ '''
+ Returns a dictionary of all RPC method names provided by this
+ class. This method returns the actual method names as found
+ in the class definition which have been decorated with:
+
+ @remote
+ def some_rpc_method(arg1, arg2)
+
+
+ Unless:
+ (1) the name has been remapped, in which case the new
+ name will be returned.
+
+ @remote("new_name")
+ def some_rpc_method(arg1, arg2)
+
+ (2) the method is set to hidden
+
+ @remote(False)
+ def some_hidden_method(arg1, arg2)
+ '''
+ result = dict()
+ for function in dir(self):
+ test_attr = getattr(self, function, None)
+ try:
+ if test_attr._rpc:
+ result[test_attr._rpc_name] = test_attr
+ except Exception:
+ pass
+ return result
+
+
+
+class Proxy(Endpoint):
+ '''
+ Implementation of the Proxy pattern which is intended to wrap
+ around Endpoints in order to intercept calls, marshall them and
+ forward them to the remote object.
+ '''
+
+ def __init__(self, endpoint, callback = None):
+ '''
+ Initializes a new Proxy.
+
+ Arguments:
+ endpoint -- The endpoint which is proxified.
+ '''
+ self._endpoint = endpoint
+ self._callback = callback
+
+ def __getattribute__(self, name, *args):
+ if name in ('__dict__', '_endpoint', 'async', '_callback'):
+ return object.__getattribute__(self, name)
+ else:
+ attribute = self._endpoint.__getattribute__(name)
+ if hasattr(attribute, '__call__'):
+ try:
+ if attribute._rpc:
+ def _remote_call(*args, **kwargs):
+ log.debug("Remotely calling '%s.%s' with arguments %s.", self._endpoint.FQN(), attribute._rpc_name, args)
+ return self._endpoint.session._call_remote(self._endpoint.target_jid, "%s.%s" % (self._endpoint.FQN(), attribute._rpc_name), self._callback, *args, **kwargs)
+ return _remote_call
+ except:
+ pass # If the attribute doesn't exist, don't care!
+ return attribute
+
+ def async(self, callback):
+ return Proxy(self._endpoint, callback)
+
+ def get_endpoint(self):
+ '''
+ Returns the proxified endpoint.
+ '''
+ return self._endpoint
+
+ def FQN(self):
+ return self._endpoint.FQN()
+
+
+class JabberRPCEntry(object):
+
+
+ def __init__(self, endpoint_FQN, call):
+ self._endpoint_FQN = endpoint_FQN
+ self._call = call
+
+ def call_method(self, args):
+ return_value = self._call(*args)
+ if return_value is None:
+ return return_value
+ else:
+ return self._return(return_value)
+
+ def get_endpoint_FQN(self):
+ return self._endpoint_FQN
+
+ def _return(self, *args):
+ return args
+
+
+class RemoteSession(object):
+ '''
+ A context object for a Jabber-RPC session.
+ '''
+
+
+ def __init__(self, client, session_close_callback):
+ '''
+ Initializes a new RPC session.
+
+ Arguments:
+ client -- The SleekXMPP client associated with this session.
+ session_close_callback -- A callback called when the
+ session is closed.
+ '''
+ self._client = client
+ self._session_close_callback = session_close_callback
+ self._event = threading.Event()
+ self._entries = {}
+ self._callbacks = {}
+ self._acls = {}
+ self._lock = RLock()
+
+ def _wait(self):
+ self._event.wait()
+
+ def _notify(self, event):
+ log.debug("RPC Session as %s started.", self._client.boundjid.full)
+ self._client.sendPresence()
+ self._event.set()
+ pass
+
+ def _register_call(self, endpoint, method, name=None):
+ '''
+ Registers a method from an endpoint as remotely callable.
+ '''
+ if name is None:
+ name = method.__name__
+ key = "%s.%s" % (endpoint, name)
+ log.debug("Registering call handler for %s (%s).", key, method)
+ with self._lock:
+ if key in self._entries:
+ raise KeyError("A handler for %s has already been regisered!" % endpoint)
+ self._entries[key] = JabberRPCEntry(endpoint, method)
+ return key
+
+ def _register_acl(self, endpoint, acl):
+ log.debug("Registering ACL %s for endpoint %s.", repr(acl), endpoint)
+ with self._lock:
+ self._acls[endpoint] = acl
+
+ def _register_callback(self, pid, callback):
+ with self._lock:
+ self._callbacks[pid] = callback
+
+ def forget_callback(self, callback):
+ with self._lock:
+ pid = self._find_key(self._callbacks, callback)
+ if pid is not None:
+ del self._callback[pid]
+ else:
+ raise ValueError("Unknown callback!")
+ pass
+
+ def _find_key(self, dict, value):
+ """return the key of dictionary dic given the value"""
+ search = [k for k, v in dict.iteritems() if v == value]
+ if len(search) == 0:
+ return None
+ else:
+ return search[0]
+
+ def _unregister_call(self, key):
+ #removes the registered call
+ with self._lock:
+ if self._entries[key]:
+ del self._entries[key]
+ else:
+ raise ValueError()
+
+ def new_proxy(self, target_jid, endpoint_cls):
+ '''
+ Instantiates a new proxy object, which proxies to a remote
+ endpoint. This method uses a class reference without
+ constructor arguments to instantiate the proxy.
+
+ Arguments:
+ target_jid -- the XMPP entity ID hosting the endpoint.
+ endpoint_cls -- The remote (duck) type.
+ '''
+ try:
+ argspec = inspect.getargspec(endpoint_cls.__init__)
+ args = [None] * (len(argspec[0]) - 1)
+ result = endpoint_cls(*args)
+ Endpoint.__init__(result, self, target_jid)
+ return Proxy(result)
+ except:
+ traceback.print_exc(file=sys.stdout)
+
+ def new_handler(self, acl, handler_cls, *args, **kwargs):
+ '''
+ Instantiates a new handler object, which is called remotely
+ by others. The user can control the effect of the call by
+ implementing the remote method in the local endpoint class. The
+ returned reference can be called locally and will behave as a
+ regular instance.
+
+ Arguments:
+ acl -- Access control list (see ACL class)
+ handler_clss -- The local (duck) type.
+ *args -- Constructor arguments for the local type.
+ **kwargs -- Constructor keyworded arguments for the local
+ type.
+ '''
+ argspec = inspect.getargspec(handler_cls.__init__)
+ base_argspec = inspect.getargspec(Endpoint.__init__)
+ if(argspec == base_argspec):
+ result = handler_cls(self, self._client.boundjid.full)
+ else:
+ result = handler_cls(*args, **kwargs)
+ Endpoint.__init__(result, self, self._client.boundjid.full)
+ method_dict = result.get_methods()
+ for method_name, method in method_dict.iteritems():
+ #!!! self._client.plugin['xep_0009'].register_call(result.FQN(), method, method_name)
+ self._register_call(result.FQN(), method, method_name)
+ self._register_acl(result.FQN(), acl)
+ return result
+
+# def is_available(self, targetCls, pto):
+# return self._client.is_available(pto)
+
+ def _call_remote(self, pto, pmethod, callback, *arguments):
+ iq = self._client.plugin['xep_0009'].make_iq_method_call(pto, pmethod, py2xml(*arguments))
+ pid = iq['id']
+ if callback is None:
+ future = Future()
+ self._register_callback(pid, future)
+ iq.send()
+ return future.get_value(30)
+ else:
+ log.debug("[RemoteSession] _call_remote %s", callback)
+ self._register_callback(pid, callback)
+ iq.send()
+
+ def close(self):
+ '''
+ Closes this session.
+ '''
+ self._client.disconnect(False)
+ self._session_close_callback()
+
+ def _on_jabber_rpc_method_call(self, iq):
+ iq.enable('rpc_query')
+ params = iq['rpc_query']['method_call']['params']
+ args = xml2py(params)
+ pmethod = iq['rpc_query']['method_call']['method_name']
+ try:
+ with self._lock:
+ entry = self._entries[pmethod]
+ rules = self._acls[entry.get_endpoint_FQN()]
+ if ACL.check(rules, iq['from'], pmethod):
+ return_value = entry.call_method(args)
+ else:
+ raise AuthorizationException("Unauthorized access to %s from %s!" % (pmethod, iq['from']))
+ if return_value is None:
+ return_value = ()
+ response = self._client.plugin['xep_0009'].make_iq_method_response(iq['id'], iq['from'], py2xml(*return_value))
+ response.send()
+ except InvocationException as ie:
+ fault = dict()
+ fault['code'] = 500
+ fault['string'] = ie.get_message()
+ self._client.plugin['xep_0009']._send_fault(iq, fault2xml(fault))
+ except AuthorizationException as ae:
+ log.error(ae.get_message())
+ error = self._client.plugin['xep_0009']._forbidden(iq)
+ error.send()
+ except Exception as e:
+ if isinstance(e, KeyError):
+ log.error("No handler available for %s!", pmethod)
+ error = self._client.plugin['xep_0009']._item_not_found(iq)
+ else:
+ traceback.print_exc(file=sys.stderr)
+ log.error("An unexpected problem occurred invoking method %s!", pmethod)
+ error = self._client.plugin['xep_0009']._undefined_condition(iq)
+ #! print "[REMOTE.PY] _handle_remote_procedure_call AN ERROR SHOULD BE SENT NOW %s " % e
+ error.send()
+
+ def _on_jabber_rpc_method_response(self, iq):
+ iq.enable('rpc_query')
+ args = xml2py(iq['rpc_query']['method_response']['params'])
+ pid = iq['id']
+ with self._lock:
+ callback = self._callbacks[pid]
+ del self._callbacks[pid]
+ if(len(args) > 0):
+ callback.set_value(args[0])
+ else:
+ callback.set_value(None)
+ pass
+
+ def _on_jabber_rpc_method_response2(self, iq):
+ iq.enable('rpc_query')
+ if iq['rpc_query']['method_response']['fault'] is not None:
+ self._on_jabber_rpc_method_fault(iq)
+ else:
+ args = xml2py(iq['rpc_query']['method_response']['params'])
+ pid = iq['id']
+ with self._lock:
+ callback = self._callbacks[pid]
+ del self._callbacks[pid]
+ if(len(args) > 0):
+ callback.set_value(args[0])
+ else:
+ callback.set_value(None)
+ pass
+
+ def _on_jabber_rpc_method_fault(self, iq):
+ iq.enable('rpc_query')
+ fault = xml2fault(iq['rpc_query']['method_response']['fault'])
+ pid = iq['id']
+ with self._lock:
+ callback = self._callbacks[pid]
+ del self._callbacks[pid]
+ e = {
+ 500: InvocationException
+ }[fault['code']](fault['string'])
+ callback.cancel_with_error(e)
+
+ def _on_jabber_rpc_error(self, iq):
+ pid = iq['id']
+ pmethod = self._client.plugin['xep_0009']._extract_method(iq['rpc_query'])
+ code = iq['error']['code']
+ type = iq['error']['type']
+ condition = iq['error']['condition']
+ #! print("['REMOTE.PY']._BINDING_handle_remote_procedure_error -> ERROR! ERROR! ERROR! Condition is '%s'" % condition)
+ with self._lock:
+ callback = self._callbacks[pid]
+ del self._callbacks[pid]
+ e = {
+ 'item-not-found': RemoteException("No remote handler available for %s at %s!" % (pmethod, iq['from'])),
+ 'forbidden': AuthorizationException("Forbidden to invoke remote handler for %s at %s!" % (pmethod, iq['from'])),
+ 'undefined-condition': RemoteException("An unexpected problem occured trying to invoke %s at %s!" % (pmethod, iq['from'])),
+ }[condition]
+ if e is None:
+ RemoteException("An unexpected exception occurred at %s!" % iq['from'])
+ callback.cancel_with_error(e)
+
+
+class Remote(object):
+ '''
+ Bootstrap class for Jabber-RPC sessions. New sessions are openend
+ with an existing XMPP client, or one is instantiated on demand.
+ '''
+ _instance = None
+ _sessions = dict()
+ _lock = threading.RLock()
+
+ @classmethod
+ def new_session_with_client(cls, client, callback=None):
+ '''
+ Opens a new session with a given client.
+
+ Arguments:
+ client -- An XMPP client.
+ callback -- An optional callback which can be used to track
+ the starting state of the session.
+ '''
+ with Remote._lock:
+ if(client.boundjid.bare in cls._sessions):
+ raise RemoteException("There already is a session associated with these credentials!")
+ else:
+ cls._sessions[client.boundjid.bare] = client;
+ def _session_close_callback():
+ with Remote._lock:
+ del cls._sessions[client.boundjid.bare]
+ result = RemoteSession(client, _session_close_callback)
+ client.plugin['xep_0009'].xmpp.add_event_handler('jabber_rpc_method_call', result._on_jabber_rpc_method_call, threaded=True)
+ client.plugin['xep_0009'].xmpp.add_event_handler('jabber_rpc_method_response', result._on_jabber_rpc_method_response, threaded=True)
+ client.plugin['xep_0009'].xmpp.add_event_handler('jabber_rpc_method_fault', result._on_jabber_rpc_method_fault, threaded=True)
+ client.plugin['xep_0009'].xmpp.add_event_handler('jabber_rpc_error', result._on_jabber_rpc_error, threaded=True)
+ if callback is None:
+ start_event_handler = result._notify
+ else:
+ start_event_handler = callback
+ client.add_event_handler("session_start", start_event_handler)
+ if client.connect():
+ client.process(threaded=True)
+ else:
+ raise RemoteException("Could not connect to XMPP server!")
+ pass
+ if callback is None:
+ result._wait()
+ return result
+
+ @classmethod
+ def new_session(cls, jid, password, callback=None):
+ '''
+ Opens a new session and instantiates a new XMPP client.
+
+ Arguments:
+ jid -- The XMPP JID for logging in.
+ password -- The password for logging in.
+ callback -- An optional callback which can be used to track
+ the starting state of the session.
+ '''
+ client = sleekxmpp.ClientXMPP(jid, password)
+ #? Register plug-ins.
+ client.registerPlugin('xep_0004') # Data Forms
+ client.registerPlugin('xep_0009') # Jabber-RPC
+ client.registerPlugin('xep_0030') # Service Discovery
+ client.registerPlugin('xep_0060') # PubSub
+ client.registerPlugin('xep_0199') # XMPP Ping
+ return cls.new_session_with_client(client, callback)
+
diff --git a/sleekxmpp/plugins/xep_0009/rpc.py b/sleekxmpp/plugins/xep_0009/rpc.py
new file mode 100644
index 00000000..4f749f30
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0009/rpc.py
@@ -0,0 +1,221 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2011 Nathanael C. Fritz, Dann Martens (TOMOTON).
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+from sleekxmpp.plugins import base
+from sleekxmpp.plugins.xep_0009.stanza.RPC import RPCQuery, MethodCall, MethodResponse
+from sleekxmpp.stanza.iq import Iq
+from sleekxmpp.xmlstream.handler.callback import Callback
+from sleekxmpp.xmlstream.matcher.xpath import MatchXPath
+from sleekxmpp.xmlstream.stanzabase import register_stanza_plugin
+from xml.etree import cElementTree as ET
+import logging
+
+
+
+log = logging.getLogger(__name__)
+
+
+
+class xep_0009(base.base_plugin):
+
+ def plugin_init(self):
+ self.xep = '0009'
+ self.description = 'Jabber-RPC'
+ #self.stanza = sleekxmpp.plugins.xep_0009.stanza
+
+ register_stanza_plugin(Iq, RPCQuery)
+ register_stanza_plugin(RPCQuery, MethodCall)
+ register_stanza_plugin(RPCQuery, MethodResponse)
+
+ self.xmpp.registerHandler(
+ Callback('RPC Call', MatchXPath('{%s}iq/{%s}query/{%s}methodCall' % (self.xmpp.default_ns, RPCQuery.namespace, RPCQuery.namespace)),
+ self._handle_method_call)
+ )
+ self.xmpp.registerHandler(
+ Callback('RPC Call', MatchXPath('{%s}iq/{%s}query/{%s}methodResponse' % (self.xmpp.default_ns, RPCQuery.namespace, RPCQuery.namespace)),
+ self._handle_method_response)
+ )
+ self.xmpp.registerHandler(
+ Callback('RPC Call', MatchXPath('{%s}iq/{%s}error' % (self.xmpp.default_ns, self.xmpp.default_ns)),
+ self._handle_error)
+ )
+ self.xmpp.add_event_handler('jabber_rpc_method_call', self._on_jabber_rpc_method_call)
+ self.xmpp.add_event_handler('jabber_rpc_method_response', self._on_jabber_rpc_method_response)
+ self.xmpp.add_event_handler('jabber_rpc_method_fault', self._on_jabber_rpc_method_fault)
+ self.xmpp.add_event_handler('jabber_rpc_error', self._on_jabber_rpc_error)
+ self.xmpp.add_event_handler('error', self._handle_error)
+ #self.activeCalls = []
+
+ def post_init(self):
+ base.base_plugin.post_init(self)
+ self.xmpp.plugin['xep_0030'].add_feature('jabber:iq:rpc')
+ self.xmpp.plugin['xep_0030'].add_identity('automation','rpc')
+
+ def make_iq_method_call(self, pto, pmethod, params):
+ iq = self.xmpp.makeIqSet()
+ iq.attrib['to'] = pto
+ iq.attrib['from'] = self.xmpp.boundjid.full
+ iq.enable('rpc_query')
+ iq['rpc_query']['method_call']['method_name'] = pmethod
+ iq['rpc_query']['method_call']['params'] = params
+ return iq;
+
+ def make_iq_method_response(self, pid, pto, params):
+ iq = self.xmpp.makeIqResult(pid)
+ iq.attrib['to'] = pto
+ iq.attrib['from'] = self.xmpp.boundjid.full
+ iq.enable('rpc_query')
+ iq['rpc_query']['method_response']['params'] = params
+ return iq
+
+ def make_iq_method_response_fault(self, pid, pto, params):
+ iq = self.xmpp.makeIqResult(pid)
+ iq.attrib['to'] = pto
+ iq.attrib['from'] = self.xmpp.boundjid.full
+ iq.enable('rpc_query')
+ iq['rpc_query']['method_response']['params'] = None
+ iq['rpc_query']['method_response']['fault'] = params
+ return iq
+
+# def make_iq_method_error(self, pto, pid, pmethod, params, code, type, condition):
+# iq = self.xmpp.makeIqError(pid)
+# iq.attrib['to'] = pto
+# iq.attrib['from'] = self.xmpp.boundjid.full
+# iq['error']['code'] = code
+# iq['error']['type'] = type
+# iq['error']['condition'] = condition
+# iq['rpc_query']['method_call']['method_name'] = pmethod
+# iq['rpc_query']['method_call']['params'] = params
+# return iq
+
+ def _item_not_found(self, iq):
+ payload = iq.get_payload()
+ iq.reply().error().set_payload(payload);
+ iq['error']['code'] = '404'
+ iq['error']['type'] = 'cancel'
+ iq['error']['condition'] = 'item-not-found'
+ return iq
+
+ def _undefined_condition(self, iq):
+ payload = iq.get_payload()
+ iq.reply().error().set_payload(payload)
+ iq['error']['code'] = '500'
+ iq['error']['type'] = 'cancel'
+ iq['error']['condition'] = 'undefined-condition'
+ return iq
+
+ def _forbidden(self, iq):
+ payload = iq.get_payload()
+ iq.reply().error().set_payload(payload)
+ iq['error']['code'] = '403'
+ iq['error']['type'] = 'auth'
+ iq['error']['condition'] = 'forbidden'
+ return iq
+
+ def _recipient_unvailable(self, iq):
+ payload = iq.get_payload()
+ iq.reply().error().set_payload(payload)
+ iq['error']['code'] = '404'
+ iq['error']['type'] = 'wait'
+ iq['error']['condition'] = 'recipient-unavailable'
+ return iq
+
+ def _handle_method_call(self, iq):
+ type = iq['type']
+ if type == 'set':
+ log.debug("Incoming Jabber-RPC call from %s", iq['from'])
+ self.xmpp.event('jabber_rpc_method_call', iq)
+ else:
+ if type == 'error' and ['rpc_query'] is None:
+ self.handle_error(iq)
+ else:
+ log.debug("Incoming Jabber-RPC error from %s", iq['from'])
+ self.xmpp.event('jabber_rpc_error', iq)
+
+ def _handle_method_response(self, iq):
+ if iq['rpc_query']['method_response']['fault'] is not None:
+ log.debug("Incoming Jabber-RPC fault from %s", iq['from'])
+ #self._on_jabber_rpc_method_fault(iq)
+ self.xmpp.event('jabber_rpc_method_fault', iq)
+ else:
+ log.debug("Incoming Jabber-RPC response from %s", iq['from'])
+ self.xmpp.event('jabber_rpc_method_response', iq)
+
+ def _handle_error(self, iq):
+ print("['XEP-0009']._handle_error -> ERROR! Iq is '%s'" % iq)
+ print("#######################")
+ print("### NOT IMPLEMENTED ###")
+ print("#######################")
+
+ def _on_jabber_rpc_method_call(self, iq, forwarded=False):
+ """
+ A default handler for Jabber-RPC method call. If another
+ handler is registered, this one will defer and not run.
+
+ If this handler is called by your own custom handler with
+ forwarded set to True, then it will run as normal.
+ """
+ if not forwarded and self.xmpp.event_handled('jabber_rpc_method_call') > 1:
+ return
+ # Reply with error by default
+ error = self.client.plugin['xep_0009']._item_not_found(iq)
+ error.send()
+
+ def _on_jabber_rpc_method_response(self, iq, forwarded=False):
+ """
+ A default handler for Jabber-RPC method response. If another
+ handler is registered, this one will defer and not run.
+
+ If this handler is called by your own custom handler with
+ forwarded set to True, then it will run as normal.
+ """
+ if not forwarded and self.xmpp.event_handled('jabber_rpc_method_response') > 1:
+ return
+ error = self.client.plugin['xep_0009']._recpient_unavailable(iq)
+ error.send()
+
+ def _on_jabber_rpc_method_fault(self, iq, forwarded=False):
+ """
+ A default handler for Jabber-RPC fault response. If another
+ handler is registered, this one will defer and not run.
+
+ If this handler is called by your own custom handler with
+ forwarded set to True, then it will run as normal.
+ """
+ if not forwarded and self.xmpp.event_handled('jabber_rpc_method_fault') > 1:
+ return
+ error = self.client.plugin['xep_0009']._recpient_unavailable(iq)
+ error.send()
+
+ def _on_jabber_rpc_error(self, iq, forwarded=False):
+ """
+ A default handler for Jabber-RPC error response. If another
+ handler is registered, this one will defer and not run.
+
+ If this handler is called by your own custom handler with
+ forwarded set to True, then it will run as normal.
+ """
+ if not forwarded and self.xmpp.event_handled('jabber_rpc_error') > 1:
+ return
+ error = self.client.plugin['xep_0009']._recpient_unavailable(iq, iq.get_payload())
+ error.send()
+
+ def _send_fault(self, iq, fault_xml): #
+ fault = self.make_iq_method_response_fault(iq['id'], iq['from'], fault_xml)
+ fault.send()
+
+ def _send_error(self, iq):
+ print("['XEP-0009']._send_error -> ERROR! Iq is '%s'" % iq)
+ print("#######################")
+ print("### NOT IMPLEMENTED ###")
+ print("#######################")
+
+ def _extract_method(self, stanza):
+ xml = ET.fromstring("%s" % stanza)
+ return xml.find("./methodCall/methodName").text
+
diff --git a/sleekxmpp/plugins/xep_0009/stanza/RPC.py b/sleekxmpp/plugins/xep_0009/stanza/RPC.py
new file mode 100644
index 00000000..3d1c77a2
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0009/stanza/RPC.py
@@ -0,0 +1,64 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2011 Nathanael C. Fritz, Dann Martens (TOMOTON).
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+from sleekxmpp.xmlstream.stanzabase import ElementBase
+from xml.etree import cElementTree as ET
+
+
+class RPCQuery(ElementBase):
+ name = 'query'
+ namespace = 'jabber:iq:rpc'
+ plugin_attrib = 'rpc_query'
+ interfaces = set(())
+ subinterfaces = set(())
+ plugin_attrib_map = {}
+ plugin_tag_map = {}
+
+
+class MethodCall(ElementBase):
+ name = 'methodCall'
+ namespace = 'jabber:iq:rpc'
+ plugin_attrib = 'method_call'
+ interfaces = set(('method_name', 'params'))
+ subinterfaces = set(())
+ plugin_attrib_map = {}
+ plugin_tag_map = {}
+
+ def get_method_name(self):
+ return self._get_sub_text('methodName')
+
+ def set_method_name(self, value):
+ return self._set_sub_text('methodName', value)
+
+ def get_params(self):
+ return self.xml.find('{%s}params' % self.namespace)
+
+ def set_params(self, params):
+ self.append(params)
+
+
+class MethodResponse(ElementBase):
+ name = 'methodResponse'
+ namespace = 'jabber:iq:rpc'
+ plugin_attrib = 'method_response'
+ interfaces = set(('params', 'fault'))
+ subinterfaces = set(())
+ plugin_attrib_map = {}
+ plugin_tag_map = {}
+
+ def get_params(self):
+ return self.xml.find('{%s}params' % self.namespace)
+
+ def set_params(self, params):
+ self.append(params)
+
+ def get_fault(self):
+ return self.xml.find('{%s}fault' % self.namespace)
+
+ def set_fault(self, fault):
+ self.append(fault)
diff --git a/sleekxmpp/plugins/xep_0009/stanza/__init__.py b/sleekxmpp/plugins/xep_0009/stanza/__init__.py
new file mode 100644
index 00000000..5dcbf330
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0009/stanza/__init__.py
@@ -0,0 +1,9 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2011 Nathanael C. Fritz, Dann Martens (TOMOTON).
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+from sleekxmpp.plugins.xep_0009.stanza.RPC import RPCQuery, MethodCall, MethodResponse
diff --git a/sleekxmpp/plugins/xep_0012.py b/sleekxmpp/plugins/xep_0012.py
new file mode 100644
index 00000000..c5532bd4
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0012.py
@@ -0,0 +1,115 @@
+"""
+ 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
+import logging
+
+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 LastActivity(ElementBase):
+ name = 'query'
+ namespace = 'jabber:iq:last'
+ plugin_attrib = 'last_activity'
+ interfaces = set(('seconds', 'status'))
+
+ def get_seconds(self):
+ return int(self._get_attr('seconds'))
+
+ def set_seconds(self, value):
+ self._set_attr('seconds', str(value))
+
+ def get_status(self):
+ return self.xml.text
+
+ def set_status(self, value):
+ self.xml.text = str(value)
+
+ def del_status(self):
+ self.xml.text = ''
+
+class xep_0012(base.base_plugin):
+ """
+ XEP-0012 Last Activity
+ """
+ def plugin_init(self):
+ self.description = "Last Activity"
+ self.xep = "0012"
+
+ self.xmpp.registerHandler(
+ Callback('Last Activity',
+ MatchXPath('{%s}iq/{%s}query' % (self.xmpp.default_ns,
+ LastActivity.namespace)),
+ self.handle_last_activity_query))
+ register_stanza_plugin(Iq, LastActivity)
+
+ self.xmpp.add_event_handler('last_activity_request', self.handle_last_activity)
+
+
+ def post_init(self):
+ base.base_plugin.post_init(self)
+ if self.xmpp.is_component:
+ # We are a component, so we track the uptime
+ self.xmpp.add_event_handler("session_start", self._reset_uptime)
+ self._start_datetime = datetime.now()
+ self.xmpp.plugin['xep_0030'].add_feature('jabber:iq:last')
+
+ def _reset_uptime(self, event):
+ self._start_datetime = datetime.now()
+
+ def handle_last_activity_query(self, iq):
+ if iq['type'] == 'get':
+ log.debug("Last activity requested by %s", iq['from'])
+ self.xmpp.event('last_activity_request', iq)
+ elif iq['type'] == 'result':
+ log.debug("Last activity result from %s", iq['from'])
+ self.xmpp.event('last_activity', iq)
+
+ def handle_last_activity(self, iq):
+ jid = iq['from']
+
+ if self.xmpp.is_component:
+ # Send the uptime
+ result = LastActivity()
+ td = (datetime.now() - self._start_datetime)
+ result['seconds'] = td.seconds + td.days * 24 * 3600
+ reply = iq.reply().setPayload(result.xml).send()
+ else:
+ barejid = JID(jid).bare
+ if barejid in self.xmpp.roster and ( self.xmpp.roster[barejid]['subscription'] in ('from', 'both') or
+ barejid == self.xmpp.boundjid.bare ):
+ # We don't know how to calculate it
+ iq.reply().error().setPayload(iq['last_activity'].xml)
+ iq['error']['code'] = '503'
+ iq['error']['type'] = 'cancel'
+ iq['error']['condition'] = 'service-unavailable'
+ iq.send()
+ else:
+ iq.reply().error().setPayload(iq['last_activity'].xml)
+ iq['error']['code'] = '403'
+ iq['error']['type'] = 'auth'
+ iq['error']['condition'] = 'forbidden'
+ iq.send()
+
+ def get_last_activity(self, jid):
+ """Query the LastActivity of jid and return it in seconds"""
+ iq = self.xmpp.makeIqGet()
+ query = LastActivity()
+ iq.append(query.xml)
+ iq.attrib['to'] = jid
+ iq.attrib['from'] = self.xmpp.boundjid.full
+ id = iq.get('id')
+ result = iq.send()
+ return result['last_activity']['seconds']
diff --git a/sleekxmpp/plugins/xep_0030/__init__.py b/sleekxmpp/plugins/xep_0030/__init__.py
new file mode 100644
index 00000000..2e183852
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0030/__init__.py
@@ -0,0 +1,12 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+from sleekxmpp.plugins.xep_0030 import stanza
+from sleekxmpp.plugins.xep_0030.stanza import DiscoInfo, DiscoItems
+from sleekxmpp.plugins.xep_0030.static import StaticDisco
+from sleekxmpp.plugins.xep_0030.disco import xep_0030
diff --git a/sleekxmpp/plugins/xep_0030/disco.py b/sleekxmpp/plugins/xep_0030/disco.py
new file mode 100644
index 00000000..2267401e
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0030/disco.py
@@ -0,0 +1,800 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+import logging
+
+import sleekxmpp
+from sleekxmpp import Iq
+from sleekxmpp.exceptions import XMPPError, IqError, IqTimeout
+from sleekxmpp.plugins.base import base_plugin
+from sleekxmpp.xmlstream.handler import Callback
+from sleekxmpp.xmlstream.matcher import StanzaPath
+from sleekxmpp.xmlstream import register_stanza_plugin, ElementBase, ET, JID
+from sleekxmpp.plugins.xep_0030 import DiscoInfo, DiscoItems, StaticDisco
+
+
+log = logging.getLogger(__name__)
+
+
+class xep_0030(base_plugin):
+
+ """
+ XEP-0030: Service Discovery
+
+ Service discovery in XMPP allows entities to discover information about
+ other agents in the network, such as the feature sets supported by a
+ client, or signposts to other, related entities.
+
+ Also see <http://www.xmpp.org/extensions/xep-0030.html>.
+
+ The XEP-0030 plugin works using a hierarchy of dynamic
+ node handlers, ranging from global handlers to specific
+ JID+node handlers. The default set of handlers operate
+ in a static manner, storing disco information in memory.
+ However, custom handlers may use any available backend
+ storage mechanism desired, such as SQLite or Redis.
+
+ Node handler hierarchy:
+ JID | Node | Level
+ ---------------------
+ None | None | Global
+ Given | None | All nodes for the JID
+ None | Given | Node on self.xmpp.boundjid
+ Given | Given | A single node
+
+ Stream Handlers:
+ Disco Info -- Any Iq stanze that includes a query with the
+ namespace http://jabber.org/protocol/disco#info.
+ Disco Items -- Any Iq stanze that includes a query with the
+ namespace http://jabber.org/protocol/disco#items.
+
+ Events:
+ disco_info -- Received a disco#info Iq query result.
+ disco_items -- Received a disco#items Iq query result.
+ disco_info_query -- Received a disco#info Iq query request.
+ disco_items_query -- Received a disco#items Iq query request.
+
+ Attributes:
+ stanza -- A reference to the module containing the
+ stanza classes provided by this plugin.
+ static -- Object containing the default set of
+ static node handlers.
+ default_handlers -- A dictionary mapping operations to the default
+ global handler (by default, the static handlers).
+ xmpp -- The main SleekXMPP object.
+
+ Methods:
+ set_node_handler -- Assign a handler to a JID/node combination.
+ del_node_handler -- Remove a handler from a JID/node combination.
+ get_info -- Retrieve disco#info data, locally or remote.
+ get_items -- Retrieve disco#items data, locally or remote.
+ set_identities --
+ set_features --
+ set_items --
+ del_items --
+ del_identity --
+ del_feature --
+ del_item --
+ add_identity --
+ add_feature --
+ add_item --
+ """
+
+ def plugin_init(self):
+ """
+ Start the XEP-0030 plugin.
+ """
+ self.xep = '0030'
+ self.description = 'Service Discovery'
+ self.stanza = sleekxmpp.plugins.xep_0030.stanza
+
+ self.xmpp.register_handler(
+ Callback('Disco Info',
+ StanzaPath('iq/disco_info'),
+ self._handle_disco_info))
+
+ self.xmpp.register_handler(
+ Callback('Disco Items',
+ StanzaPath('iq/disco_items'),
+ self._handle_disco_items))
+
+ register_stanza_plugin(Iq, DiscoInfo)
+ register_stanza_plugin(Iq, DiscoItems)
+
+ self.static = StaticDisco(self.xmpp, self)
+
+ self.use_cache = self.config.get('use_cache', True)
+ self.wrap_results = self.config.get('wrap_results', False)
+
+ self._disco_ops = [
+ 'get_info', 'set_info', 'set_identities', 'set_features',
+ 'get_items', 'set_items', 'del_items', 'add_identity',
+ 'del_identity', 'add_feature', 'del_feature', 'add_item',
+ 'del_item', 'del_identities', 'del_features', 'cache_info',
+ 'get_cached_info', 'supports', 'has_identity']
+
+ self.default_handlers = {}
+ self._handlers = {}
+ for op in self._disco_ops:
+ self._add_disco_op(op, getattr(self.static, op))
+
+ def post_init(self):
+ """Handle cross-plugin dependencies."""
+ base_plugin.post_init(self)
+ if 'xep_0059' in self.xmpp.plugin:
+ register_stanza_plugin(DiscoItems,
+ self.xmpp['xep_0059'].stanza.Set)
+
+ def _add_disco_op(self, op, default_handler):
+ self.default_handlers[op] = default_handler
+ self._handlers[op] = {'global': default_handler,
+ 'jid': {},
+ 'node': {}}
+
+ def set_node_handler(self, htype, jid=None, node=None, handler=None):
+ """
+ Add a node handler for the given hierarchy level and
+ handler type.
+
+ Node handlers are ordered in a hierarchy where the
+ most specific handler is executed. Thus, a fallback,
+ global handler can be used for the majority of cases
+ with a few node specific handler that override the
+ global behavior.
+
+ Node handler hierarchy:
+ JID | Node | Level
+ ---------------------
+ None | None | Global
+ Given | None | All nodes for the JID
+ None | Given | Node on self.xmpp.boundjid
+ Given | Given | A single node
+
+ Handler types:
+ get_info
+ get_items
+ set_identities
+ set_features
+ set_items
+ del_items
+ del_identities
+ del_identity
+ del_feature
+ del_features
+ del_item
+ add_identity
+ add_feature
+ add_item
+
+ Arguments:
+ htype -- The operation provided by the handler.
+ jid -- The JID the handler applies to. May be narrowed
+ further if a node is given.
+ node -- The particular node the handler is for. If no JID
+ is given, then the self.xmpp.boundjid.full is
+ assumed.
+ handler -- The handler function to use.
+ """
+ if htype not in self._disco_ops:
+ return
+ if jid is None and node is None:
+ self._handlers[htype]['global'] = handler
+ elif node is None:
+ self._handlers[htype]['jid'][jid] = handler
+ elif jid is None:
+ if self.xmpp.is_component:
+ jid = self.xmpp.boundjid.full
+ else:
+ jid = self.xmpp.boundjid.bare
+ self._handlers[htype]['node'][(jid, node)] = handler
+ else:
+ self._handlers[htype]['node'][(jid, node)] = handler
+
+ def del_node_handler(self, htype, jid, node):
+ """
+ Remove a handler type for a JID and node combination.
+
+ The next handler in the hierarchy will be used if one
+ exists. If removing the global handler, make sure that
+ other handlers exist to process existing nodes.
+
+ Node handler hierarchy:
+ JID | Node | Level
+ ---------------------
+ None | None | Global
+ Given | None | All nodes for the JID
+ None | Given | Node on self.xmpp.boundjid
+ Given | Given | A single node
+
+ Arguments:
+ htype -- The type of handler to remove.
+ jid -- The JID from which to remove the handler.
+ node -- The node from which to remove the handler.
+ """
+ self.set_node_handler(htype, jid, node, None)
+
+ def restore_defaults(self, jid=None, node=None, handlers=None):
+ """
+ Change all or some of a node's handlers to the default
+ handlers. Useful for manually overriding the contents
+ of a node that would otherwise be handled by a JID level
+ or global level dynamic handler.
+
+ The default is to use the built-in static handlers, but that
+ may be changed by modifying self.default_handlers.
+
+ Arguments:
+ jid -- The JID owning the node to modify.
+ node -- The node to change to using static handlers.
+ handlers -- Optional list of handlers to change to the
+ default version. If provided, only these
+ handlers will be changed. Otherwise, all
+ handlers will use the default version.
+ """
+ if handlers is None:
+ handlers = self._disco_ops
+ for op in handlers:
+ self.del_node_handler(op, jid, node)
+ self.set_node_handler(op, jid, node, self.default_handlers[op])
+
+ def supports(self, jid=None, node=None, feature=None, local=False,
+ cached=True, ifrom=None):
+ """
+ Check if a JID supports a given feature.
+
+ Return values:
+ True -- The feature is supported
+ False -- The feature is not listed as supported
+ None -- Nothing could be found due to a timeout
+
+ Arguments:
+ jid -- Request info from this JID.
+ node -- The particular node to query.
+ feature -- The name of the feature to check.
+ local -- If true, then the query is for a JID/node
+ combination handled by this Sleek instance and
+ no stanzas need to be sent.
+ Otherwise, a disco stanza must be sent to the
+ remove JID to retrieve the info.
+ cached -- If true, then look for the disco info data from
+ the local cache system. If no results are found,
+ send the query as usual. The self.use_cache
+ setting must be set to true for this option to
+ be useful. If set to false, then the cache will
+ be skipped, even if a result has already been
+ cached. Defaults to false.
+ ifrom -- Specifiy the sender's JID.
+ """
+ data = {'feature': feature,
+ 'local': local,
+ 'cached': cached}
+ return self._run_node_handler('supports', jid, node, ifrom, data)
+
+ def has_identity(self, jid=None, node=None, category=None, itype=None,
+ lang=None, local=False, cached=True, ifrom=None):
+ """
+ Check if a JID provides a given identity.
+
+ Return values:
+ True -- The identity is provided
+ False -- The identity is not listed
+ None -- Nothing could be found due to a timeout
+
+ Arguments:
+ jid -- Request info from this JID.
+ node -- The particular node to query.
+ category -- The category of the identity to check.
+ itype -- The type of the identity to check.
+ lang -- The language of the identity to check.
+ local -- If true, then the query is for a JID/node
+ combination handled by this Sleek instance and
+ no stanzas need to be sent.
+ Otherwise, a disco stanza must be sent to the
+ remove JID to retrieve the info.
+ cached -- If true, then look for the disco info data from
+ the local cache system. If no results are found,
+ send the query as usual. The self.use_cache
+ setting must be set to true for this option to
+ be useful. If set to false, then the cache will
+ be skipped, even if a result has already been
+ cached. Defaults to false.
+ ifrom -- Specifiy the sender's JID.
+ """
+ data = {'category': category,
+ 'itype': itype,
+ 'lang': lang,
+ 'local': local,
+ 'cached': cached}
+ return self._run_node_handler('has_identity', jid, node, ifrom, data)
+
+ def get_info(self, jid=None, node=None, local=False,
+ cached=None, **kwargs):
+ """
+ Retrieve the disco#info results from a given JID/node combination.
+
+ Info may be retrieved from both local resources and remote agents;
+ the local parameter indicates if the information should be gathered
+ by executing the local node handlers, or if a disco#info stanza
+ must be generated and sent.
+
+ If requesting items from a local JID/node, then only a DiscoInfo
+ stanza will be returned. Otherwise, an Iq stanza will be returned.
+
+ Arguments:
+ jid -- Request info from this JID.
+ node -- The particular node to query.
+ local -- If true, then the query is for a JID/node
+ combination handled by this Sleek instance and
+ no stanzas need to be sent.
+ Otherwise, a disco stanza must be sent to the
+ remove JID to retrieve the info.
+ cached -- If true, then look for the disco info data from
+ the local cache system. If no results are found,
+ send the query as usual. The self.use_cache
+ setting must be set to true for this option to
+ be useful. If set to false, then the cache will
+ be skipped, even if a result has already been
+ cached. Defaults to false.
+ 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. The
+ timeout value is only used when block=True.
+ callback -- Optional callback to execute when a reply is
+ received instead of blocking and waiting for
+ the reply.
+ """
+ if jid is not None and not isinstance(jid, JID):
+ jid = JID(jid)
+ if self.xmpp.is_component:
+ if jid.domain == self.xmpp.boundjid.domain:
+ local = True
+ else:
+ if str(jid) == str(self.xmpp.boundjid):
+ local = True
+ jid = jid.full
+
+ if local or jid in (None, ''):
+ log.debug("Looking up local disco#info data " + \
+ "for %s, node %s.", jid, node)
+ info = self._run_node_handler('get_info',
+ jid, node, kwargs.get('ifrom', None), kwargs)
+ info = self._fix_default_info(info)
+ return self._wrap(kwargs.get('ifrom', None), jid, info)
+
+ if cached:
+ log.debug("Looking up cached disco#info data " + \
+ "for %s, node %s.", jid, node)
+ info = self._run_node_handler('get_cached_info',
+ jid, node, kwargs.get('ifrom', None), kwargs)
+ if info is not None:
+ return self._wrap(kwargs.get('ifrom', None), jid, info)
+
+ iq = self.xmpp.Iq()
+ # Check dfrom parameter for backwards compatibility
+ iq['from'] = kwargs.get('ifrom', kwargs.get('dfrom', ''))
+ iq['to'] = jid
+ iq['type'] = 'get'
+ iq['disco_info']['node'] = node if node else ''
+ return iq.send(timeout=kwargs.get('timeout', None),
+ block=kwargs.get('block', True),
+ callback=kwargs.get('callback', None))
+
+ def set_info(self, jid=None, node=None, info=None):
+ """
+ Set the disco#info data for a JID/node based on an existing
+ disco#info stanza.
+ """
+ if isinstance(info, Iq):
+ info = info['disco_info']
+ self._run_node_handler('set_info', jid, node, None, info)
+
+ def get_items(self, jid=None, node=None, local=False, **kwargs):
+ """
+ Retrieve the disco#items results from a given JID/node combination.
+
+ Items may be retrieved from both local resources and remote agents;
+ the local parameter indicates if the items should be gathered by
+ executing the local node handlers, or if a disco#items stanza must
+ be generated and sent.
+
+ If requesting items from a local JID/node, then only a DiscoItems
+ stanza will be returned. Otherwise, an Iq stanza will be returned.
+
+ Arguments:
+ jid -- Request info from this JID.
+ node -- The particular node to query.
+ local -- If true, then the query is for a JID/node
+ combination handled by this Sleek instance and
+ no stanzas need to be sent.
+ Otherwise, a disco stanza must be sent to the
+ remove JID to retrieve the items.
+ 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.
+ iterator -- If True, return a result set iterator using
+ the XEP-0059 plugin, if the plugin is loaded.
+ Otherwise the parameter is ignored.
+ """
+ if local or jid is None:
+ items = self._run_node_handler('get_items',
+ jid, node, kwargs.get('ifrom', None), kwargs)
+ return self._wrap(kwargs.get('ifrom', None), jid, items)
+
+ iq = self.xmpp.Iq()
+ # Check dfrom parameter for backwards compatibility
+ iq['from'] = kwargs.get('ifrom', kwargs.get('dfrom', ''))
+ iq['to'] = jid
+ iq['type'] = 'get'
+ iq['disco_items']['node'] = node if node else ''
+ if kwargs.get('iterator', False) and self.xmpp['xep_0059']:
+ return self.xmpp['xep_0059'].iterate(iq, 'disco_items')
+ else:
+ return iq.send(timeout=kwargs.get('timeout', None),
+ block=kwargs.get('block', True),
+ callback=kwargs.get('callback', None))
+
+ def set_items(self, jid=None, node=None, **kwargs):
+ """
+ Set or replace all items for the specified JID/node combination.
+
+ The given items must be in a list or set where each item is a
+ tuple of the form: (jid, node, name).
+
+ Arguments:
+ jid -- The JID to modify.
+ node -- Optional node to modify.
+ items -- A series of items in tuple format.
+ """
+ self._run_node_handler('set_items', jid, node, None, kwargs)
+
+ def del_items(self, jid=None, node=None, **kwargs):
+ """
+ Remove all items from the given JID/node combination.
+
+ Arguments:
+ jid -- The JID to modify.
+ node -- Optional node to modify.
+ """
+ self._run_node_handler('del_items', jid, node, None, kwargs)
+
+ def add_item(self, jid='', name='', node=None, subnode='', ijid=None):
+ """
+ Add a new item element to the given JID/node combination.
+
+ Each item is required to have a JID, but may also specify
+ a node value to reference non-addressable entities.
+
+ Arguments:
+ jid -- The JID for the item.
+ name -- Optional name for the item.
+ node -- The node to modify.
+ subnode -- Optional node for the item.
+ ijid -- The JID to modify.
+ """
+ if not jid:
+ jid = self.xmpp.boundjid.full
+ kwargs = {'ijid': jid,
+ 'name': name,
+ 'inode': subnode}
+ self._run_node_handler('add_item', ijid, node, None, kwargs)
+
+ def del_item(self, jid=None, node=None, **kwargs):
+ """
+ Remove a single item from the given JID/node combination.
+
+ Arguments:
+ jid -- The JID to modify.
+ node -- The node to modify.
+ ijid -- The item's JID.
+ inode -- The item's node.
+ """
+ self._run_node_handler('del_item', jid, node, None, kwargs)
+
+ def add_identity(self, category='', itype='', name='',
+ node=None, jid=None, lang=None):
+ """
+ Add a new identity to the given JID/node combination.
+
+ Each identity must be unique in terms of all four identity
+ components: category, type, name, and language.
+
+ Multiple, identical category/type pairs are allowed only
+ if the xml:lang values are different. Likewise, multiple
+ category/type/xml:lang pairs are allowed so long as the
+ names are different. A category and type is always required.
+
+ Arguments:
+ category -- The identity's category.
+ itype -- The identity's type.
+ name -- Optional name for the identity.
+ lang -- Optional two-letter language code.
+ node -- The node to modify.
+ jid -- The JID to modify.
+ """
+ kwargs = {'category': category,
+ 'itype': itype,
+ 'name': name,
+ 'lang': lang}
+ self._run_node_handler('add_identity', jid, node, None, kwargs)
+
+ def add_feature(self, feature, node=None, jid=None):
+ """
+ Add a feature to a JID/node combination.
+
+ Arguments:
+ feature -- The namespace of the supported feature.
+ node -- The node to modify.
+ jid -- The JID to modify.
+ """
+ kwargs = {'feature': feature}
+ self._run_node_handler('add_feature', jid, node, None, kwargs)
+
+ def del_identity(self, jid=None, node=None, **kwargs):
+ """
+ Remove an identity from the given JID/node combination.
+
+ Arguments:
+ jid -- The JID to modify.
+ node -- The node to modify.
+ category -- The identity's category.
+ itype -- The identity's type value.
+ name -- Optional, human readable name for the identity.
+ lang -- Optional, the identity's xml:lang value.
+ """
+ self._run_node_handler('del_identity', jid, node, None, kwargs)
+
+ def del_feature(self, jid=None, node=None, **kwargs):
+ """
+ Remove a feature from a given JID/node combination.
+
+ Arguments:
+ jid -- The JID to modify.
+ node -- The node to modify.
+ feature -- The feature's namespace.
+ """
+ self._run_node_handler('del_feature', jid, node, None, kwargs)
+
+ def set_identities(self, jid=None, node=None, **kwargs):
+ """
+ Add or replace all identities for the given JID/node combination.
+
+ The identities must be in a set where each identity is a tuple
+ of the form: (category, type, lang, name)
+
+ Arguments:
+ jid -- The JID to modify.
+ node -- The node to modify.
+ identities -- A set of identities in tuple form.
+ lang -- Optional, xml:lang value.
+ """
+ self._run_node_handler('set_identities', jid, node, None, kwargs)
+
+ def del_identities(self, jid=None, node=None, **kwargs):
+ """
+ Remove all identities for a JID/node combination.
+
+ If a language is specified, only identities using that
+ language will be removed.
+
+ Arguments:
+ jid -- The JID to modify.
+ node -- The node to modify.
+ lang -- Optional. If given, only remove identities
+ using this xml:lang value.
+ """
+ self._run_node_handler('del_identities', jid, node, None, kwargs)
+
+ def set_features(self, jid=None, node=None, **kwargs):
+ """
+ Add or replace the set of supported features
+ for a JID/node combination.
+
+ Arguments:
+ jid -- The JID to modify.
+ node -- The node to modify.
+ features -- The new set of supported features.
+ """
+ self._run_node_handler('set_features', jid, node, None, kwargs)
+
+ def del_features(self, jid=None, node=None, **kwargs):
+ """
+ Remove all features from a JID/node combination.
+
+ Arguments:
+ jid -- The JID to modify.
+ node -- The node to modify.
+ """
+ self._run_node_handler('del_features', jid, node, None, kwargs)
+
+ def _run_node_handler(self, htype, jid, node=None, ifrom=None, data={}):
+ """
+ Execute the most specific node handler for the given
+ JID/node combination.
+
+ Arguments:
+ htype -- The handler type to execute.
+ jid -- The JID requested.
+ node -- The node requested.
+ data -- Optional, custom data to pass to the handler.
+ """
+ if isinstance(jid, JID):
+ jid = jid.full
+
+ if jid in (None, ''):
+ if self.xmpp.is_component:
+ jid = self.xmpp.boundjid.full
+ else:
+ jid = self.xmpp.boundjid.bare
+ if node is None:
+ node = ''
+
+ try:
+ args = (jid, node, ifrom, data)
+ if self._handlers[htype]['node'].get((jid, node), False):
+ return self._handlers[htype]['node'][(jid, node)](*args)
+ elif self._handlers[htype]['jid'].get(jid, False):
+ return self._handlers[htype]['jid'][jid](*args)
+ elif self._handlers[htype]['global']:
+ return self._handlers[htype]['global'](*args)
+ else:
+ return None
+ except TypeError:
+ # To preserve backward compatibility, drop the ifrom parameter
+ # for existing handlers that don't understand it.
+ args = (jid, node, data)
+ if self._handlers[htype]['node'].get((jid, node), False):
+ return self._handlers[htype]['node'][(jid, node)](*args)
+ elif self._handlers[htype]['jid'].get(jid, False):
+ return self._handlers[htype]['jid'][jid](*args)
+ elif self._handlers[htype]['global']:
+ return self._handlers[htype]['global'](*args)
+ else:
+ return None
+
+ def _handle_disco_info(self, iq):
+ """
+ Process an incoming disco#info stanza. If it is a get
+ request, find and return the appropriate identities
+ and features. If it is an info result, fire the
+ disco_info event.
+
+ Arguments:
+ iq -- The incoming disco#items stanza.
+ """
+ if iq['type'] == 'get':
+ log.debug("Received disco info query from " + \
+ "<%s> to <%s>.", iq['from'], iq['to'])
+ if self.xmpp.is_component:
+ jid = iq['to'].full
+ else:
+ jid = iq['to'].bare
+ info = self._run_node_handler('get_info',
+ jid,
+ iq['disco_info']['node'],
+ iq['from'],
+ iq)
+ if isinstance(info, Iq):
+ info.send()
+ else:
+ iq.reply()
+ if info:
+ info = self._fix_default_info(info)
+ iq.set_payload(info.xml)
+ iq.send()
+ elif iq['type'] == 'result':
+ log.debug("Received disco info result from " + \
+ "<%s> to <%s>.", iq['from'], iq['to'])
+ if self.use_cache:
+ log.debug("Caching disco info result from " \
+ "<%s> to <%s>.", iq['from'], iq['to'])
+ if self.xmpp.is_component:
+ ito = iq['to'].full
+ else:
+ ito = None
+ self._run_node_handler('cache_info',
+ iq['from'].full,
+ iq['disco_info']['node'],
+ ito,
+ iq)
+ self.xmpp.event('disco_info', iq)
+
+ def _handle_disco_items(self, iq):
+ """
+ Process an incoming disco#items stanza. If it is a get
+ request, find and return the appropriate items. If it
+ is an items result, fire the disco_items event.
+
+ Arguments:
+ iq -- The incoming disco#items stanza.
+ """
+ if iq['type'] == 'get':
+ log.debug("Received disco items query from " + \
+ "<%s> to <%s>.", iq['from'], iq['to'])
+ if self.xmpp.is_component:
+ jid = iq['to'].full
+ else:
+ jid = iq['to'].bare
+ items = self._run_node_handler('get_items',
+ jid,
+ iq['disco_items']['node'],
+ iq['from'].full,
+ iq)
+ if isinstance(items, Iq):
+ items.send()
+ else:
+ iq.reply()
+ if items:
+ iq.set_payload(items.xml)
+ iq.send()
+ elif iq['type'] == 'result':
+ log.debug("Received disco items result from " + \
+ "%s to %s.", iq['from'], iq['to'])
+ self.xmpp.event('disco_items', iq)
+
+ def _fix_default_info(self, info):
+ """
+ Disco#info results for a JID are required to include at least
+ one identity and feature. As a default, if no other identity is
+ provided, SleekXMPP will use either the generic component or the
+ bot client identity. A the standard disco#info feature will also be
+ added if no features are provided.
+
+ Arguments:
+ info -- The disco#info quest (not the full Iq stanza) to modify.
+ """
+ result = info
+ if isinstance(info, Iq):
+ info = iq['disco_info']
+ if not info['node']:
+ if not info['identities']:
+ if self.xmpp.is_component:
+ log.debug("No identity found for this entity. " + \
+ "Using default component identity.")
+ info.add_identity('component', 'generic')
+ else:
+ log.debug("No identity found for this entity. " + \
+ "Using default client identity.")
+ info.add_identity('client', 'bot')
+ if not info['features']:
+ log.debug("No features found for this entity. " + \
+ "Using default disco#info feature.")
+ info.add_feature(info.namespace)
+ return result
+
+ def _wrap(self, ito, ifrom, payload, force=False):
+ """
+ Ensure that results are wrapped in an Iq stanza
+ if self.wrap_results has been set to True.
+
+ Arguments:
+ ito -- The JID to use as the 'to' value
+ ifrom -- The JID to use as the 'from' value
+ payload -- The disco data to wrap
+ force -- Force wrapping, regardless of self.wrap_results
+ """
+ if (force or self.wrap_results) and not isinstance(payload, Iq):
+ iq = self.xmpp.Iq()
+ # Since we're simulating a result, we have to treat
+ # the 'from' and 'to' values opposite the normal way.
+ iq['to'] = self.xmpp.boundjid if ito is None else ito
+ iq['from'] = self.xmpp.boundjid if ifrom is None else ifrom
+ iq['type'] = 'result'
+ iq.append(payload)
+ return iq
+ return payload
+
+
+# Retain some backwards compatibility
+xep_0030.getInfo = xep_0030.get_info
+xep_0030.getItems = xep_0030.get_items
+xep_0030.make_static = xep_0030.restore_defaults
diff --git a/sleekxmpp/plugins/xep_0030/stanza/__init__.py b/sleekxmpp/plugins/xep_0030/stanza/__init__.py
new file mode 100644
index 00000000..0d97cf3d
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0030/stanza/__init__.py
@@ -0,0 +1,10 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+from sleekxmpp.plugins.xep_0030.stanza.info import DiscoInfo
+from sleekxmpp.plugins.xep_0030.stanza.items import DiscoItems
diff --git a/sleekxmpp/plugins/xep_0030/stanza/info.py b/sleekxmpp/plugins/xep_0030/stanza/info.py
new file mode 100644
index 00000000..25d1d07f
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0030/stanza/info.py
@@ -0,0 +1,276 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2010 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 DiscoInfo(ElementBase):
+
+ """
+ XMPP allows for users and agents to find the identities and features
+ supported by other entities in the XMPP network through service discovery,
+ or "disco". In particular, the "disco#info" query type for <iq> stanzas is
+ used to request the list of identities and features offered by a JID.
+
+ An identity is a combination of a category and type, such as the 'client'
+ category with a type of 'pc' to indicate the agent is a human operated
+ client with a GUI, or a category of 'gateway' with a type of 'aim' to
+ identify the agent as a gateway for the legacy AIM protocol. See
+ <http://xmpp.org/registrar/disco-categories.html> for a full list of
+ accepted category and type combinations.
+
+ Features are simply a set of the namespaces that identify the supported
+ features. For example, a client that supports service discovery will
+ include the feature 'http://jabber.org/protocol/disco#info'.
+
+ Since clients and components may operate in several roles at once, identity
+ and feature information may be grouped into "nodes". If one were to write
+ all of the identities and features used by a client, then node names would
+ be like section headings.
+
+ Example disco#info stanzas:
+ <iq type="get">
+ <query xmlns="http://jabber.org/protocol/disco#info" />
+ </iq>
+
+ <iq type="result">
+ <query xmlns="http://jabber.org/protocol/disco#info">
+ <identity category="client" type="bot" name="SleekXMPP Bot" />
+ <feature var="http://jabber.org/protocol/disco#info" />
+ <feature var="jabber:x:data" />
+ <feature var="urn:xmpp:ping" />
+ </query>
+ </iq>
+
+ Stanza Interface:
+ node -- The name of the node to either
+ query or return info from.
+ identities -- A set of 4-tuples, where each tuple contains
+ the category, type, xml:lang, and name
+ of an identity.
+ features -- A set of namespaces for features.
+
+ Methods:
+ add_identity -- Add a new, single identity.
+ del_identity -- Remove a single identity.
+ get_identities -- Return all identities in tuple form.
+ set_identities -- Use multiple identities, each given in tuple form.
+ del_identities -- Remove all identities.
+ add_feature -- Add a single feature.
+ del_feature -- Remove a single feature.
+ get_features -- Return a list of all features.
+ set_features -- Use a given list of features.
+ del_features -- Remove all features.
+ """
+
+ name = 'query'
+ namespace = 'http://jabber.org/protocol/disco#info'
+ plugin_attrib = 'disco_info'
+ interfaces = set(('node', 'features', 'identities'))
+ lang_interfaces = set(('identities',))
+
+ # Cache identities and features
+ _identities = set()
+ _features = set()
+
+ def setup(self, xml=None):
+ """
+ Populate the stanza object using an optional XML object.
+
+ Overrides ElementBase.setup
+
+ Caches identity and feature information.
+
+ Arguments:
+ xml -- Use an existing XML object for the stanza's values.
+ """
+ ElementBase.setup(self, xml)
+
+ self._identities = set([id[0:3] for id in self['identities']])
+ self._features = self['features']
+
+ def add_identity(self, category, itype, name=None, lang=None):
+ """
+ Add a new identity element. Each identity must be unique
+ in terms of all four identity components.
+
+ Multiple, identical category/type pairs are allowed only
+ if the xml:lang values are different. Likewise, multiple
+ category/type/xml:lang pairs are allowed so long as the names
+ are different. In any case, a category and type are required.
+
+ Arguments:
+ category -- The general category to which the agent belongs.
+ itype -- A more specific designation with the category.
+ name -- Optional human readable name for this identity.
+ lang -- Optional standard xml:lang value.
+ """
+ identity = (category, itype, lang)
+ if identity not in self._identities:
+ self._identities.add(identity)
+ id_xml = ET.Element('{%s}identity' % self.namespace)
+ id_xml.attrib['category'] = category
+ id_xml.attrib['type'] = itype
+ if lang:
+ id_xml.attrib['{%s}lang' % self.xml_ns] = lang
+ if name:
+ id_xml.attrib['name'] = name
+ self.xml.append(id_xml)
+ return True
+ return False
+
+ def del_identity(self, category, itype, name=None, lang=None):
+ """
+ Remove a given identity.
+
+ Arguments:
+ category -- The general category to which the agent belonged.
+ itype -- A more specific designation with the category.
+ name -- Optional human readable name for this identity.
+ lang -- Optional, standard xml:lang value.
+ """
+ identity = (category, itype, lang)
+ if identity in self._identities:
+ self._identities.remove(identity)
+ for id_xml in self.findall('{%s}identity' % self.namespace):
+ id = (id_xml.attrib['category'],
+ id_xml.attrib['type'],
+ id_xml.attrib.get('{%s}lang' % self.xml_ns, None))
+ if id == identity:
+ self.xml.remove(id_xml)
+ return True
+ return False
+
+ def get_identities(self, lang=None, dedupe=True):
+ """
+ Return a set of all identities in tuple form as so:
+ (category, type, lang, name)
+
+ If a language was specified, only return identities using
+ that language.
+
+ Arguments:
+ lang -- Optional, standard xml:lang value.
+ dedupe -- If True, de-duplicate identities, otherwise
+ return a list of all identities.
+ """
+ if dedupe:
+ identities = set()
+ else:
+ identities = []
+ for id_xml in self.findall('{%s}identity' % self.namespace):
+ xml_lang = id_xml.attrib.get('{%s}lang' % self.xml_ns, None)
+ if lang is None or xml_lang == lang:
+ id = (id_xml.attrib['category'],
+ id_xml.attrib['type'],
+ id_xml.attrib.get('{%s}lang' % self.xml_ns, None),
+ id_xml.attrib.get('name', None))
+ if dedupe:
+ identities.add(id)
+ else:
+ identities.append(id)
+ return identities
+
+ def set_identities(self, identities, lang=None):
+ """
+ Add or replace all identities. The identities must be a in set
+ where each identity is a tuple of the form:
+ (category, type, lang, name)
+
+ If a language is specifified, any identities using that language
+ will be removed to be replaced with the given identities.
+
+ NOTE: An identity's language will not be changed regardless of
+ the value of lang.
+
+ Arguments:
+ identities -- A set of identities in tuple form.
+ lang -- Optional, standard xml:lang value.
+ """
+ self.del_identities(lang)
+ for identity in identities:
+ category, itype, lang, name = identity
+ self.add_identity(category, itype, name, lang)
+
+ def del_identities(self, lang=None):
+ """
+ Remove all identities. If a language was specified, only
+ remove identities using that language.
+
+ Arguments:
+ lang -- Optional, standard xml:lang value.
+ """
+ for id_xml in self.findall('{%s}identity' % self.namespace):
+ if lang is None:
+ self.xml.remove(id_xml)
+ elif id_xml.attrib.get('{%s}lang' % self.xml_ns, None) == lang:
+ self._identities.remove((
+ id_xml.attrib['category'],
+ id_xml.attrib['type'],
+ id_xml.attrib.get('{%s}lang' % self.xml_ns, None)))
+ self.xml.remove(id_xml)
+
+ def add_feature(self, feature):
+ """
+ Add a single, new feature.
+
+ Arguments:
+ feature -- The namespace of the supported feature.
+ """
+ if feature not in self._features:
+ self._features.add(feature)
+ feature_xml = ET.Element('{%s}feature' % self.namespace)
+ feature_xml.attrib['var'] = feature
+ self.xml.append(feature_xml)
+ return True
+ return False
+
+ def del_feature(self, feature):
+ """
+ Remove a single feature.
+
+ Arguments:
+ feature -- The namespace of the removed feature.
+ """
+ if feature in self._features:
+ self._features.remove(feature)
+ for feature_xml in self.findall('{%s}feature' % self.namespace):
+ if feature_xml.attrib['var'] == feature:
+ self.xml.remove(feature_xml)
+ return True
+ return False
+
+ def get_features(self, dedupe=True):
+ """Return the set of all supported features."""
+ if dedupe:
+ features = set()
+ else:
+ features = []
+ for feature_xml in self.findall('{%s}feature' % self.namespace):
+ if dedupe:
+ features.add(feature_xml.attrib['var'])
+ else:
+ features.append(feature_xml.attrib['var'])
+ return features
+
+ def set_features(self, features):
+ """
+ Add or replace the set of supported features.
+
+ Arguments:
+ features -- The new set of supported features.
+ """
+ self.del_features()
+ for feature in features:
+ self.add_feature(feature)
+
+ def del_features(self):
+ """Remove all features."""
+ self._features = set()
+ for feature_xml in self.findall('{%s}feature' % self.namespace):
+ self.xml.remove(feature_xml)
diff --git a/sleekxmpp/plugins/xep_0030/stanza/items.py b/sleekxmpp/plugins/xep_0030/stanza/items.py
new file mode 100644
index 00000000..a1fb819c
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0030/stanza/items.py
@@ -0,0 +1,136 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2010 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 DiscoItems(ElementBase):
+
+ """
+ Example disco#items stanzas:
+ <iq type="get">
+ <query xmlns="http://jabber.org/protocol/disco#items" />
+ </iq>
+
+ <iq type="result">
+ <query xmlns="http://jabber.org/protocol/disco#items">
+ <item jid="chat.example.com"
+ node="xmppdev"
+ name="XMPP Dev" />
+ <item jid="chat.example.com"
+ node="sleekdev"
+ name="SleekXMPP Dev" />
+ </query>
+ </iq>
+
+ Stanza Interface:
+ node -- The name of the node to either
+ query or return info from.
+ items -- A list of 3-tuples, where each tuple contains
+ the JID, node, and name of an item.
+
+ Methods:
+ add_item -- Add a single new item.
+ del_item -- Remove a single item.
+ get_items -- Return all items.
+ set_items -- Set or replace all items.
+ del_items -- Remove all items.
+ """
+
+ name = 'query'
+ namespace = 'http://jabber.org/protocol/disco#items'
+ plugin_attrib = 'disco_items'
+ interfaces = set(('node', 'items'))
+
+ # Cache items
+ _items = set()
+
+ def setup(self, xml=None):
+ """
+ Populate the stanza object using an optional XML object.
+
+ Overrides ElementBase.setup
+
+ Caches item information.
+
+ Arguments:
+ xml -- Use an existing XML object for the stanza's values.
+ """
+ ElementBase.setup(self, xml)
+ self._items = set([item[0:2] for item in self['items']])
+
+ def add_item(self, jid, node=None, name=None):
+ """
+ Add a new item element. Each item is required to have a
+ JID, but may also specify a node value to reference
+ non-addressable entitities.
+
+ Arguments:
+ jid -- The JID for the item.
+ node -- Optional additional information to reference
+ non-addressable items.
+ name -- Optional human readable name for the item.
+ """
+ if (jid, node) not in self._items:
+ self._items.add((jid, node))
+ item_xml = ET.Element('{%s}item' % self.namespace)
+ item_xml.attrib['jid'] = jid
+ if name:
+ item_xml.attrib['name'] = name
+ if node:
+ item_xml.attrib['node'] = node
+ self.xml.append(item_xml)
+ return True
+ return False
+
+ def del_item(self, jid, node=None):
+ """
+ Remove a single item.
+
+ Arguments:
+ jid -- JID of the item to remove.
+ node -- Optional extra identifying information.
+ """
+ if (jid, node) in self._items:
+ for item_xml in self.findall('{%s}item' % self.namespace):
+ item = (item_xml.attrib['jid'],
+ item_xml.attrib.get('node', None))
+ if item == (jid, node):
+ self.xml.remove(item_xml)
+ return True
+ return False
+
+ def get_items(self):
+ """Return all items."""
+ items = set()
+ for item_xml in self.findall('{%s}item' % self.namespace):
+ item = (item_xml.attrib['jid'],
+ item_xml.attrib.get('node'),
+ item_xml.attrib.get('name'))
+ items.add(item)
+ return items
+
+ def set_items(self, items):
+ """
+ Set or replace all items. The given items must be in a
+ list or set where each item is a tuple of the form:
+ (jid, node, name)
+
+ Arguments:
+ items -- A series of items in tuple format.
+ """
+ self.del_items()
+ for item in items:
+ jid, node, name = item
+ self.add_item(jid, node, name)
+
+ def del_items(self):
+ """Remove all items."""
+ self._items = set()
+ for item_xml in self.findall('{%s}item' % self.namespace):
+ self.xml.remove(item_xml)
diff --git a/sleekxmpp/plugins/xep_0030/static.py b/sleekxmpp/plugins/xep_0030/static.py
new file mode 100644
index 00000000..e0ac29c6
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0030/static.py
@@ -0,0 +1,441 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+import logging
+import threading
+
+import sleekxmpp
+from sleekxmpp import Iq
+from sleekxmpp.exceptions import XMPPError
+from sleekxmpp.plugins.base import base_plugin
+from sleekxmpp.xmlstream.handler import Callback
+from sleekxmpp.xmlstream.matcher import StanzaPath
+from sleekxmpp.xmlstream import register_stanza_plugin, ElementBase, ET, JID
+from sleekxmpp.plugins.xep_0030 import DiscoInfo, DiscoItems
+
+
+log = logging.getLogger(__name__)
+
+
+class StaticDisco(object):
+
+ """
+ While components will likely require fully dynamic handling
+ of service discovery information, most clients and simple bots
+ only need to manage a few disco nodes that will remain mostly
+ static.
+
+ StaticDisco provides a set of node handlers that will store
+ static sets of disco info and items in memory.
+
+ Attributes:
+ nodes -- A dictionary mapping (JID, node) tuples to a dict
+ containing a disco#info and a disco#items stanza.
+ xmpp -- The main SleekXMPP object.
+ """
+
+ def __init__(self, xmpp, disco):
+ """
+ Create a static disco interface. Sets of disco#info and
+ disco#items are maintained for every given JID and node
+ combination. These stanzas are used to store disco
+ information in memory without any additional processing.
+
+ Arguments:
+ xmpp -- The main SleekXMPP object.
+ """
+ self.nodes = {}
+ self.xmpp = xmpp
+ self.disco = disco
+ self.lock = threading.RLock()
+
+ def add_node(self, jid=None, node=None, ifrom=None):
+ """
+ Create a new set of stanzas for the provided
+ JID and node combination.
+
+ Arguments:
+ jid -- The JID that will own the new stanzas.
+ node -- The node that will own the new stanzas.
+ """
+ with self.lock:
+ if jid is None:
+ jid = self.xmpp.boundjid.full
+ if node is None:
+ node = ''
+ if ifrom is None:
+ ifrom = ''
+ if isinstance(ifrom, JID):
+ ifrom = ifrom.full
+ if (jid, node, ifrom) not in self.nodes:
+ self.nodes[(jid, node, ifrom)] = {'info': DiscoInfo(),
+ 'items': DiscoItems()}
+ self.nodes[(jid, node, ifrom)]['info']['node'] = node
+ self.nodes[(jid, node, ifrom)]['items']['node'] = node
+
+ def get_node(self, jid=None, node=None, ifrom=None):
+ with self.lock:
+ if jid is None:
+ jid = self.xmpp.boundjid.full
+ if node is None:
+ node = ''
+ if ifrom is None:
+ ifrom = ''
+ if isinstance(ifrom, JID):
+ ifrom = ifrom.full
+ if (jid, node, ifrom) not in self.nodes:
+ self.add_node(jid, node, ifrom)
+ return self.nodes[(jid, node, ifrom)]
+
+ def node_exists(self, jid=None, node=None, ifrom=None):
+ with self.lock:
+ if jid is None:
+ jid = self.xmpp.boundjid.full
+ if node is None:
+ node = ''
+ if ifrom is None:
+ ifrom = ''
+ if isinstance(ifrom, JID):
+ ifrom = ifrom.full
+ if (jid, node, ifrom) not in self.nodes:
+ return False
+ return True
+
+ # =================================================================
+ # Node Handlers
+ #
+ # Each handler accepts four arguments: jid, node, ifrom, and data.
+ # The jid and node parameters together determine the set of info
+ # and items stanzas that will be retrieved or added. Additionally,
+ # the ifrom value allows for cached results when results vary based
+ # on the requester's JID. The data parameter is a dictionary with
+ # additional parameters that will be passed to other calls.
+ #
+ # This implementation does not allow different responses based on
+ # the requester's JID, except for cached results. To do that,
+ # register a custom node handler.
+
+ def supports(self, jid, node, ifrom, data):
+ """
+ Check if a JID supports a given feature.
+
+ The data parameter may provide:
+ feature -- The feature to check for support.
+ local -- If true, then the query is for a JID/node
+ combination handled by this Sleek instance and
+ no stanzas need to be sent.
+ Otherwise, a disco stanza must be sent to the
+ remove JID to retrieve the info.
+ cached -- If true, then look for the disco info data from
+ the local cache system. If no results are found,
+ send the query as usual. The self.use_cache
+ setting must be set to true for this option to
+ be useful. If set to false, then the cache will
+ be skipped, even if a result has already been
+ cached. Defaults to false.
+ """
+ feature = data.get('feature', None)
+
+ data = {'local': data.get('local', False),
+ 'cached': data.get('cached', True)}
+
+ if not feature:
+ return False
+
+ try:
+ info = self.disco.get_info(jid=jid, node=node,
+ ifrom=ifrom, **data)
+ info = self.disco._wrap(ifrom, jid, info, True)
+ features = info['disco_info']['features']
+ return feature in features
+ except IqError:
+ return False
+ except IqTimeout:
+ return None
+
+ def has_identity(self, jid, node, ifrom, data):
+ """
+ Check if a JID has a given identity.
+
+ The data parameter may provide:
+ category -- The category of the identity to check.
+ itype -- The type of the identity to check.
+ lang -- The language of the identity to check.
+ local -- If true, then the query is for a JID/node
+ combination handled by this Sleek instance and
+ no stanzas need to be sent.
+ Otherwise, a disco stanza must be sent to the
+ remove JID to retrieve the info.
+ cached -- If true, then look for the disco info data from
+ the local cache system. If no results are found,
+ send the query as usual. The self.use_cache
+ setting must be set to true for this option to
+ be useful. If set to false, then the cache will
+ be skipped, even if a result has already been
+ cached. Defaults to false.
+ """
+ identity = (data.get('category', None),
+ data.get('itype', None),
+ data.get('lang', None))
+
+ data = {'local': data.get('local', False),
+ 'cached': data.get('cached', True)}
+
+ if node in (None, ''):
+ info = self.caps.get_caps(jid)
+ if info and identity in info['identities']:
+ return True
+
+ try:
+ info = self.disco.get_info(jid=jid, node=node,
+ ifrom=ifrom, **data)
+ info = self.disco._wrap(ifrom, jid, info, True)
+ trunc = lambda i: (i[0], i[1], i[2])
+ return identity in map(trunc, info['disco_info']['identities'])
+ except IqError:
+ return False
+ except IqTimeout:
+ return None
+
+
+ def get_info(self, jid, node, ifrom, data):
+ """
+ Return the stored info data for the requested JID/node combination.
+
+ The data parameter is not used.
+ """
+ with self.lock:
+ if not self.node_exists(jid, node):
+ if not node:
+ return DiscoInfo()
+ else:
+ raise XMPPError(condition='item-not-found')
+ else:
+ return self.get_node(jid, node)['info']
+
+ def set_info(self, jid, node, ifrom, data):
+ """
+ Set the entire info stanza for a JID/node at once.
+
+ The data parameter is a disco#info substanza.
+ """
+ with self.lock:
+ self.add_node(jid, node)
+ self.get_node(jid, node)['info'] = data
+
+ def del_info(self, jid, node, ifrom, data):
+ """
+ Reset the info stanza for a given JID/node combination.
+
+ The data parameter is not used.
+ """
+ with self.lock:
+ if self.node_exists(jid, node):
+ self.get_node(jid, node)['info'] = DiscoInfo()
+
+ def get_items(self, jid, node, ifrom, data):
+ """
+ Return the stored items data for the requested JID/node combination.
+
+ The data parameter is not used.
+ """
+ with self.lock:
+ if not self.node_exists(jid, node):
+ if not node:
+ return DiscoInfo()
+ else:
+ raise XMPPError(condition='item-not-found')
+ else:
+ return self.get_node(jid, node)['items']
+
+ def set_items(self, jid, node, ifrom, data):
+ """
+ Replace the stored items data for a JID/node combination.
+
+ The data parameter may provide:
+ items -- A set of items in tuple format.
+ """
+ with self.lock:
+ items = data.get('items', set())
+ self.add_node(jid, node)
+ self.get_node(jid, node)['items']['items'] = items
+
+ def del_items(self, jid, node, ifrom, data):
+ """
+ Reset the items stanza for a given JID/node combination.
+
+ The data parameter is not used.
+ """
+ with self.lock:
+ if self.node_exists(jid, node):
+ self.get_node(jid, node)['items'] = DiscoItems()
+
+ def add_identity(self, jid, node, ifrom, data):
+ """
+ Add a new identity to te JID/node combination.
+
+ The data parameter may provide:
+ category -- The general category to which the agent belongs.
+ itype -- A more specific designation with the category.
+ name -- Optional human readable name for this identity.
+ lang -- Optional standard xml:lang value.
+ """
+ with self.lock:
+ self.add_node(jid, node)
+ self.get_node(jid, node)['info'].add_identity(
+ data.get('category', ''),
+ data.get('itype', ''),
+ data.get('name', None),
+ data.get('lang', None))
+
+ def set_identities(self, jid, node, ifrom, data):
+ """
+ Add or replace all identities for a JID/node combination.
+
+ The data parameter should include:
+ identities -- A list of identities in tuple form:
+ (category, type, name, lang)
+ """
+ with self.lock:
+ identities = data.get('identities', set())
+ self.add_node(jid, node)
+ self.get_node(jid, node)['info']['identities'] = identities
+
+ def del_identity(self, jid, node, ifrom, data):
+ """
+ Remove an identity from a JID/node combination.
+
+ The data parameter may provide:
+ category -- The general category to which the agent belonged.
+ itype -- A more specific designation with the category.
+ name -- Optional human readable name for this identity.
+ lang -- Optional, standard xml:lang value.
+ """
+ with self.lock:
+ if self.node_exists(jid, node):
+ self.get_node(jid, node)['info'].del_identity(
+ data.get('category', ''),
+ data.get('itype', ''),
+ data.get('name', None),
+ data.get('lang', None))
+
+ def del_identities(self, jid, node, ifrom, data):
+ """
+ Remove all identities from a JID/node combination.
+
+ The data parameter is not used.
+ """
+ with self.lock:
+ if self.node_exists(jid, node):
+ del self.get_node(jid, node)['info']['identities']
+
+ def add_feature(self, jid, node, ifrom, data):
+ """
+ Add a feature to a JID/node combination.
+
+ The data parameter should include:
+ feature -- The namespace of the supported feature.
+ """
+ with self.lock:
+ self.add_node(jid, node)
+ self.get_node(jid, node)['info'].add_feature(data.get('feature', ''))
+
+ def set_features(self, jid, node, ifrom, data):
+ """
+ Add or replace all features for a JID/node combination.
+
+ The data parameter should include:
+ features -- The new set of supported features.
+ """
+ with self.lock:
+ features = data.get('features', set())
+ self.add_node(jid, node)
+ self.get_node(jid, node)['info']['features'] = features
+
+ def del_feature(self, jid, node, ifrom, data):
+ """
+ Remove a feature from a JID/node combination.
+
+ The data parameter should include:
+ feature -- The namespace of the removed feature.
+ """
+ with self.lock:
+ if self.node_exists(jid, node):
+ self.get_node(jid, node)['info'].del_feature(data.get('feature', ''))
+
+ def del_features(self, jid, node, ifrom, data):
+ """
+ Remove all features from a JID/node combination.
+
+ The data parameter is not used.
+ """
+ with self.lock:
+ if not self.node_exists(jid, node):
+ return
+ del self.get_node(jid, node)['info']['features']
+
+ def add_item(self, jid, node, ifrom, data):
+ """
+ Add an item to a JID/node combination.
+
+ The data parameter may include:
+ ijid -- The JID for the item.
+ inode -- Optional additional information to reference
+ non-addressable items.
+ name -- Optional human readable name for the item.
+ """
+ with self.lock:
+ self.add_node(jid, node)
+ self.get_node(jid, node)['items'].add_item(
+ data.get('ijid', ''),
+ node=data.get('inode', ''),
+ name=data.get('name', ''))
+
+ def del_item(self, jid, node, ifrom, data):
+ """
+ Remove an item from a JID/node combination.
+
+ The data parameter may include:
+ ijid -- JID of the item to remove.
+ inode -- Optional extra identifying information.
+ """
+ with self.lock:
+ if self.node_exists(jid, node):
+ self.get_node(jid, node)['items'].del_item(
+ data.get('ijid', ''),
+ node=data.get('inode', None))
+
+ def cache_info(self, jid, node, ifrom, data):
+ """
+ Cache disco information for an external JID.
+
+ The data parameter is the Iq result stanza
+ containing the disco info to cache, or
+ the disco#info substanza itself.
+ """
+ with self.lock:
+ if isinstance(data, Iq):
+ data = data['disco_info']
+
+ self.add_node(jid, node, ifrom)
+ self.get_node(jid, node, ifrom)['info'] = data
+
+ def get_cached_info(self, jid, node, ifrom, data):
+ """
+ Retrieve cached disco info data.
+
+ The data parameter is not used.
+ """
+ with self.lock:
+ if isinstance(jid, JID):
+ jid = jid.full
+
+ if not self.node_exists(jid, node, ifrom):
+ return None
+ else:
+ return self.get_node(jid, node, ifrom)['info']
diff --git a/sleekxmpp/plugins/xep_0033.py b/sleekxmpp/plugins/xep_0033.py
new file mode 100644
index 00000000..c0c4d89d
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0033.py
@@ -0,0 +1,161 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+import logging
+from . import base
+from .. xmlstream.handler.callback import Callback
+from .. xmlstream.matcher.xpath import MatchXPath
+from .. xmlstream.stanzabase import registerStanzaPlugin, ElementBase, ET, JID
+from .. stanza.message import Message
+
+
+class Addresses(ElementBase):
+ namespace = 'http://jabber.org/protocol/address'
+ name = 'addresses'
+ plugin_attrib = 'addresses'
+ interfaces = set(('addresses', 'bcc', 'cc', 'noreply', 'replyroom', 'replyto', 'to'))
+
+ def addAddress(self, atype='to', jid='', node='', uri='', desc='', delivered=False):
+ address = Address(parent=self)
+ address['type'] = atype
+ address['jid'] = jid
+ address['node'] = node
+ address['uri'] = uri
+ address['desc'] = desc
+ address['delivered'] = delivered
+ return address
+
+ def getAddresses(self, atype=None):
+ addresses = []
+ for addrXML in self.xml.findall('{%s}address' % Address.namespace):
+ # ElementTree 1.2.6 does not support [@attr='value'] in findall
+ if atype is None or addrXML.attrib.get('type') == atype:
+ addresses.append(Address(xml=addrXML, parent=None))
+ return addresses
+
+ def setAddresses(self, addresses, set_type=None):
+ self.delAddresses(set_type)
+ for addr in addresses:
+ addr = dict(addr)
+ # Remap 'type' to 'atype' to match the add method
+ if set_type is not None:
+ addr['type'] = set_type
+ curr_type = addr.get('type', None)
+ if curr_type is not None:
+ del addr['type']
+ addr['atype'] = curr_type
+ self.addAddress(**addr)
+
+ def delAddresses(self, atype=None):
+ if atype is None:
+ return
+ for addrXML in self.xml.findall('{%s}address' % Address.namespace):
+ # ElementTree 1.2.6 does not support [@attr='value'] in findall
+ if addrXML.attrib.get('type') == atype:
+ self.xml.remove(addrXML)
+
+ # --------------------------------------------------------------
+
+ def delBcc(self):
+ self.delAddresses('bcc')
+
+ def delCc(self):
+ self.delAddresses('cc')
+
+ def delNoreply(self):
+ self.delAddresses('noreply')
+
+ def delReplyroom(self):
+ self.delAddresses('replyroom')
+
+ def delReplyto(self):
+ self.delAddresses('replyto')
+
+ def delTo(self):
+ self.delAddresses('to')
+
+ # --------------------------------------------------------------
+
+ def getBcc(self):
+ return self.getAddresses('bcc')
+
+ def getCc(self):
+ return self.getAddresses('cc')
+
+ def getNoreply(self):
+ return self.getAddresses('noreply')
+
+ def getReplyroom(self):
+ return self.getAddresses('replyroom')
+
+ def getReplyto(self):
+ return self.getAddresses('replyto')
+
+ def getTo(self):
+ return self.getAddresses('to')
+
+ # --------------------------------------------------------------
+
+ def setBcc(self, addresses):
+ self.setAddresses(addresses, 'bcc')
+
+ def setCc(self, addresses):
+ self.setAddresses(addresses, 'cc')
+
+ def setNoreply(self, addresses):
+ self.setAddresses(addresses, 'noreply')
+
+ def setReplyroom(self, addresses):
+ self.setAddresses(addresses, 'replyroom')
+
+ def setReplyto(self, addresses):
+ self.setAddresses(addresses, 'replyto')
+
+ def setTo(self, addresses):
+ self.setAddresses(addresses, 'to')
+
+
+class Address(ElementBase):
+ namespace = 'http://jabber.org/protocol/address'
+ name = 'address'
+ plugin_attrib = 'address'
+ interfaces = set(('delivered', 'desc', 'jid', 'node', 'type', 'uri'))
+ address_types = set(('bcc', 'cc', 'noreply', 'replyroom', 'replyto', 'to'))
+
+ def getDelivered(self):
+ return self.xml.attrib.get('delivered', False)
+
+ def setDelivered(self, delivered):
+ if delivered:
+ self.xml.attrib['delivered'] = "true"
+ else:
+ del self['delivered']
+
+ def setUri(self, uri):
+ if uri:
+ del self['jid']
+ del self['node']
+ self.xml.attrib['uri'] = uri
+ elif 'uri' in self.xml.attrib:
+ del self.xml.attrib['uri']
+
+
+class xep_0033(base.base_plugin):
+ """
+ XEP-0033: Extended Stanza Addressing
+ """
+
+ def plugin_init(self):
+ self.xep = '0033'
+ self.description = 'Extended Stanza Addressing'
+
+ registerStanzaPlugin(Message, Addresses)
+
+ def post_init(self):
+ base.base_plugin.post_init(self)
+ self.xmpp.plugin['xep_0030'].add_feature(Addresses.namespace)
diff --git a/sleekxmpp/plugins/xep_0045.py b/sleekxmpp/plugins/xep_0045.py
new file mode 100644
index 00000000..ab3f750a
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0045.py
@@ -0,0 +1,376 @@
+"""
+ 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 . import base
+import logging
+from xml.etree import cElementTree as ET
+from .. xmlstream.stanzabase import registerStanzaPlugin, ElementBase, JID
+from .. stanza.presence import Presence
+from .. xmlstream.handler.callback import Callback
+from .. xmlstream.matcher.xpath import MatchXPath
+from .. xmlstream.matcher.xmlmask import MatchXMLMask
+from sleekxmpp.exceptions import IqError, IqTimeout
+
+
+log = logging.getLogger(__name__)
+
+
+class MUCPresence(ElementBase):
+ name = 'x'
+ namespace = 'http://jabber.org/protocol/muc#user'
+ plugin_attrib = 'muc'
+ interfaces = set(('affiliation', 'role', 'jid', 'nick', 'room'))
+ affiliations = set(('', ))
+ roles = set(('', ))
+
+ def getXMLItem(self):
+ item = self.xml.find('{http://jabber.org/protocol/muc#user}item')
+ if item is None:
+ item = ET.Element('{http://jabber.org/protocol/muc#user}item')
+ self.xml.append(item)
+ return item
+
+ def getAffiliation(self):
+ #TODO if no affilation, set it to the default and return default
+ item = self.getXMLItem()
+ return item.get('affiliation', '')
+
+ def setAffiliation(self, value):
+ item = self.getXMLItem()
+ #TODO check for valid affiliation
+ item.attrib['affiliation'] = value
+ return self
+
+ def delAffiliation(self):
+ item = self.getXMLItem()
+ #TODO set default affiliation
+ if 'affiliation' in item.attrib: del item.attrib['affiliation']
+ return self
+
+ def getJid(self):
+ item = self.getXMLItem()
+ return JID(item.get('jid', ''))
+
+ def setJid(self, value):
+ item = self.getXMLItem()
+ if not isinstance(value, str):
+ value = str(value)
+ item.attrib['jid'] = value
+ return self
+
+ def delJid(self):
+ item = self.getXMLItem()
+ if 'jid' in item.attrib: del item.attrib['jid']
+ return self
+
+ def getRole(self):
+ item = self.getXMLItem()
+ #TODO get default role, set default role if none
+ return item.get('role', '')
+
+ def setRole(self, value):
+ item = self.getXMLItem()
+ #TODO check for valid role
+ item.attrib['role'] = value
+ return self
+
+ def delRole(self):
+ item = self.getXMLItem()
+ #TODO set default role
+ if 'role' in item.attrib: del item.attrib['role']
+ return self
+
+ def getNick(self):
+ return self.parent()['from'].resource
+
+ def getRoom(self):
+ return self.parent()['from'].bare
+
+ def setNick(self, value):
+ log.warning("Cannot set nick through mucpresence plugin.")
+ return self
+
+ def setRoom(self, value):
+ log.warning("Cannot set room through mucpresence plugin.")
+ return self
+
+ def delNick(self):
+ log.warning("Cannot delete nick through mucpresence plugin.")
+ return self
+
+ def delRoom(self):
+ log.warning("Cannot delete room through mucpresence plugin.")
+ return self
+
+class xep_0045(base.base_plugin):
+ """
+ Implements XEP-0045 Multi User Chat
+ """
+
+ def plugin_init(self):
+ self.rooms = {}
+ self.ourNicks = {}
+ self.xep = '0045'
+ self.description = 'Multi User Chat'
+ # load MUC support in presence stanzas
+ registerStanzaPlugin(Presence, MUCPresence)
+ self.xmpp.registerHandler(Callback('MUCPresence', MatchXMLMask("<presence xmlns='%s' />" % self.xmpp.default_ns), self.handle_groupchat_presence))
+ self.xmpp.registerHandler(Callback('MUCMessage', MatchXMLMask("<message xmlns='%s' type='groupchat'><body/></message>" % self.xmpp.default_ns), self.handle_groupchat_message))
+ self.xmpp.registerHandler(Callback('MUCSubject', MatchXMLMask("<message xmlns='%s' type='groupchat'><subject/></message>" % self.xmpp.default_ns), self.handle_groupchat_subject))
+ self.xmpp.registerHandler(Callback('MUCInvite', MatchXPath("{%s}message/{http://jabber.org/protocol/muc#user}x/invite" % self.xmpp.default_ns), self.handle_groupchat_invite))
+
+ def handle_groupchat_invite(self, inv):
+ """ Handle an invite into a muc.
+ """
+ logging.debug("MUC invite to %s from %s: %s", inv['from'], inv["from"], inv)
+ if inv['from'] not in self.rooms.keys():
+ self.xmpp.event("groupchat_invite", inv)
+
+ def handle_groupchat_presence(self, pr):
+ """ Handle a presence in a muc.
+ """
+ got_offline = False
+ got_online = False
+ if pr['muc']['room'] not in self.rooms.keys():
+ return
+ entry = pr['muc'].getStanzaValues()
+ entry['show'] = pr['show']
+ entry['status'] = pr['status']
+ if pr['type'] == 'unavailable':
+ if entry['nick'] in self.rooms[entry['room']]:
+ del self.rooms[entry['room']][entry['nick']]
+ got_offline = True
+ else:
+ if entry['nick'] not in self.rooms[entry['room']]:
+ got_online = True
+ self.rooms[entry['room']][entry['nick']] = entry
+ log.debug("MUC presence from %s/%s : %s", entry['room'],entry['nick'], entry)
+ self.xmpp.event("groupchat_presence", pr)
+ self.xmpp.event("muc::%s::presence" % entry['room'], pr)
+ if got_offline:
+ self.xmpp.event("muc::%s::got_offline" % entry['room'], pr)
+ if got_online:
+ self.xmpp.event("muc::%s::got_online" % entry['room'], pr)
+
+ def handle_groupchat_message(self, msg):
+ """ Handle a message event in a muc.
+ """
+ self.xmpp.event('groupchat_message', msg)
+ self.xmpp.event("muc::%s::message" % msg['from'].bare, msg)
+
+ def handle_groupchat_subject(self, msg):
+ """ Handle a message coming from a muc indicating
+ a change of subject (or announcing it when joining the room)
+ """
+ self.xmpp.event('groupchat_subject', msg)
+
+ def jidInRoom(self, room, jid):
+ for nick in self.rooms[room]:
+ entry = self.rooms[room][nick]
+ if entry is not None and entry['jid'].full == jid:
+ return True
+ return False
+
+ def getNick(self, room, jid):
+ for nick in self.rooms[room]:
+ entry = self.rooms[room][nick]
+ if entry is not None and entry['jid'].full == jid:
+ return nick
+
+ def getRoomForm(self, room, ifrom=None):
+ iq = self.xmpp.makeIqGet()
+ iq['to'] = room
+ if ifrom is not None:
+ iq['from'] = ifrom
+ query = ET.Element('{http://jabber.org/protocol/muc#owner}query')
+ iq.append(query)
+ # For now, swallow errors to preserve existing API
+ try:
+ result = iq.send()
+ except IqError:
+ return False
+ except IqTimeout:
+ return False
+ xform = result.xml.find('{http://jabber.org/protocol/muc#owner}query/{jabber:x:data}x')
+ if xform is None: return False
+ form = self.xmpp.plugin['old_0004'].buildForm(xform)
+ return form
+
+ def configureRoom(self, room, form=None, ifrom=None):
+ if form is None:
+ form = self.getRoomForm(room, ifrom=ifrom)
+ #form = self.xmpp.plugin['old_0004'].makeForm(ftype='submit')
+ #form.addField('FORM_TYPE', value='http://jabber.org/protocol/muc#roomconfig')
+ iq = self.xmpp.makeIqSet()
+ iq['to'] = room
+ if ifrom is not None:
+ iq['from'] = ifrom
+ query = ET.Element('{http://jabber.org/protocol/muc#owner}query')
+ form = form.getXML('submit')
+ query.append(form)
+ iq.append(query)
+ # For now, swallow errors to preserve existing API
+ try:
+ result = iq.send()
+ except IqError:
+ return False
+ except IqTimeout:
+ return False
+ return True
+
+ def joinMUC(self, room, nick, maxhistory="0", password='', wait=False, pstatus=None, pshow=None, pfrom=None):
+ """ Join the specified room, requesting 'maxhistory' lines of history.
+ """
+ stanza = self.xmpp.makePresence(pto="%s/%s" % (room, nick), pstatus=pstatus, pshow=pshow, pfrom=pfrom)
+ x = ET.Element('{http://jabber.org/protocol/muc}x')
+ if password:
+ passelement = ET.Element('password')
+ passelement.text = password
+ x.append(passelement)
+ if maxhistory:
+ history = ET.Element('history')
+ if maxhistory == "0":
+ history.attrib['maxchars'] = maxhistory
+ else:
+ history.attrib['maxstanzas'] = maxhistory
+ x.append(history)
+ stanza.append(x)
+ if not wait:
+ self.xmpp.send(stanza)
+ else:
+ #wait for our own room presence back
+ expect = ET.Element("{%s}presence" % self.xmpp.default_ns, {'from':"%s/%s" % (room, nick)})
+ self.xmpp.send(stanza, expect)
+ self.rooms[room] = {}
+ self.ourNicks[room] = nick
+
+ def destroy(self, room, reason='', altroom = '', ifrom=None):
+ iq = self.xmpp.makeIqSet()
+ if ifrom is not None:
+ iq['from'] = ifrom
+ iq['to'] = room
+ query = ET.Element('{http://jabber.org/protocol/muc#owner}query')
+ destroy = ET.Element('destroy')
+ if altroom:
+ destroy.attrib['jid'] = altroom
+ xreason = ET.Element('reason')
+ xreason.text = reason
+ destroy.append(xreason)
+ query.append(destroy)
+ iq.append(query)
+ # For now, swallow errors to preserve existing API
+ try:
+ r = iq.send()
+ except IqError:
+ return False
+ except IqTimeout:
+ return False
+ return True
+
+ def setAffiliation(self, room, jid=None, nick=None, affiliation='member', ifrom=None):
+ """ Change room affiliation."""
+ if affiliation not in ('outcast', 'member', 'admin', 'owner', 'none'):
+ raise TypeError
+ query = ET.Element('{http://jabber.org/protocol/muc#admin}query')
+ if nick is not None:
+ item = ET.Element('item', {'affiliation':affiliation, 'nick':nick})
+ else:
+ item = ET.Element('item', {'affiliation':affiliation, 'jid':jid})
+ query.append(item)
+ iq = self.xmpp.makeIqSet(query)
+ iq['to'] = room
+ iq['from'] = ifrom
+ # For now, swallow errors to preserve existing API
+ try:
+ result = iq.send()
+ except IqError:
+ return False
+ except IqTimeout:
+ return False
+ return True
+
+ def invite(self, room, jid, reason='', mfrom=''):
+ """ Invite a jid to a room."""
+ msg = self.xmpp.makeMessage(room)
+ msg['from'] = mfrom
+ x = ET.Element('{http://jabber.org/protocol/muc#user}x')
+ invite = ET.Element('{http://jabber.org/protocol/muc#user}invite', {'to': jid})
+ if reason:
+ rxml = ET.Element('reason')
+ rxml.text = reason
+ invite.append(rxml)
+ x.append(invite)
+ msg.append(x)
+ self.xmpp.send(msg)
+
+ def leaveMUC(self, room, nick, msg='', pfrom=None):
+ """ Leave the specified room.
+ """
+ if msg:
+ self.xmpp.sendPresence(pshow='unavailable', pto="%s/%s" % (room, nick), pstatus=msg, pfrom=pfrom)
+ else:
+ self.xmpp.sendPresence(pshow='unavailable', pto="%s/%s" % (room, nick), pfrom=pfrom)
+ del self.rooms[room]
+
+ def getRoomConfig(self, room, ifrom=''):
+ iq = self.xmpp.makeIqGet('http://jabber.org/protocol/muc#owner')
+ iq['to'] = room
+ iq['from'] = ifrom
+ # For now, swallow errors to preserve existing API
+ try:
+ result = iq.send()
+ except IqError:
+ raise ValueError
+ except IqTimeout:
+ raise ValueError
+ form = result.xml.find('{http://jabber.org/protocol/muc#owner}query/{jabber:x:data}x')
+ if form is None:
+ raise ValueError
+ return self.xmpp.plugin['xep_0004'].buildForm(form)
+
+ def cancelConfig(self, room, ifrom=None):
+ query = ET.Element('{http://jabber.org/protocol/muc#owner}query')
+ x = ET.Element('{jabber:x:data}x', type='cancel')
+ query.append(x)
+ iq = self.xmpp.makeIqSet(query)
+ iq['to'] = room
+ iq['from'] = ifrom
+ iq.send()
+
+ def setRoomConfig(self, room, config, ifrom=''):
+ query = ET.Element('{http://jabber.org/protocol/muc#owner}query')
+ x = config.getXML('submit')
+ query.append(x)
+ iq = self.xmpp.makeIqSet(query)
+ iq['to'] = room
+ iq['from'] = ifrom
+ iq.send()
+
+ def getJoinedRooms(self):
+ return self.rooms.keys()
+
+ def getOurJidInRoom(self, roomJid):
+ """ Return the jid we're using in a room.
+ """
+ return "%s/%s" % (roomJid, self.ourNicks[roomJid])
+
+ def getJidProperty(self, room, nick, jidProperty):
+ """ Get the property of a nick in a room, such as its 'jid' or 'affiliation'
+ If not found, return None.
+ """
+ if room in self.rooms and nick in self.rooms[room] and jidProperty in self.rooms[room][nick]:
+ return self.rooms[room][nick][jidProperty]
+ else:
+ return None
+
+ def getRoster(self, room):
+ """ Get the list of nicks in a room.
+ """
+ if room not in self.rooms.keys():
+ return None
+ return self.rooms[room].keys()
diff --git a/sleekxmpp/plugins/xep_0050/__init__.py b/sleekxmpp/plugins/xep_0050/__init__.py
new file mode 100644
index 00000000..99f44f2a
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0050/__init__.py
@@ -0,0 +1,10 @@
+"""
+ 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_0050.stanza import Command
+from sleekxmpp.plugins.xep_0050.adhoc import xep_0050
diff --git a/sleekxmpp/plugins/xep_0050/adhoc.py b/sleekxmpp/plugins/xep_0050/adhoc.py
new file mode 100644
index 00000000..ec7b7041
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0050/adhoc.py
@@ -0,0 +1,614 @@
+"""
+ 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 time
+
+from sleekxmpp import Iq
+from sleekxmpp.exceptions import IqError
+from sleekxmpp.xmlstream.handler import Callback
+from sleekxmpp.xmlstream.matcher import StanzaPath
+from sleekxmpp.xmlstream import register_stanza_plugin, JID
+from sleekxmpp.plugins.base import base_plugin
+from sleekxmpp.plugins.xep_0050 import stanza
+from sleekxmpp.plugins.xep_0050 import Command
+from sleekxmpp.plugins.xep_0004 import Form
+
+
+log = logging.getLogger(__name__)
+
+
+class xep_0050(base_plugin):
+
+ """
+ XEP-0050: Ad-Hoc Commands
+
+ XMPP's Adhoc Commands provides a generic workflow mechanism for
+ interacting with applications. The result is similar to menu selections
+ and multi-step dialogs in normal desktop applications. Clients do not
+ need to know in advance what commands are provided by any particular
+ application or agent. While adhoc commands provide similar functionality
+ to Jabber-RPC, adhoc commands are used primarily for human interaction.
+
+ Also see <http://xmpp.org/extensions/xep-0050.html>
+
+ Configuration Values:
+ threaded -- Indicates if command events should be threaded.
+ Defaults to True.
+
+ Events:
+ command_execute -- Received a command with action="execute"
+ command_next -- Received a command with action="next"
+ command_complete -- Received a command with action="complete"
+ command_cancel -- Received a command with action="cancel"
+
+ Attributes:
+ threaded -- Indicates if command events should be threaded.
+ Defaults to True.
+ commands -- A dictionary mapping JID/node pairs to command
+ names and handlers.
+ sessions -- A dictionary or equivalent backend mapping
+ session IDs to dictionaries containing data
+ relevant to a command's session.
+
+ Methods:
+ plugin_init -- Overrides base_plugin.plugin_init
+ post_init -- Overrides base_plugin.post_init
+ new_session -- Return a new session ID.
+ prep_handlers -- Placeholder. May call with a list of handlers
+ to prepare them for use with the session storage
+ backend, if needed.
+ set_backend -- Replace the default session storage with some
+ external storage mechanism, such as a database.
+ The provided backend wrapper must be able to
+ act using the same syntax as a dictionary.
+ add_command -- Add a command for use by external entitites.
+ get_commands -- Retrieve a list of commands provided by a
+ remote agent.
+ send_command -- Send a command request to a remote agent.
+ start_command -- Command user API: initiate a command session
+ continue_command -- Command user API: proceed to the next step
+ cancel_command -- Command user API: cancel a command
+ complete_command -- Command user API: finish a command
+ terminate_command -- Command user API: delete a command's session
+ """
+
+ def plugin_init(self):
+ """Start the XEP-0050 plugin."""
+ self.xep = '0050'
+ self.description = 'Ad-Hoc Commands'
+ self.stanza = stanza
+
+ self.threaded = self.config.get('threaded', True)
+ self.commands = {}
+ self.sessions = self.config.get('session_db', {})
+
+ self.xmpp.register_handler(
+ Callback("Ad-Hoc Execute",
+ StanzaPath('iq@type=set/command'),
+ self._handle_command))
+
+ register_stanza_plugin(Iq, Command)
+ register_stanza_plugin(Command, Form)
+
+ self.xmpp.add_event_handler('command_execute',
+ self._handle_command_start,
+ threaded=self.threaded)
+ self.xmpp.add_event_handler('command_next',
+ self._handle_command_next,
+ threaded=self.threaded)
+ self.xmpp.add_event_handler('command_cancel',
+ self._handle_command_cancel,
+ threaded=self.threaded)
+ self.xmpp.add_event_handler('command_complete',
+ self._handle_command_complete,
+ threaded=self.threaded)
+
+ def post_init(self):
+ """Handle cross-plugin interactions."""
+ base_plugin.post_init(self)
+ self.xmpp['xep_0030'].add_feature(Command.namespace)
+
+ def set_backend(self, db):
+ """
+ Replace the default session storage dictionary with
+ a generic, external data storage mechanism.
+
+ The replacement backend must be able to interact through
+ the same syntax and interfaces as a normal dictionary.
+
+ Arguments:
+ db -- The new session storage mechanism.
+ """
+ self.sessions = db
+
+ def prep_handlers(self, handlers, **kwargs):
+ """
+ Prepare a list of functions for use by the backend service.
+
+ Intended to be replaced by the backend service as needed.
+
+ Arguments:
+ handlers -- A list of function pointers
+ **kwargs -- Any additional parameters required by the backend.
+ """
+ pass
+
+ # =================================================================
+ # Server side (command provider) API
+
+ def add_command(self, jid=None, node=None, name='', handler=None):
+ """
+ Make a new command available to external entities.
+
+ Access control may be implemented in the provided handler.
+
+ Command workflow is done across a sequence of command handlers. The
+ first handler is given the initial Iq stanza of the request in order
+ to support access control. Subsequent handlers are given only the
+ payload items of the command. All handlers will receive the command's
+ session data.
+
+ Arguments:
+ jid -- The JID that will expose the command.
+ node -- The node associated with the command.
+ name -- A human readable name for the command.
+ handler -- A function that will generate the response to the
+ initial command request, as well as enforcing any
+ access control policies.
+ """
+ if jid is None:
+ jid = self.xmpp.boundjid
+ elif not isinstance(jid, JID):
+ jid = JID(jid)
+ item_jid = jid.full
+
+ # Client disco uses only the bare JID
+ if self.xmpp.is_component:
+ jid = jid.full
+ else:
+ jid = jid.bare
+
+ self.xmpp['xep_0030'].add_identity(category='automation',
+ itype='command-list',
+ name='Ad-Hoc commands',
+ node=Command.namespace,
+ jid=jid)
+ self.xmpp['xep_0030'].add_item(jid=item_jid,
+ name=name,
+ node=Command.namespace,
+ subnode=node,
+ ijid=jid)
+ self.xmpp['xep_0030'].add_identity(category='automation',
+ itype='command-node',
+ name=name,
+ node=node,
+ jid=jid)
+ self.xmpp['xep_0030'].add_feature(Command.namespace, None, jid)
+
+ self.commands[(item_jid, node)] = (name, handler)
+
+ def new_session(self):
+ """Return a new session ID."""
+ return str(time.time()) + '-' + self.xmpp.new_id()
+
+ def _handle_command(self, iq):
+ """Raise command events based on the command action."""
+ self.xmpp.event('command_%s' % iq['command']['action'], iq)
+
+ def _handle_command_start(self, iq):
+ """
+ Process an initial request to execute a command.
+
+ Arguments:
+ iq -- The command execution request.
+ """
+ sessionid = self.new_session()
+ node = iq['command']['node']
+ key = (iq['to'].full, node)
+ name, handler = self.commands.get(key, ('Not found', None))
+ if not handler:
+ log.debug('Command not found: %s, %s', key, self.commands)
+ initial_session = {'id': sessionid,
+ 'from': iq['from'],
+ 'to': iq['to'],
+ 'node': node,
+ 'payload': None,
+ 'interfaces': '',
+ 'payload_classes': None,
+ 'notes': None,
+ 'has_next': False,
+ 'allow_complete': False,
+ 'allow_prev': False,
+ 'past': [],
+ 'next': None,
+ 'prev': None,
+ 'cancel': None}
+
+ session = handler(iq, initial_session)
+
+ self._process_command_response(iq, session)
+
+ def _handle_command_next(self, iq):
+ """
+ Process a request for the next step in the workflow
+ for a command with multiple steps.
+
+ Arguments:
+ iq -- The command continuation request.
+ """
+ sessionid = iq['command']['sessionid']
+ session = self.sessions[sessionid]
+
+ handler = session['next']
+ interfaces = session['interfaces']
+ results = []
+ for stanza in iq['command']['substanzas']:
+ if stanza.plugin_attrib in interfaces:
+ results.append(stanza)
+ if len(results) == 1:
+ results = results[0]
+
+ session = handler(results, session)
+
+ self._process_command_response(iq, session)
+
+ def _process_command_response(self, iq, session):
+ """
+ Generate a command reply stanza based on the
+ provided session data.
+
+ Arguments:
+ iq -- The command request stanza.
+ session -- A dictionary of relevant session data.
+ """
+ sessionid = session['id']
+
+ payload = session['payload']
+ if not isinstance(payload, list):
+ payload = [payload]
+
+ session['interfaces'] = [item.plugin_attrib for item in payload]
+ session['payload_classes'] = [item.__class__ for item in payload]
+
+ self.sessions[sessionid] = session
+
+ for item in payload:
+ register_stanza_plugin(Command, item.__class__, iterable=True)
+
+ iq.reply()
+ iq['command']['node'] = session['node']
+ iq['command']['sessionid'] = session['id']
+
+ if session['next'] is None:
+ iq['command']['actions'] = []
+ iq['command']['status'] = 'completed'
+ elif session['has_next']:
+ actions = ['next']
+ if session['allow_complete']:
+ actions.append('complete')
+ if session['allow_prev']:
+ actions.append('prev')
+ iq['command']['actions'] = actions
+ iq['command']['status'] = 'executing'
+ else:
+ iq['command']['actions'] = ['complete']
+ iq['command']['status'] = 'executing'
+
+ iq['command']['notes'] = session['notes']
+
+ for item in payload:
+ iq['command'].append(item)
+
+ iq.send()
+
+ def _handle_command_cancel(self, iq):
+ """
+ Process a request to cancel a command's execution.
+
+ Arguments:
+ iq -- The command cancellation request.
+ """
+ node = iq['command']['node']
+ sessionid = iq['command']['sessionid']
+ session = self.sessions[sessionid]
+ handler = session['cancel']
+
+ if handler:
+ handler(iq, session)
+
+ try:
+ del self.sessions[sessionid]
+ except:
+ pass
+
+ iq.reply()
+ iq['command']['node'] = node
+ iq['command']['sessionid'] = sessionid
+ iq['command']['status'] = 'canceled'
+ iq['command']['notes'] = session['notes']
+ iq.send()
+
+ def _handle_command_complete(self, iq):
+ """
+ Process a request to finish the execution of command
+ and terminate the workflow.
+
+ All data related to the command session will be removed.
+
+ Arguments:
+ iq -- The command completion request.
+ """
+ node = iq['command']['node']
+ sessionid = iq['command']['sessionid']
+ session = self.sessions[sessionid]
+ handler = session['next']
+ interfaces = session['interfaces']
+ results = []
+ for stanza in iq['command']['substanzas']:
+ if stanza.plugin_attrib in interfaces:
+ results.append(stanza)
+ if len(results) == 1:
+ results = results[0]
+
+ if handler:
+ handler(results, session)
+
+ iq.reply()
+ iq['command']['node'] = node
+ iq['command']['sessionid'] = sessionid
+ iq['command']['actions'] = []
+ iq['command']['status'] = 'completed'
+ iq['command']['notes'] = session['notes']
+ iq.send()
+
+ del self.sessions[sessionid]
+
+
+ # =================================================================
+ # Client side (command user) API
+
+ def get_commands(self, jid, **kwargs):
+ """
+ Return a list of commands provided by a given JID.
+
+ Arguments:
+ jid -- The JID to query for commands.
+ local -- If true, then the query is for a JID/node
+ combination handled by this Sleek instance and
+ no stanzas need to be sent.
+ Otherwise, a disco stanza must be sent to the
+ remove JID to retrieve the items.
+ 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.
+ iterator -- If True, return a result set iterator using
+ the XEP-0059 plugin, if the plugin is loaded.
+ Otherwise the parameter is ignored.
+ """
+ return self.xmpp['xep_0030'].get_items(jid=jid,
+ node=Command.namespace,
+ **kwargs)
+
+ def send_command(self, jid, node, ifrom=None, action='execute',
+ payload=None, sessionid=None, flow=False, **kwargs):
+ """
+ Create and send a command stanza, without using the provided
+ workflow management APIs.
+
+ Arguments:
+ jid -- The JID to send the command request or result.
+ node -- The node for the command.
+ ifrom -- Specify the sender's JID.
+ action -- May be one of: execute, cancel, complete,
+ or cancel.
+ payload -- Either a list of payload items, or a single
+ payload item such as a data form.
+ sessionid -- The current session's ID value.
+ flow -- If True, process the Iq result using the
+ command workflow methods contained in the
+ session instead of returning the response
+ stanza itself. Defaults to False.
+ block -- Specify if the send call will block until a
+ response is received, or a timeout occurs.
+ Defaults to True.
+ timeout -- The length of time (in seconds) to wait for a
+ response before exiting the send call
+ if blocking is used. Defaults to
+ sleekxmpp.xmlstream.RESPONSE_TIMEOUT
+ callback -- Optional reference to a stream handler
+ function. Will be executed when a reply
+ stanza is received if flow=False.
+ """
+ iq = self.xmpp.Iq()
+ iq['type'] = 'set'
+ iq['to'] = jid
+ iq['from'] = ifrom
+ iq['command']['node'] = node
+ iq['command']['action'] = action
+ if sessionid is not None:
+ iq['command']['sessionid'] = sessionid
+ if payload is not None:
+ if not isinstance(payload, list):
+ payload = [payload]
+ for item in payload:
+ iq['command'].append(item)
+ if not flow:
+ return iq.send(**kwargs)
+ else:
+ if kwargs.get('block', True):
+ try:
+ result = iq.send(**kwargs)
+ except IqError as err:
+ result = err.iq
+ self._handle_command_result(result)
+ else:
+ iq.send(block=False, callback=self._handle_command_result)
+
+ def start_command(self, jid, node, session, ifrom=None, block=False):
+ """
+ Initiate executing a command provided by a remote agent.
+
+ The default workflow provided is non-blocking, but a blocking
+ version may be used with block=True.
+
+ The provided session dictionary should contain:
+ next -- A handler for processing the command result.
+ error -- A handler for processing any error stanzas
+ generated by the request.
+
+ Arguments:
+ jid -- The JID to send the command request.
+ node -- The node for the desired command.
+ session -- A dictionary of relevant session data.
+ ifrom -- Optionally specify the sender's JID.
+ block -- If True, block execution until a result
+ is received. Defaults to False.
+ """
+ session['jid'] = jid
+ session['node'] = node
+ session['timestamp'] = time.time()
+ session['payload'] = None
+ session['block'] = block
+ iq = self.xmpp.Iq()
+ iq['type'] = 'set'
+ iq['to'] = jid
+ iq['from'] = ifrom
+ session['from'] = ifrom
+ iq['command']['node'] = node
+ iq['command']['action'] = 'execute'
+ sessionid = 'client:pending_' + iq['id']
+ session['id'] = sessionid
+ self.sessions[sessionid] = session
+ if session['block']:
+ try:
+ result = iq.send(block=True)
+ except IqError as err:
+ result = err.iq
+ self._handle_command_result(result)
+ else:
+ iq.send(block=False, callback=self._handle_command_result)
+
+ def continue_command(self, session):
+ """
+ Execute the next action of the command.
+
+ Arguments:
+ session -- All stored data relevant to the current
+ command session.
+ """
+ sessionid = 'client:' + session['id']
+ self.sessions[sessionid] = session
+
+ self.send_command(session['jid'],
+ session['node'],
+ ifrom=session.get('from', None),
+ action='next',
+ payload=session.get('payload', None),
+ sessionid=session['id'],
+ flow=True,
+ block=session['block'])
+
+ def cancel_command(self, session):
+ """
+ Cancel the execution of a command.
+
+ Arguments:
+ session -- All stored data relevant to the current
+ command session.
+ """
+ sessionid = 'client:' + session['id']
+ self.sessions[sessionid] = session
+
+ self.send_command(session['jid'],
+ session['node'],
+ ifrom=session.get('from', None),
+ action='cancel',
+ payload=session.get('payload', None),
+ sessionid=session['id'],
+ flow=True,
+ block=session['block'])
+
+ def complete_command(self, session):
+ """
+ Finish the execution of a command workflow.
+
+ Arguments:
+ session -- All stored data relevant to the current
+ command session.
+ """
+ sessionid = 'client:' + session['id']
+ self.sessions[sessionid] = session
+
+ self.send_command(session['jid'],
+ session['node'],
+ ifrom=session.get('from', None),
+ action='complete',
+ payload=session.get('payload', None),
+ sessionid=session['id'],
+ flow=True,
+ block=session['block'])
+
+ def terminate_command(self, session):
+ """
+ Delete a command's session after a command has completed
+ or an error has occured.
+
+ Arguments:
+ session -- All stored data relevant to the current
+ command session.
+ """
+ try:
+ del self.sessions[session['id']]
+ except:
+ pass
+
+ def _handle_command_result(self, iq):
+ """
+ Process the results of a command request.
+
+ Will execute the 'next' handler stored in the session
+ data, or the 'error' handler depending on the Iq's type.
+
+ Arguments:
+ iq -- The command response.
+ """
+ sessionid = 'client:' + iq['command']['sessionid']
+ pending = False
+
+ if sessionid not in self.sessions:
+ pending = True
+ pendingid = 'client:pending_' + iq['id']
+ if pendingid not in self.sessions:
+ return
+ sessionid = pendingid
+
+ session = self.sessions[sessionid]
+ sessionid = 'client:' + iq['command']['sessionid']
+ session['id'] = iq['command']['sessionid']
+
+ self.sessions[sessionid] = session
+
+ if pending:
+ del self.sessions[pendingid]
+
+ handler_type = 'next'
+ if iq['type'] == 'error':
+ handler_type = 'error'
+ handler = session.get(handler_type, None)
+ if handler:
+ handler(iq, session)
+ elif iq['type'] == 'error':
+ self.terminate_command(session)
+
+ if iq['command']['status'] == 'completed':
+ self.terminate_command(session)
diff --git a/sleekxmpp/plugins/xep_0050/stanza.py b/sleekxmpp/plugins/xep_0050/stanza.py
new file mode 100644
index 00000000..31a4a5d5
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0050/stanza.py
@@ -0,0 +1,185 @@
+"""
+ 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 Command(ElementBase):
+
+ """
+ XMPP's Adhoc Commands provides a generic workflow mechanism for
+ interacting with applications. The result is similar to menu selections
+ and multi-step dialogs in normal desktop applications. Clients do not
+ need to know in advance what commands are provided by any particular
+ application or agent. While adhoc commands provide similar functionality
+ to Jabber-RPC, adhoc commands are used primarily for human interaction.
+
+ Also see <http://xmpp.org/extensions/xep-0050.html>
+
+ Example command stanzas:
+ <iq type="set">
+ <command xmlns="http://jabber.org/protocol/commands"
+ node="run_foo"
+ action="execute" />
+ </iq>
+
+ <iq type="result">
+ <command xmlns="http://jabber.org/protocol/commands"
+ node="run_foo"
+ sessionid="12345"
+ status="executing">
+ <actions>
+ <complete />
+ </actions>
+ <note type="info">Information!</note>
+ <x xmlns="jabber:x:data">
+ <field var="greeting"
+ type="text-single"
+ label="Greeting" />
+ </x>
+ </command>
+ </iq>
+
+ Stanza Interface:
+ action -- The action to perform.
+ actions -- The set of allowable next actions.
+ node -- The node associated with the command.
+ notes -- A list of tuples for informative notes.
+ sessionid -- A unique identifier for a command session.
+ status -- May be one of: canceled, completed, or executing.
+
+ Attributes:
+ actions -- A set of allowed action values.
+ statuses -- A set of allowed status values.
+ next_actions -- A set of allowed next action names.
+
+ Methods:
+ get_action -- Return the requested action.
+ get_actions -- Return the allowable next actions.
+ set_actions -- Set the allowable next actions.
+ del_actions -- Remove the current set of next actions.
+ get_notes -- Return a list of informative note data.
+ set_notes -- Set informative notes.
+ del_notes -- Remove any note data.
+ add_note -- Add a single note.
+ """
+
+ name = 'command'
+ namespace = 'http://jabber.org/protocol/commands'
+ plugin_attrib = 'command'
+ interfaces = set(('action', 'sessionid', 'node',
+ 'status', 'actions', 'notes'))
+ actions = set(('cancel', 'complete', 'execute', 'next', 'prev'))
+ statuses = set(('canceled', 'completed', 'executing'))
+ next_actions = set(('prev', 'next', 'complete'))
+
+ def get_action(self):
+ """
+ Return the value of the action attribute.
+
+ If the Iq stanza's type is "set" then use a default
+ value of "execute".
+ """
+ if self.parent()['type'] == 'set':
+ return self._get_attr('action', default='execute')
+ return self._get_attr('action')
+
+ def set_actions(self, values):
+ """
+ Assign the set of allowable next actions.
+
+ Arguments:
+ values -- A list containing any combination of:
+ 'prev', 'next', and 'complete'
+ """
+ self.del_actions()
+ if values:
+ self._set_sub_text('{%s}actions' % self.namespace, '', True)
+ actions = self.find('{%s}actions' % self.namespace)
+ for val in values:
+ if val in self.next_actions:
+ action = ET.Element('{%s}%s' % (self.namespace, val))
+ actions.append(action)
+
+ def get_actions(self):
+ """
+ Return the set of allowable next actions.
+ """
+ actions = []
+ actions_xml = self.find('{%s}actions' % self.namespace)
+ if actions_xml is not None:
+ for action in self.next_actions:
+ action_xml = actions_xml.find('{%s}%s' % (self.namespace,
+ action))
+ if action_xml is not None:
+ actions.append(action)
+ return actions
+
+ def del_actions(self):
+ """
+ Remove all allowable next actions.
+ """
+ self._del_sub('{%s}actions' % self.namespace)
+
+ def get_notes(self):
+ """
+ Return a list of note information.
+
+ Example:
+ [('info', 'Some informative data'),
+ ('warning', 'Use caution'),
+ ('error', 'The command ran, but had errors')]
+ """
+ notes = []
+ notes_xml = self.findall('{%s}note' % self.namespace)
+ for note in notes_xml:
+ notes.append((note.attrib.get('type', 'info'),
+ note.text))
+ return notes
+
+ def set_notes(self, notes):
+ """
+ Add multiple notes to the command result.
+
+ Each note is a tuple, with the first item being one of:
+ 'info', 'warning', or 'error', and the second item being
+ any human readable message.
+
+ Example:
+ [('info', 'Some informative data'),
+ ('warning', 'Use caution'),
+ ('error', 'The command ran, but had errors')]
+
+
+ Arguments:
+ notes -- A list of tuples of note information.
+ """
+ self.del_notes()
+ for note in notes:
+ self.add_note(note[1], note[0])
+
+ def del_notes(self):
+ """
+ Remove all notes associated with the command result.
+ """
+ notes_xml = self.findall('{%s}note' % self.namespace)
+ for note in notes_xml:
+ self.xml.remove(note)
+
+ def add_note(self, msg='', ntype='info'):
+ """
+ Add a single note annotation to the command.
+
+ Arguments:
+ msg -- A human readable message.
+ ntype -- One of: 'info', 'warning', 'error'
+ """
+ xml = ET.Element('{%s}note' % self.namespace)
+ xml.attrib['type'] = ntype
+ xml.text = msg
+ self.xml.append(xml)
diff --git a/sleekxmpp/plugins/xep_0059/__init__.py b/sleekxmpp/plugins/xep_0059/__init__.py
new file mode 100644
index 00000000..3a9b8edf
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0059/__init__.py
@@ -0,0 +1,10 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz, Erik Reuterborg Larsson
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+from sleekxmpp.plugins.xep_0059.stanza import Set
+from sleekxmpp.plugins.xep_0059.rsm import ResultIterator, xep_0059
diff --git a/sleekxmpp/plugins/xep_0059/rsm.py b/sleekxmpp/plugins/xep_0059/rsm.py
new file mode 100644
index 00000000..35908473
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0059/rsm.py
@@ -0,0 +1,119 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz, Erik Reuterborg Larsson
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+import logging
+
+import sleekxmpp
+from sleekxmpp import Iq
+from sleekxmpp.plugins.base import base_plugin
+from sleekxmpp.xmlstream import register_stanza_plugin
+from sleekxmpp.plugins.xep_0059 import Set
+
+
+log = logging.getLogger(__name__)
+
+
+class ResultIterator():
+
+ """
+ An iterator for Result Set Managment
+ """
+
+ def __init__(self, query, interface, amount=10, start=None, reverse=False):
+ """
+ Arguments:
+ query -- The template query
+ interface -- The substanza of the query, for example disco_items
+ amount -- The max amounts of items to request per iteration
+ start -- From which item id to start
+ reverse -- If True, page backwards through the results
+
+ Example:
+ q = Iq()
+ q['to'] = 'pubsub.example.com'
+ q['disco_items']['node'] = 'blog'
+ for i in ResultIterator(q, 'disco_items', '10'):
+ print i['disco_items']['items']
+
+ """
+ self.query = query
+ self.amount = amount
+ self.start = start
+ self.interface = interface
+ self.reverse = reverse
+
+ def __iter__(self):
+ return self
+
+ def __next__(self):
+ return self.next()
+
+ def next(self):
+ """
+ Return the next page of results from a query.
+
+ Note: If using backwards paging, then the next page of
+ results will be the items before the current page
+ of items.
+ """
+ self.query[self.interface]['rsm']['before'] = self.reverse
+ self.query['id'] = self.query.stream.new_id()
+ self.query[self.interface]['rsm']['max'] = str(self.amount)
+
+ if self.start and self.reverse:
+ self.query[self.interface]['rsm']['before'] = self.start
+ elif self.start:
+ self.query[self.interface]['rsm']['after'] = self.start
+
+ r = self.query.send(block=True)
+
+ if not r or not r[self.interface]['rsm']['first'] and \
+ not r[self.interface]['rsm']['last']:
+ raise StopIteration
+
+ if self.reverse:
+ self.start = r[self.interface]['rsm']['first']
+ else:
+ self.start = r[self.interface]['rsm']['last']
+
+ return r
+
+
+class xep_0059(base_plugin):
+
+ """
+ XEP-0050: Result Set Management
+ """
+
+ def plugin_init(self):
+ """
+ Start the XEP-0059 plugin.
+ """
+ self.xep = '0059'
+ self.description = 'Result Set Management'
+ self.stanza = sleekxmpp.plugins.xep_0059.stanza
+
+ def post_init(self):
+ """Handle inter-plugin dependencies."""
+ base_plugin.post_init(self)
+ self.xmpp['xep_0030'].add_feature(Set.namespace)
+
+ def iterate(self, stanza, interface):
+ """
+ Create a new result set iterator for a given stanza query.
+
+ Arguments:
+ stanza -- A stanza object to serve as a template for
+ queries made each iteration. For example, a
+ basic disco#items query.
+ interface -- The name of the substanza to which the
+ result set management stanza should be
+ appended. For example, for disco#items queries
+ the interface 'disco_items' should be used.
+ """
+ return ResultIterator(stanza, interface)
diff --git a/sleekxmpp/plugins/xep_0059/stanza.py b/sleekxmpp/plugins/xep_0059/stanza.py
new file mode 100644
index 00000000..7c637d0b
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0059/stanza.py
@@ -0,0 +1,108 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz, Erik Reuterborg Larsson
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+from sleekxmpp.xmlstream import ElementBase, ET
+from sleekxmpp.plugins.xep_0030.stanza.items import DiscoItems
+
+
+class Set(ElementBase):
+
+ """
+ XEP-0059 (Result Set Managment) can be used to manage the
+ results of queries. For example, limiting the number of items
+ per response or starting at certain positions.
+
+ Example set stanzas:
+ <iq type="get">
+ <query xmlns="http://jabber.org/protocol/disco#items">
+ <set xmlns="http://jabber.org/protocol/rsm">
+ <max>2</max>
+ </set>
+ </query>
+ </iq>
+
+ <iq type="result">
+ <query xmlns="http://jabber.org/protocol/disco#items">
+ <item jid="conference.example.com" />
+ <item jid="pubsub.example.com" />
+ <set xmlns="http://jabber.org/protocol/rsm">
+ <first>conference.example.com</first>
+ <last>pubsub.example.com</last>
+ </set>
+ </query>
+ </iq>
+
+ Stanza Interface:
+ first_index -- The index attribute of <first>
+ after -- The id defining from which item to start
+ before -- The id defining from which item to
+ start when browsing backwards
+ max -- Max amount per response
+ first -- Id for the first item in the response
+ last -- Id for the last item in the response
+ index -- Used to set an index to start from
+ count -- The number of remote items available
+
+ Methods:
+ set_first_index -- Sets the index attribute for <first> and
+ creates the element if it doesn't exist
+ get_first_index -- Returns the value of the index
+ attribute for <first>
+ del_first_index -- Removes the index attribute for <first>
+ but keeps the element
+ set_before -- Sets the value of <before>, if the value is True
+ then the element will be created without a value
+ get_before -- Returns the value of <before>, if it is
+ empty it will return True
+
+ """
+ namespace = 'http://jabber.org/protocol/rsm'
+ name = 'set'
+ plugin_attrib = 'rsm'
+ sub_interfaces = set(('first', 'after', 'before', 'count',
+ 'index', 'last', 'max'))
+ interfaces = set(('first_index', 'first', 'after', 'before',
+ 'count', 'index', 'last', 'max'))
+
+ def set_first_index(self, val):
+ fi = self.find("{%s}first" % (self.namespace))
+ if fi is not None:
+ if val:
+ fi.attrib['index'] = val
+ else:
+ del fi.attrib['index']
+ elif val:
+ fi = ET.Element("{%s}first" % (self.namespace))
+ fi.attrib['index'] = val
+ self.xml.append(fi)
+
+ def get_first_index(self):
+ fi = self.find("{%s}first" % (self.namespace))
+ if fi is not None:
+ return fi.attrib.get('index', '')
+
+ def del_first_index(self):
+ fi = self.xml.find("{%s}first" % (self.namespace))
+ if fi is not None:
+ del fi.attrib['index']
+
+ def set_before(self, val):
+ b = self.xml.find("{%s}before" % (self.namespace))
+ if b is None and val == True:
+ self._set_sub_text('{%s}before' % self.namespace, '', True)
+ else:
+ self._set_sub_text('{%s}before' % self.namespace, val)
+
+ def get_before(self):
+ b = self.xml.find("{%s}before" % (self.namespace))
+ if b is not None and not b.text:
+ return True
+ elif b is not None:
+ return b.text
+ else:
+ return None
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..9e394ef2
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0060/pubsub.py
@@ -0,0 +1,450 @@
+"""
+ 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.xmlstream import JID
+from sleekxmpp.plugins.base import base_plugin
+from sleekxmpp.plugins.xep_0060 import stanza
+
+
+log = logging.getLogger(__name__)
+
+
+class xep_0060(base_plugin):
+
+ """
+ XEP-0060 Publish Subscribe
+ """
+
+ def plugin_init(self):
+ self.xep = '0060'
+ self.description = 'Publish-Subscribe'
+ self.stanza = stanza
+
+ def create_node(self, jid, node, config=None, ntype=None, ifrom=None,
+ block=True, callback=None, timeout=None):
+ """
+ Create and configure a new pubsub node.
+
+ A server MAY use a different name for the node than the one provided,
+ so be sure to check the result stanza for a server assigned name.
+
+ If no configuration form is provided, the node will be created using
+ the server's default configuration. To get the default configuration
+ use get_node_config().
+
+ Arguments:
+ jid -- The JID of the pubsub service.
+ node -- Optional name of the node to create. If no name is
+ provided, the server MAY generate a node ID for you.
+ The server can also assign a different name than the
+ one you provide; check the result stanza to see if
+ the server assigned a name.
+ config -- Optional XEP-0004 data form of configuration settings.
+ ntype -- The type of node to create. Servers typically default
+ to using 'leaf' if no type is provided.
+ ifrom -- Specify the sender's JID.
+ block -- Specify if the send call will block until a response
+ is received, or a timeout occurs. Defaults to True.
+ timeout -- The length of time (in seconds) to wait for a response
+ before exiting the send call if blocking is used.
+ Defaults to sleekxmpp.xmlstream.RESPONSE_TIMEOUT
+ callback -- Optional reference to a stream handler function. Will
+ be executed when a reply stanza is received.
+ """
+ iq = self.xmpp.Iq(sto=jid, sfrom=ifrom, stype='set')
+ iq['pubsub']['create']['node'] = node
+
+ if config is not None:
+ form_type = 'http://jabber.org/protocol/pubsub#node_config'
+ if 'FORM_TYPE' in config['fields']:
+ config.field['FORM_TYPE']['value'] = form_type
+ else:
+ config.add_field(var='FORM_TYPE',
+ ftype='hidden',
+ value=form_type)
+ if ntype:
+ if 'pubsub#node_type' in config['fields']:
+ config.field['pubsub#node_type']['value'] = ntype
+ else:
+ config.add_field(var='pubsub#node_type', value=ntype)
+ iq['pubsub']['configure'].append(config)
+
+ return iq.send(block=block, callback=callback, timeout=timeout)
+
+ def subscribe(self, jid, node, bare=True, subscribee=None, options=None,
+ ifrom=None, block=True, callback=None, timeout=None):
+ """
+ Subscribe to updates from a pubsub node.
+
+ The rules for determining the JID that is subscribing to the node are:
+ 1. If subscribee is given, use that as provided.
+ 2. If ifrom was given, use the bare or full version based on bare.
+ 3. Otherwise, use self.xmpp.boundjid based on bare.
+
+ Arguments:
+ jid -- The pubsub service JID.
+ node -- The node to subscribe to.
+ bare -- Indicates if the subscribee is a bare or full JID.
+ Defaults to True for a bare JID.
+ subscribee -- The JID that is subscribing to the node.
+ options --
+ ifrom -- Specify the sender's JID.
+ block -- Specify if the send call will block until a response
+ is received, or a timeout occurs. Defaults to True.
+ timeout -- The length of time (in seconds) to wait for a response
+ before exiting the send call if blocking is used.
+ Defaults to sleekxmpp.xmlstream.RESPONSE_TIMEOUT
+ callback -- Optional reference to a stream handler function. Will
+ be executed when a reply stanza is received.
+ """
+ iq = self.xmpp.Iq(sto=jid, sfrom=ifrom, stype='set')
+ iq['pubsub']['subscribe']['node'] = node
+
+ if subscribee is None:
+ if ifrom:
+ if bare:
+ subscribee = JID(ifrom).bare
+ else:
+ subscribee = ifrom
+ else:
+ if bare:
+ subscribee = self.xmpp.boundjid.bare
+ else:
+ subscribee = self.xmpp.boundjid
+
+ iq['pubsub']['subscribe']['jid'] = subscribee
+ if options is not None:
+ iq['pubsub']['options'].append(options)
+ return iq.send(block=block, callback=callback, timeout=timeout)
+
+ def unsubscribe(self, jid, node, subid=None, bare=True, subscribee=None,
+ ifrom=None, block=True, callback=None, timeout=None):
+ """
+ Unubscribe from updates from a pubsub node.
+
+ The rules for determining the JID that is unsubscribing
+ from the node are:
+ 1. If subscribee is given, use that as provided.
+ 2. If ifrom was given, use the bare or full version based on bare.
+ 3. Otherwise, use self.xmpp.boundjid based on bare.
+
+ Arguments:
+ jid -- The pubsub service JID.
+ node -- The node to subscribe to.
+ subid -- The specific subscription, if multiple subscriptions
+ exist for this JID/node combination.
+ bare -- Indicates if the subscribee is a bare or full JID.
+ Defaults to True for a bare JID.
+ subscribee -- The JID that is subscribing to the node.
+ ifrom -- Specify the sender's JID.
+ block -- Specify if the send call will block until a response
+ is received, or a timeout occurs. Defaults to True.
+ timeout -- The length of time (in seconds) to wait for a response
+ before exiting the send call if blocking is used.
+ Defaults to sleekxmpp.xmlstream.RESPONSE_TIMEOUT
+ callback -- Optional reference to a stream handler function. Will
+ be executed when a reply stanza is received.
+ """
+ iq = self.xmpp.Iq(sto=jid, sfrom=ifrom, stype='set')
+ iq['pubsub']['unsubscribe']['node'] = node
+
+ if subscribee is None:
+ if ifrom:
+ if bare:
+ subscribee = JID(ifrom).bare
+ else:
+ subscribee = ifrom
+ else:
+ if bare:
+ subscribee = self.xmpp.boundjid.bare
+ else:
+ subscribee = self.xmpp.boundjid
+
+ iq['pubsub']['unsubscribe']['jid'] = subscribee
+ iq['pubsub']['unsubscribe']['subid'] = subid
+ return iq.send(block=block, callback=callback, timeout=timeout)
+
+ def get_subscriptions(self, jid, node=None, ifrom=None, block=True,
+ callback=None, timeout=None):
+ iq = self.xmpp.Iq(sto=jid, sfrom=ifrom, stype='get')
+ iq['pubsub']['subscriptions']['node'] = node
+ return iq.send(block=block, callback=callback, timeout=timeout)
+
+ def get_affiliations(self, jid, node=None, ifrom=None, block=True,
+ callback=None, timeout=None):
+ iq = self.xmpp.Iq(sto=jid, sfrom=ifrom, stype='get')
+ iq['pubsub']['affiliations']['node'] = node
+ return iq.send(block=block, callback=callback, timeout=timeout)
+
+ def get_subscription_options(self, jid, node=None, user_jid=None, ifrom=None,
+ block=True, callback=None, timeout=None):
+ iq = self.xmpp.Iq(sto=jid, sfrom=ifrom, stype='get')
+ if user_jid is None:
+ iq['pubsub']['default']['node'] = node
+ else:
+ iq['pubsub']['options']['node'] = node
+ iq['pubsub']['options']['jid'] = user_jid
+ return iq.send(block=block, callback=callback, timeout=timeout)
+
+ def set_subscription_options(self, jid, node, user_jid, options,
+ ifrom=None, block=True, callback=None,
+ timeout=None):
+ iq = self.xmpp.Iq(sto=jid, sfrom=ifrom, stype='get')
+ iq['pubsub']['options']['node'] = node
+ iq['pubsub']['options']['jid'] = user_jid
+ iq['pubsub']['options'].append(options)
+ return iq.send(block=block, callback=callback, timeout=timeout)
+
+ def get_node_config(self, jid, node=None, ifrom=None, block=True,
+ callback=None, timeout=None):
+ """
+ Retrieve the configuration for a node, or the pubsub service's
+ default configuration for new nodes.
+
+ Arguments:
+ jid -- The JID of the pubsub service.
+ node -- The node to retrieve the configuration for. If None,
+ the default configuration for new nodes will be
+ requested. Defaults to None.
+ ifrom -- Specify the sender's JID.
+ block -- Specify if the send call will block until a response
+ is received, or a timeout occurs. Defaults to True.
+ timeout -- The length of time (in seconds) to wait for a response
+ before exiting the send call if blocking is used.
+ Defaults to sleekxmpp.xmlstream.RESPONSE_TIMEOUT
+ callback -- Optional reference to a stream handler function. Will
+ be executed when a reply stanza is received.
+ """
+ iq = self.xmpp.Iq(sto=jid, sfrom=ifrom, stype='get')
+ if node is None:
+ iq['pubsub_owner']['default']
+ else:
+ iq['pubsub_owner']['configure']['node'] = node
+ return iq.send(block=block, callback=callback, timeout=timeout)
+
+ def get_node_subscriptions(self, jid, node, ifrom=None, block=True,
+ callback=None, timeout=None):
+ """
+ Retrieve the subscriptions associated with a given node.
+
+ Arguments:
+ jid -- The JID of the pubsub service.
+ node -- The node to retrieve subscriptions from.
+ ifrom -- Specify the sender's JID.
+ block -- Specify if the send call will block until a response
+ is received, or a timeout occurs. Defaults to True.
+ timeout -- The length of time (in seconds) to wait for a response
+ before exiting the send call if blocking is used.
+ Defaults to sleekxmpp.xmlstream.RESPONSE_TIMEOUT
+ callback -- Optional reference to a stream handler function. Will
+ be executed when a reply stanza is received.
+ """
+ iq = self.xmpp.Iq(sto=jid, sfrom=ifrom, stype='get')
+ iq['pubsub_owner']['subscriptions']['node'] = node
+ return iq.send(block=block, callback=callback, timeout=timeout)
+
+ def get_node_affiliations(self, jid, node, ifrom=None, block=True,
+ callback=None, timeout=None):
+ """
+ Retrieve the affiliations associated with a given node.
+
+ Arguments:
+ jid -- The JID of the pubsub service.
+ node -- The node to retrieve affiliations from.
+ ifrom -- Specify the sender's JID.
+ block -- Specify if the send call will block until a response
+ is received, or a timeout occurs. Defaults to True.
+ timeout -- The length of time (in seconds) to wait for a response
+ before exiting the send call if blocking is used.
+ Defaults to sleekxmpp.xmlstream.RESPONSE_TIMEOUT
+ callback -- Optional reference to a stream handler function. Will
+ be executed when a reply stanza is received.
+ """
+ iq = self.xmpp.Iq(sto=jid, sfrom=ifrom, stype='get')
+ iq['pubsub_owner']['affiliations']['node'] = node
+ return iq.send(block=block, callback=callback, timeout=timeout)
+
+ def delete_node(self, jid, node, ifrom=None, block=True,
+ callback=None, timeout=None):
+ """
+ Delete a a pubsub node.
+
+ Arguments:
+ jid -- The JID of the pubsub service.
+ node -- The node to delete.
+ ifrom -- Specify the sender's JID.
+ block -- Specify if the send call will block until a response
+ is received, or a timeout occurs. Defaults to True.
+ timeout -- The length of time (in seconds) to wait for a response
+ before exiting the send call if blocking is used.
+ Defaults to sleekxmpp.xmlstream.RESPONSE_TIMEOUT
+ callback -- Optional reference to a stream handler function. Will
+ be executed when a reply stanza is received.
+ """
+ iq = self.xmpp.Iq(sto=jid, sfrom=ifrom, stype='set')
+ iq['pubsub_owner']['delete']['node'] = node
+ return iq.send(block=block, callback=callback, timeout=timeout)
+
+ def set_node_config(self, jid, node, config, ifrom=None, block=True,
+ callback=None, timeout=None):
+ iq = self.xmpp.Iq(sto=jid, sfrom=ifrom, stype='set')
+ iq['pubsub_owner']['configure']['node'] = node
+ iq['pubsub_owner']['configure']['form'].values = config.values
+ return iq.send(block=block, callback=callback, timeout=timeout)
+
+ def publish(self, jid, node, id=None, payload=None, options=None,
+ ifrom=None, block=True, callback=None, timeout=None):
+ """
+ Add a new item to a node, or edit an existing item.
+
+ For services that support it, you can use the publish command
+ as an event signal by not including an ID or payload.
+
+ When including a payload and you do not provide an ID then
+ the service will generally create an ID for you.
+
+ Publish options may be specified, and how those options
+ are processed is left to the service, such as treating
+ the options as preconditions that the node's settings
+ must match.
+
+ Arguments:
+ jid -- The JID of the pubsub service.
+ node -- The node to publish the item to.
+ id -- Optionally specify the ID of the item.
+ payload -- The item content to publish.
+ options -- A form of publish options.
+ ifrom -- Specify the sender's JID.
+ block -- Specify if the send call will block until a response
+ is received, or a timeout occurs. Defaults to True.
+ timeout -- The length of time (in seconds) to wait for a response
+ before exiting the send call if blocking is used.
+ Defaults to sleekxmpp.xmlstream.RESPONSE_TIMEOUT
+ callback -- Optional reference to a stream handler function. Will
+ be executed when a reply stanza is received.
+ """
+ iq = self.xmpp.Iq(sto=jid, sfrom=ifrom, stype='set')
+ iq['pubsub']['publish']['node'] = node
+ if id is not None:
+ iq['pubsub']['publish']['item']['id'] = id
+ if payload is not None:
+ iq['pubsub']['publish']['item']['payload'] = payload
+ iq['pubsub']['publish_options'] = options
+ return iq.send(block=block, callback=callback, timeout=timeout)
+
+ def retract(self, jid, node, id, notify=None, ifrom=None, block=True,
+ callback=None, timeout=None):
+ """
+ Delete a single item from a node.
+ """
+ iq = self.xmpp.Iq(sto=jid, sfrom=ifrom, stype='set')
+
+ iq['pubsub']['retract']['node'] = node
+ iq['pubsub']['retract']['notify'] = notify
+ iq['pubsub']['retract']['item']['id'] = id
+ return iq.send(block=block, callback=callback, timeout=timeout)
+
+ def purge(self, jid, node, ifrom=None, block=True, callback=None,
+ timeout=None):
+ """
+ Remove all items from a node.
+ """
+ iq = self.xmpp.Iq(sto=jid, sfrom=ifrom, stype='set')
+ iq['pubsub_owner']['purge']['node'] = node
+ return iq.send(block=block, callback=callback, timeout=timeout)
+
+ def get_nodes(self, *args, **kwargs):
+ """
+ Discover the nodes provided by a Pubsub service, using disco.
+ """
+ return self.xmpp.plugin['xep_0030'].get_items(*args, **kwargs)
+
+ def get_item(self, jid, node, item_id, ifrom=None, block=True,
+ callback=None, timeout=None):
+ """
+ Retrieve the content of an individual item.
+ """
+ iq = self.xmpp.Iq(sto=jid, sfrom=ifrom, stype='get')
+ item = self.stanza.Item()
+ item['id'] = item_id
+ iq['pubsub']['items']['node'] = node
+ iq['pubsub']['items'].append(item)
+ return iq.send(block=block, callback=callback, timeout=timeout)
+
+ def get_items(self, jid, node, item_ids=None, max_items=None,
+ iterator=False, ifrom=None, block=False,
+ callback=None, timeout=None):
+ """
+ Request the contents of a node's items.
+
+ The desired items can be specified, or a query for the last
+ few published items can be used.
+
+ Pubsub services may use result set management for nodes with
+ many items, so an iterator can be returned if needed.
+ """
+ iq = self.xmpp.Iq(sto=jid, sfrom=ifrom, stype='get')
+ iq['pubsub']['items']['node'] = node
+ iq['pubsub']['items']['max_items'] = max_items
+
+ if item_ids is not None:
+ for item_id in item_ids:
+ item = self.stanza.Item()
+ item['id'] = item_id
+ iq['pubsub']['items'].append(item)
+
+ if iterator:
+ return self.xmpp['xep_0059'].iterate(iq, 'pubsub')
+ else:
+ return iq.send(block=block, callback=callback, timeout=timeout)
+
+ def get_item_ids(self, jid, node, ifrom=None, block=True,
+ callback=None, timeout=None, iterator=False):
+ """
+ Retrieve the ItemIDs hosted by a given node, using disco.
+ """
+ return self.xmpp.plugin['xep_0030'].get_items(jid, node,
+ ifrom=ifrom,
+ block=block,
+ callback=callback,
+ timeout=timeout,
+ iterator=iterator)
+
+ def modify_affiliations(self, jid, node, affiliations=None, ifrom=None,
+ block=True, callback=None, timeout=None):
+ iq = self.xmpp.Iq(sto=jid, sfrom=ifrom, stype='set')
+ iq['pubsub_owner']['affiliations']['node'] = node
+
+ if affiliations is None:
+ affiliations = []
+
+ for jid, affiliation in affiliations:
+ aff = self.stanza.OwnerAffiliation()
+ aff['jid'] = jid
+ aff['affiliation'] = affiliation
+ iq['pubsub_owner']['affiliations'].append(aff)
+
+ return iq.send(block=block, callback=callback, timeout=timeout)
+
+ def modify_subscriptions(self, jid, node, subscriptions=None, ifrom=None,
+ block=True, callback=None, timeout=None):
+ iq = self.xmpp.Iq(sto=jid, sfrom=ifrom, stype='set')
+ iq['pubsub_owner']['subscriptions']['node'] = node
+
+ if subscriptions is None:
+ subscriptions = []
+
+ for jid, subscription in subscriptions:
+ sub = self.stanza.OwnerSubscription()
+ sub['jid'] = jid
+ sub['subscription'] = subscription
+ iq['pubsub_owner']['subscriptions'].append(sub)
+
+ return iq.send(block=block, callback=callback, timeout=timeout)
diff --git a/sleekxmpp/plugins/xep_0060/stanza/__init__.py b/sleekxmpp/plugins/xep_0060/stanza/__init__.py
new file mode 100644
index 00000000..37f52f0e
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0060/stanza/__init__.py
@@ -0,0 +1,12 @@
+"""
+ 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.plugins.xep_0060.stanza.pubsub import *
+from sleekxmpp.plugins.xep_0060.stanza.pubsub_owner import *
+from sleekxmpp.plugins.xep_0060.stanza.pubsub_event import *
+from sleekxmpp.plugins.xep_0060.stanza.pubsub_errors import *
diff --git a/sleekxmpp/plugins/xep_0060/stanza/base.py b/sleekxmpp/plugins/xep_0060/stanza/base.py
new file mode 100644
index 00000000..d0b7851e
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0060/stanza/base.py
@@ -0,0 +1,29 @@
+"""
+ 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 ET
+
+
+class OptionalSetting(object):
+
+ interfaces = set(('required',))
+
+ def set_required(self, value):
+ if value in (True, 'true', 'True', '1'):
+ self.xml.append(ET.Element("{%s}required" % self.namespace))
+ elif self['required']:
+ self.del_required()
+
+ def get_required(self):
+ required = self.xml.find("{%s}required" % self.namespace)
+ return required is not None
+
+ def del_required(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..004f0a02
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0060/stanza/pubsub.py
@@ -0,0 +1,300 @@
+"""
+ 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 import Iq, Message
+from sleekxmpp.xmlstream import register_stanza_plugin, ElementBase, ET, JID
+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 = name
+ interfaces = set(tuple())
+
+
+class Affiliations(ElementBase):
+ namespace = 'http://jabber.org/protocol/pubsub'
+ name = 'affiliations'
+ plugin_attrib = name
+ interfaces = set(('node',))
+
+
+class Affiliation(ElementBase):
+ namespace = 'http://jabber.org/protocol/pubsub'
+ name = 'affiliation'
+ plugin_attrib = name
+ interfaces = set(('node', 'affiliation', 'jid'))
+
+ def set_jid(self, value):
+ self._set_attr('jid', str(value))
+
+ def get_jid(self):
+ return JID(self._get_attr('jid'))
+
+
+class Subscription(ElementBase):
+ namespace = 'http://jabber.org/protocol/pubsub'
+ name = 'subscription'
+ plugin_attrib = name
+ interfaces = set(('jid', 'node', 'subscription', 'subid'))
+
+ def set_jid(self, value):
+ self._set_attr('jid', str(value))
+
+ def get_jid(self):
+ return JID(self._get_attr('jid'))
+
+
+class Subscriptions(ElementBase):
+ namespace = 'http://jabber.org/protocol/pubsub'
+ name = 'subscriptions'
+ plugin_attrib = name
+ interfaces = set(('node',))
+
+
+class SubscribeOptions(ElementBase, OptionalSetting):
+ namespace = 'http://jabber.org/protocol/pubsub'
+ name = 'subscribe-options'
+ plugin_attrib = 'suboptions'
+ interfaces = set(('required',))
+
+
+class Item(ElementBase):
+ namespace = 'http://jabber.org/protocol/pubsub'
+ name = 'item'
+ plugin_attrib = name
+ interfaces = set(('id', 'payload'))
+
+ def set_payload(self, value):
+ del self['payload']
+ self.append(value)
+
+ def get_payload(self):
+ childs = self.xml.getchildren()
+ if len(childs) > 0:
+ return childs[0]
+
+ def del_payload(self):
+ for child in self.xml.getchildren():
+ self.xml.remove(child)
+
+
+class Items(ElementBase):
+ namespace = 'http://jabber.org/protocol/pubsub'
+ name = 'items'
+ plugin_attrib = name
+ interfaces = set(('node', 'max_items'))
+
+ def set_max_items(self, value):
+ self._set_attr('max_items', str(value))
+
+
+class Create(ElementBase):
+ namespace = 'http://jabber.org/protocol/pubsub'
+ name = 'create'
+ plugin_attrib = name
+ interfaces = set(('node',))
+
+
+class Default(ElementBase):
+ namespace = 'http://jabber.org/protocol/pubsub'
+ name = 'default'
+ plugin_attrib = name
+ interfaces = set(('node', 'type'))
+
+ def get_type(self):
+ t = self._get_attr('type')
+ if not t:
+ return 'leaf'
+ return t
+
+
+class Publish(ElementBase):
+ namespace = 'http://jabber.org/protocol/pubsub'
+ name = 'publish'
+ plugin_attrib = name
+ interfaces = set(('node',))
+
+
+class Retract(ElementBase):
+ namespace = 'http://jabber.org/protocol/pubsub'
+ name = 'retract'
+ plugin_attrib = name
+ interfaces = set(('node', 'notify'))
+
+ def get_notify(self):
+ notify = self._get_attr('notify')
+ if notify in ('0', 'false'):
+ return False
+ elif notify in ('1', 'true'):
+ return True
+ return None
+
+ def set_notify(self, value):
+ del self['notify']
+ if value is None:
+ return
+ elif value in (True, '1', 'true', 'True'):
+ self._set_attr('notify', 'true')
+ else:
+ self._set_attr('notify', 'false')
+
+
+class Unsubscribe(ElementBase):
+ namespace = 'http://jabber.org/protocol/pubsub'
+ name = 'unsubscribe'
+ plugin_attrib = name
+ interfaces = set(('node', 'jid', 'subid'))
+
+ def set_jid(self, value):
+ self._set_attr('jid', str(value))
+
+ def get_jid(self):
+ return JID(self._get_attr('jid'))
+
+
+class Subscribe(ElementBase):
+ namespace = 'http://jabber.org/protocol/pubsub'
+ name = 'subscribe'
+ plugin_attrib = name
+ interfaces = set(('node', 'jid'))
+
+ def set_jid(self, value):
+ self._set_attr('jid', str(value))
+
+ def get_jid(self):
+ return JID(self._get_attr('jid'))
+
+
+class Configure(ElementBase):
+ namespace = 'http://jabber.org/protocol/pubsub'
+ name = 'configure'
+ plugin_attrib = name
+ interfaces = set(('node', 'type'))
+
+ def getType(self):
+ t = self._get_attr('type')
+ if not t:
+ t == 'leaf'
+ return t
+
+
+class Options(ElementBase):
+ namespace = 'http://jabber.org/protocol/pubsub'
+ name = 'options'
+ plugin_attrib = name
+ interfaces = set(('jid', 'node', 'options'))
+
+ def __init__(self, *args, **kwargs):
+ ElementBase.__init__(self, *args, **kwargs)
+
+ def get_options(self):
+ config = self.xml.find('{jabber:x:data}x')
+ form = xep_0004.Form(xml=config)
+ return form
+
+ def set_options(self, value):
+ self.xml.append(value.getXML())
+ return self
+
+ def del_options(self):
+ config = self.xml.find('{jabber:x:data}x')
+ self.xml.remove(config)
+
+ def set_jid(self, value):
+ self._set_attr('jid', str(value))
+
+ def get_jid(self):
+ return JID(self._get_attr('jid'))
+
+
+class PublishOptions(ElementBase):
+ namespace = 'http://jabber.org/protocol/pubsub'
+ name = 'publish-options'
+ plugin_attrib = 'publish_options'
+ interfaces = set(('publish_options',))
+ is_extension = True
+
+ def get_publish_options(self):
+ config = self.xml.find('{jabber:x:data}x')
+ if config is None:
+ return None
+ form = xep_0004.Form(xml=config)
+ return form
+
+ def set_publish_options(self, value):
+ if value is None:
+ self.del_publish_options()
+ else:
+ self.xml.append(value.getXML())
+ return self
+
+ def del_publish_options(self):
+ config = self.xml.find('{jabber:x:data}x')
+ if config is not None:
+ self.xml.remove(config)
+ self.parent().xml.remove(self.xml)
+
+
+class PubsubState(ElementBase):
+ """This is an experimental pubsub extension."""
+ namespace = 'http://jabber.org/protocol/psstate'
+ name = 'state'
+ plugin_attrib = 'psstate'
+ interfaces = set(('node', 'item', 'payload'))
+
+ def set_payload(self, value):
+ self.xml.append(value)
+
+ def get_payload(self):
+ childs = self.xml.getchildren()
+ if len(childs) > 0:
+ return childs[0]
+
+ def del_payload(self):
+ for child in self.xml.getchildren():
+ self.xml.remove(child)
+
+
+class PubsubStateEvent(ElementBase):
+ """This is an experimental pubsub extension."""
+ namespace = 'http://jabber.org/protocol/psstate#event'
+ name = 'event'
+ plugin_attrib = 'psstate_event'
+ intefaces = set(tuple())
+
+
+register_stanza_plugin(Iq, PubsubState)
+register_stanza_plugin(Message, PubsubStateEvent)
+register_stanza_plugin(PubsubStateEvent, PubsubState)
+
+
+register_stanza_plugin(Iq, Pubsub)
+register_stanza_plugin(Pubsub, Affiliations)
+register_stanza_plugin(Pubsub, Configure)
+register_stanza_plugin(Pubsub, Create)
+register_stanza_plugin(Pubsub, Default)
+register_stanza_plugin(Pubsub, Items)
+register_stanza_plugin(Pubsub, Options)
+register_stanza_plugin(Pubsub, Publish)
+register_stanza_plugin(Pubsub, PublishOptions)
+register_stanza_plugin(Pubsub, Retract)
+register_stanza_plugin(Pubsub, Subscribe)
+register_stanza_plugin(Pubsub, Subscription)
+register_stanza_plugin(Pubsub, Subscriptions)
+register_stanza_plugin(Pubsub, Unsubscribe)
+register_stanza_plugin(Affiliations, Affiliation, iterable=True)
+register_stanza_plugin(Configure, xep_0004.Form)
+register_stanza_plugin(Items, Item, iterable=True)
+register_stanza_plugin(Publish, Item, iterable=True)
+register_stanza_plugin(Retract, Item)
+register_stanza_plugin(Subscribe, Options)
+register_stanza_plugin(Subscription, SubscribeOptions)
+register_stanza_plugin(Subscriptions, Subscription, iterable=True)
diff --git a/sleekxmpp/plugins/xep_0060/stanza/pubsub_errors.py b/sleekxmpp/plugins/xep_0060/stanza/pubsub_errors.py
new file mode 100644
index 00000000..aeaeefe0
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0060/stanza/pubsub_errors.py
@@ -0,0 +1,86 @@
+"""
+ 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 Error
+from sleekxmpp.xmlstream import ElementBase, ET, register_stanza_plugin
+
+
+class PubsubErrorCondition(ElementBase):
+
+ plugin_attrib = 'pubsub'
+ interfaces = set(('condition', 'unsupported'))
+ plugin_attrib_map = {}
+ plugin_tag_map = {}
+ conditions = set(('closed-node', 'configuration-required', 'invalid-jid',
+ 'invalid-options', 'invalid-payload', 'invalid-subid',
+ 'item-forbidden', 'item-required', 'jid-required',
+ 'max-items-exceeded', 'max-nodes-exceeded',
+ 'nodeid-required', 'not-in-roster-group',
+ 'not-subscribed', 'payload-too-big',
+ 'payload-required', 'pending-subscription',
+ 'presence-subscription-required', 'subid-required',
+ 'too-many-subscriptions', 'unsupported'))
+ condition_ns = 'http://jabber.org/protocol/pubsub#errors'
+
+ def setup(self, xml):
+ """Don't create XML for the plugin."""
+ self.xml = ET.Element('')
+
+ def get_condition(self):
+ """Return the condition element's name."""
+ for child in self.parent().xml.getchildren():
+ if "{%s}" % self.condition_ns in child.tag:
+ cond = child.tag.split('}', 1)[-1]
+ if cond in self.conditions:
+ return cond
+ return ''
+
+ 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']
+ cond = ET.Element("{%s}%s" % (self.condition_ns, value))
+ self.parent().xml.append(cond)
+ return self
+
+ def del_condition(self):
+ """Remove the condition element."""
+ for child in self.parent().xml.getchildren():
+ if "{%s}" % self.condition_ns in child.tag:
+ tag = child.tag.split('}', 1)[-1]
+ if tag in self.conditions:
+ self.parent().xml.remove(child)
+ return self
+
+ def get_unsupported(self):
+ """Return the name of an unsupported feature"""
+ xml = self.parent().xml.find('{%s}unsupported' % self.condition_ns)
+ if xml is not None:
+ return xml.attrib.get('feature', '')
+ return ''
+
+ def set_unsupported(self, value):
+ """Mark a feature as unsupported"""
+ self.del_unsupported()
+ xml = ET.Element('{%s}unsupported' % self.condition_ns)
+ xml.attrib['feature'] = value
+ self.parent().xml.append(xml)
+
+ def del_unsupported(self):
+ """Delete an unsupported feature condition."""
+ xml = self.parent().xml.find('{%s}unsupported' % self.condition_ns)
+ if xml is not None:
+ self.parent().xml.remove(xml)
+
+
+register_stanza_plugin(Error, PubsubErrorCondition)
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..c7263577
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0060/stanza/pubsub_event.py
@@ -0,0 +1,112 @@
+"""
+ 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 import Message
+from sleekxmpp.xmlstream import register_stanza_plugin, ElementBase, ET, JID
+from sleekxmpp.plugins.xep_0004 import Form
+
+
+class Event(ElementBase):
+ namespace = 'http://jabber.org/protocol/pubsub#event'
+ name = 'event'
+ plugin_attrib = 'pubsub_event'
+ interfaces = set(('node',))
+
+
+class EventItem(ElementBase):
+ namespace = 'http://jabber.org/protocol/pubsub#event'
+ name = 'item'
+ plugin_attrib = name
+ interfaces = set(('id', 'payload'))
+
+ def set_payload(self, value):
+ self.xml.append(value)
+
+ def get_payload(self):
+ childs = self.xml.getchildren()
+ if len(childs) > 0:
+ return childs[0]
+
+ def del_payload(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 = name
+ interfaces = set(('id',))
+
+
+class EventItems(ElementBase):
+ namespace = 'http://jabber.org/protocol/pubsub#event'
+ name = 'items'
+ plugin_attrib = name
+ interfaces = set(('node',))
+
+
+class EventCollection(ElementBase):
+ namespace = 'http://jabber.org/protocol/pubsub#event'
+ name = 'collection'
+ plugin_attrib = name
+ interfaces = set(('node',))
+
+
+class EventAssociate(ElementBase):
+ namespace = 'http://jabber.org/protocol/pubsub#event'
+ name = 'associate'
+ plugin_attrib = name
+ interfaces = set(('node',))
+
+
+class EventDisassociate(ElementBase):
+ namespace = 'http://jabber.org/protocol/pubsub#event'
+ name = 'disassociate'
+ plugin_attrib = name
+ interfaces = set(('node',))
+
+
+class EventConfiguration(ElementBase):
+ namespace = 'http://jabber.org/protocol/pubsub#event'
+ name = 'configuration'
+ plugin_attrib = name
+ interfaces = set(('node', 'config'))
+
+
+class EventPurge(ElementBase):
+ namespace = 'http://jabber.org/protocol/pubsub#event'
+ name = 'purge'
+ plugin_attrib = name
+ interfaces = set(('node',))
+
+
+class EventSubscription(ElementBase):
+ namespace = 'http://jabber.org/protocol/pubsub#event'
+ name = 'subscription'
+ plugin_attrib = name
+ interfaces = set(('node', 'expiry', 'jid', 'subid', 'subscription'))
+
+ def set_jid(self, value):
+ self._set_attr('jid', str(value))
+
+ def get_jid(self):
+ return JID(self._get_attr('jid'))
+
+
+register_stanza_plugin(Message, Event)
+register_stanza_plugin(Event, EventCollection)
+register_stanza_plugin(Event, EventConfiguration)
+register_stanza_plugin(Event, EventItems)
+register_stanza_plugin(Event, EventPurge)
+register_stanza_plugin(Event, EventSubscription)
+register_stanza_plugin(EventCollection, EventAssociate)
+register_stanza_plugin(EventCollection, EventDisassociate)
+register_stanza_plugin(EventConfiguration, Form)
+register_stanza_plugin(EventItems, EventItem, iterable=True)
+register_stanza_plugin(EventItems, EventRetract, iterable=True)
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..4a35db9d
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0060/stanza/pubsub_owner.py
@@ -0,0 +1,131 @@
+"""
+ 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 import Iq
+from sleekxmpp.xmlstream import register_stanza_plugin, ElementBase, ET, JID
+from sleekxmpp.plugins.xep_0004 import Form
+from sleekxmpp.plugins.xep_0060.stanza.base import OptionalSetting
+from sleekxmpp.plugins.xep_0060.stanza.pubsub import Affiliations, Affiliation
+from sleekxmpp.plugins.xep_0060.stanza.pubsub import Configure, Subscriptions
+
+
+class PubsubOwner(ElementBase):
+ namespace = 'http://jabber.org/protocol/pubsub#owner'
+ name = 'pubsub'
+ plugin_attrib = 'pubsub_owner'
+ interfaces = set(tuple())
+
+
+class DefaultConfig(ElementBase):
+ namespace = 'http://jabber.org/protocol/pubsub#owner'
+ name = 'default'
+ plugin_attrib = name
+ interfaces = set(('node', 'config'))
+
+ def __init__(self, *args, **kwargs):
+ ElementBase.__init__(self, *args, **kwargs)
+
+ def get_config(self):
+ return self['form']
+
+ def set_config(self, value):
+ self['form'].values = value.values
+ return self
+
+
+class OwnerAffiliations(Affiliations):
+ namespace = 'http://jabber.org/protocol/pubsub#owner'
+ interfaces = set(('node',))
+
+ def append(self, affiliation):
+ if not isinstance(affiliation, OwnerAffiliation):
+ raise TypeError
+ self.xml.append(affiliation.xml)
+
+
+class OwnerAffiliation(Affiliation):
+ namespace = 'http://jabber.org/protocol/pubsub#owner'
+ interfaces = set(('affiliation', 'jid'))
+
+
+class OwnerConfigure(Configure):
+ namespace = 'http://jabber.org/protocol/pubsub#owner'
+ name = 'configure'
+ plugin_attrib = name
+ interfaces = set(('node',))
+
+
+class OwnerDefault(OwnerConfigure):
+ namespace = 'http://jabber.org/protocol/pubsub#owner'
+ interfaces = set(('node',))
+
+
+class OwnerDelete(ElementBase, OptionalSetting):
+ namespace = 'http://jabber.org/protocol/pubsub#owner'
+ name = 'delete'
+ plugin_attrib = name
+ interfaces = set(('node',))
+
+
+class OwnerPurge(ElementBase, OptionalSetting):
+ namespace = 'http://jabber.org/protocol/pubsub#owner'
+ name = 'purge'
+ plugin_attrib = name
+ interfaces = set(('node',))
+
+
+class OwnerRedirect(ElementBase):
+ namespace = 'http://jabber.org/protocol/pubsub#owner'
+ name = 'redirect'
+ plugin_attrib = name
+ interfaces = set(('node', 'jid'))
+
+ def set_jid(self, value):
+ self._set_attr('jid', str(value))
+
+ def get_jid(self):
+ return JID(self._get_attr('jid'))
+
+
+class OwnerSubscriptions(Subscriptions):
+ namespace = 'http://jabber.org/protocol/pubsub#owner'
+ interfaces = set(('node',))
+
+ def append(self, subscription):
+ if not isinstance(subscription, OwnerSubscription):
+ raise TypeError
+ self.xml.append(subscription.xml)
+
+
+class OwnerSubscription(ElementBase):
+ namespace = 'http://jabber.org/protocol/pubsub#owner'
+ name = 'subscription'
+ plugin_attrib = name
+ interfaces = set(('jid', 'subscription'))
+
+ def set_jid(self, value):
+ self._set_attr('jid', str(value))
+
+ def get_jid(self):
+ return JID(self._get_attr('jid'))
+
+
+register_stanza_plugin(Iq, PubsubOwner)
+register_stanza_plugin(PubsubOwner, DefaultConfig)
+register_stanza_plugin(PubsubOwner, OwnerAffiliations)
+register_stanza_plugin(PubsubOwner, OwnerConfigure)
+register_stanza_plugin(PubsubOwner, OwnerDefault)
+register_stanza_plugin(PubsubOwner, OwnerDelete)
+register_stanza_plugin(PubsubOwner, OwnerPurge)
+register_stanza_plugin(PubsubOwner, OwnerSubscriptions)
+register_stanza_plugin(DefaultConfig, Form)
+register_stanza_plugin(OwnerAffiliations, OwnerAffiliation, iterable=True)
+register_stanza_plugin(OwnerConfigure, Form)
+register_stanza_plugin(OwnerDefault, Form)
+register_stanza_plugin(OwnerDelete, OwnerRedirect)
+register_stanza_plugin(OwnerSubscriptions, OwnerSubscription, iterable=True)
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..d1f4b3ff
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0066/oob.py
@@ -0,0 +1,153 @@
+"""
+ 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
+ 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/__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..dec775a3
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0078/legacyauth.py
@@ -0,0 +1,119 @@
+"""
+ 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
+
+ try:
+ resp = iq.send(now=True)
+ except IqError:
+ log.info("Authentication failed: %s", resp['error']['condition'])
+ self.xmpp.event('failed_auth', direct=True)
+ self.xmpp.disconnect()
+ return True
+ except IqTimeout:
+ log.info("Authentication failed: %s", 'timeout')
+ self.xmpp.event('failed_auth', 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
+ try:
+ result = iq.send(now=True)
+ except IqError as err:
+ log.info("Authentication failed")
+ self.xmpp.disconnect()
+ self.xmpp.event("failed_auth", direct=True)
+ except IqTimeout:
+ log.info("Authentication failed")
+ self.xmpp.disconnect()
+ self.xmpp.event("failed_auth", direct=True)
+
+ 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')
+
+ 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..25c80fd0
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0082.py
@@ -0,0 +1,219 @@
+"""
+ 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, obj=False):
+ """
+ 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.
+ obj -- If True, return the date object instead
+ of a formatted string. Defaults to False.
+ """
+ today = dt.datetime.utcnow()
+ if year is None:
+ year = today.year
+ if month is None:
+ month = today.month
+ if day is None:
+ day = today.day
+ value = dt.date(year, month, day)
+ if obj:
+ return value
+ return format_date(value)
+
+def time(hour=None, min=None, sec=None, micro=None, offset=None, obj=False):
+ """
+ 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.
+ obj -- If True, return the time object instead
+ of a formatted string. Defaults to False.
+ """
+ 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)
+ value = dt.time(hour, min, sec, micro, offset)
+ if obj:
+ return value
+ return format_time(value)
+
+def datetime(year=None, month=None, day=None, hour=None,
+ min=None, sec=None, micro=None, offset=None,
+ separators=True, obj=False):
+ """
+ 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.
+ obj -- If True, return the datetime object instead
+ of a formatted string. Defaults to False.
+ """
+ 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)
+
+ value = dt.datetime(year, month, day, hour,
+ min, sec, micro, offset)
+ if obj:
+ return value
+ return format_datetime(value)
+
+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_0085/__init__.py b/sleekxmpp/plugins/xep_0085/__init__.py
new file mode 100644
index 00000000..ff882f05
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0085/__init__.py
@@ -0,0 +1,10 @@
+"""
+ 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 permissio
+"""
+
+from sleekxmpp.plugins.xep_0085.stanza import ChatState
+from sleekxmpp.plugins.xep_0085.chat_states import xep_0085
diff --git a/sleekxmpp/plugins/xep_0085/chat_states.py b/sleekxmpp/plugins/xep_0085/chat_states.py
new file mode 100644
index 00000000..e95434d2
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0085/chat_states.py
@@ -0,0 +1,49 @@
+"""
+ 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 permissio
+"""
+
+import logging
+
+import sleekxmpp
+from sleekxmpp.stanza import Message
+from sleekxmpp.xmlstream.handler import Callback
+from sleekxmpp.xmlstream.matcher import StanzaPath
+from sleekxmpp.xmlstream import register_stanza_plugin, ElementBase, ET
+from sleekxmpp.plugins.base import base_plugin
+from sleekxmpp.plugins.xep_0085 import stanza, ChatState
+
+
+log = logging.getLogger(__name__)
+
+
+class xep_0085(base_plugin):
+
+ """
+ XEP-0085 Chat State Notifications
+ """
+
+ def plugin_init(self):
+ self.xep = '0085'
+ self.description = 'Chat State Notifications'
+ self.stanza = stanza
+
+ for state in ChatState.states:
+ self.xmpp.register_handler(
+ Callback('Chat State: %s' % state,
+ StanzaPath('message@chat_state=%s' % state),
+ self._handle_chat_state))
+
+ register_stanza_plugin(Message, ChatState)
+
+ def post_init(self):
+ base_plugin.post_init(self)
+ self.xmpp.plugin['xep_0030'].add_feature(ChatState.namespace)
+
+ def _handle_chat_state(self, msg):
+ state = msg['chat_state']
+ log.debug("Chat State: %s, %s", state, msg['from'].jid)
+ self.xmpp.event('chatstate_%s' % state, msg)
diff --git a/sleekxmpp/plugins/xep_0085/stanza.py b/sleekxmpp/plugins/xep_0085/stanza.py
new file mode 100644
index 00000000..8c46758c
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0085/stanza.py
@@ -0,0 +1,73 @@
+"""
+ 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 permissio
+"""
+
+import sleekxmpp
+from sleekxmpp.xmlstream import ElementBase, ET
+
+
+class ChatState(ElementBase):
+
+ """
+ Example chat state stanzas:
+ <message>
+ <active xmlns="http://jabber.org/protocol/chatstates" />
+ </message>
+
+ <message>
+ <paused xmlns="http://jabber.org/protocol/chatstates" />
+ </message>
+
+ Stanza Interfaces:
+ chat_state
+
+ Attributes:
+ states
+
+ Methods:
+ get_chat_state
+ set_chat_state
+ del_chat_state
+ """
+
+ name = ''
+ namespace = 'http://jabber.org/protocol/chatstates'
+ plugin_attrib = 'chat_state'
+ interfaces = set(('chat_state',))
+ is_extension = True
+
+ states = set(('active', 'composing', 'gone', 'inactive', 'paused'))
+
+ def setup(self, xml=None):
+ self.xml = ET.Element('')
+ return True
+
+ def get_chat_state(self):
+ parent = self.parent()
+ for state in self.states:
+ state_xml = parent.find('{%s}%s' % (self.namespace, state))
+ if state_xml is not None:
+ self.xml = state_xml
+ return state
+ return ''
+
+ def set_chat_state(self, state):
+ self.del_chat_state()
+ parent = self.parent()
+ if state in self.states:
+ self.xml = ET.Element('{%s}%s' % (self.namespace, state))
+ parent.append(self.xml)
+ elif state not in [None, '']:
+ raise ValueError('Invalid chat state')
+
+ def del_chat_state(self):
+ parent = self.parent()
+ for state in self.states:
+ state_xml = parent.find('{%s}%s' % (self.namespace, state))
+ if state_xml is not None:
+ self.xml = ET.Element('')
+ parent.xml.remove(state_xml)
diff --git a/sleekxmpp/plugins/xep_0086/__init__.py b/sleekxmpp/plugins/xep_0086/__init__.py
new file mode 100644
index 00000000..b021e2b5
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0086/__init__.py
@@ -0,0 +1,10 @@
+"""
+ 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_0086.stanza import LegacyError
+from sleekxmpp.plugins.xep_0086.legacy_error import xep_0086
diff --git a/sleekxmpp/plugins/xep_0086/legacy_error.py b/sleekxmpp/plugins/xep_0086/legacy_error.py
new file mode 100644
index 00000000..25b98c5a
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0086/legacy_error.py
@@ -0,0 +1,42 @@
+"""
+ 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 Error
+from sleekxmpp.xmlstream import register_stanza_plugin
+from sleekxmpp.plugins.base import base_plugin
+from sleekxmpp.plugins.xep_0086 import stanza, LegacyError
+
+
+class xep_0086(base_plugin):
+
+ """
+ XEP-0086: Error Condition Mappings
+
+ Older XMPP implementations used code based error messages, similar
+ to HTTP response codes. Since then, error condition elements have
+ been introduced. XEP-0086 provides a mapping between the new
+ condition elements and a combination of error types and the older
+ response codes.
+
+ Also see <http://xmpp.org/extensions/xep-0086.html>.
+
+ Configuration Values:
+ override -- Indicates if applying legacy error codes should
+ be done automatically. Defaults to True.
+ If False, then inserting legacy error codes can
+ be done using:
+ iq['error']['legacy']['condition'] = ...
+ """
+
+ def plugin_init(self):
+ self.xep = '0086'
+ self.description = 'Error Condition Mappings'
+ self.stanza = stanza
+
+ register_stanza_plugin(Error, LegacyError,
+ overrides=self.config.get('override', True))
diff --git a/sleekxmpp/plugins/xep_0086/stanza.py b/sleekxmpp/plugins/xep_0086/stanza.py
new file mode 100644
index 00000000..6554d249
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0086/stanza.py
@@ -0,0 +1,91 @@
+"""
+ 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 Error
+from sleekxmpp.xmlstream import ElementBase, ET, register_stanza_plugin
+
+
+class LegacyError(ElementBase):
+
+ """
+ Older XMPP implementations used code based error messages, similar
+ to HTTP response codes. Since then, error condition elements have
+ been introduced. XEP-0086 provides a mapping between the new
+ condition elements and a combination of error types and the older
+ response codes.
+
+ Also see <http://xmpp.org/extensions/xep-0086.html>.
+
+ Example legacy error stanzas:
+ <error xmlns="jabber:client" code="501" type="cancel">
+ <feature-not-implemented
+ xmlns="urn:ietf:params:xml:ns:xmpp-stanzas" />
+ </error>
+
+ <error code="402" type="auth">
+ <payment-required
+ xmlns="urn:ietf:params:xml:ns:xmpp-stanzas" />
+ </error>
+
+ Attributes:
+ error_map -- A map of error conditions to error types and
+ code values.
+ Methods:
+ setup -- Overrides ElementBase.setup
+ set_condition -- Remap the type and code interfaces when a
+ condition is set.
+ """
+
+ name = 'legacy'
+ namespace = Error.namespace
+ plugin_attrib = name
+ interfaces = set(('condition',))
+ overrides = ['set_condition']
+
+ error_map = {'bad-request': ('modify','400'),
+ 'conflict': ('cancel','409'),
+ 'feature-not-implemented': ('cancel','501'),
+ 'forbidden': ('auth','403'),
+ 'gone': ('modify','302'),
+ 'internal-server-error': ('wait','500'),
+ 'item-not-found': ('cancel','404'),
+ 'jid-malformed': ('modify','400'),
+ 'not-acceptable': ('modify','406'),
+ 'not-allowed': ('cancel','405'),
+ 'not-authorized': ('auth','401'),
+ 'payment-required': ('auth','402'),
+ 'recipient-unavailable': ('wait','404'),
+ 'redirect': ('modify','302'),
+ 'registration-required': ('auth','407'),
+ 'remote-server-not-found': ('cancel','404'),
+ 'remote-server-timeout': ('wait','504'),
+ 'resource-constraint': ('wait','500'),
+ 'service-unavailable': ('cancel','503'),
+ 'subscription-required': ('auth','407'),
+ 'undefined-condition': (None,'500'),
+ 'unexpected-request': ('wait','400')}
+
+ def setup(self, xml):
+ """Don't create XML for the plugin."""
+ self.xml = ET.Element('')
+
+ def set_condition(self, value):
+ """
+ Set the error type and code based on the given error
+ condition value.
+
+ Arguments:
+ value -- The new error condition.
+ """
+ self.parent().set_condition(value)
+
+ error_data = self.error_map.get(value, None)
+ if error_data is not None:
+ if error_data[0] is not None:
+ self.parent()['type'] = error_data[0]
+ self.parent()['code'] = error_data[1]
diff --git a/sleekxmpp/plugins/xep_0092/__init__.py b/sleekxmpp/plugins/xep_0092/__init__.py
new file mode 100644
index 00000000..7c5bdb76
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0092/__init__.py
@@ -0,0 +1,11 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+from sleekxmpp.plugins.xep_0092 import stanza
+from sleekxmpp.plugins.xep_0092.stanza import Version
+from sleekxmpp.plugins.xep_0092.version import xep_0092
diff --git a/sleekxmpp/plugins/xep_0092/stanza.py b/sleekxmpp/plugins/xep_0092/stanza.py
new file mode 100644
index 00000000..77654e37
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0092/stanza.py
@@ -0,0 +1,42 @@
+"""
+ 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, ET
+
+
+class Version(ElementBase):
+
+ """
+ XMPP allows for an agent to advertise the name and version of the
+ underlying software libraries, as well as the operating system
+ that the agent is running on.
+
+ Example version stanzas:
+ <iq type="get">
+ <query xmlns="jabber:iq:version" />
+ </iq>
+
+ <iq type="result">
+ <query xmlns="jabber:iq:version">
+ <name>SleekXMPP</name>
+ <version>1.0</version>
+ <os>Linux</os>
+ </query>
+ </iq>
+
+ Stanza Interface:
+ name -- The human readable name of the software.
+ version -- The specific version of the software.
+ os -- The name of the operating system running the program.
+ """
+
+ name = 'query'
+ namespace = 'jabber:iq:version'
+ plugin_attrib = 'software_version'
+ interfaces = set(('name', 'version', 'os'))
+ sub_interfaces = interfaces
diff --git a/sleekxmpp/plugins/xep_0092/version.py b/sleekxmpp/plugins/xep_0092/version.py
new file mode 100644
index 00000000..ba72a9c3
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0092/version.py
@@ -0,0 +1,87 @@
+"""
+ 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 sleekxmpp
+from sleekxmpp 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.xep_0092 import Version
+
+
+log = logging.getLogger(__name__)
+
+
+class xep_0092(base_plugin):
+
+ """
+ XEP-0092: Software Version
+ """
+
+ def plugin_init(self):
+ """
+ Start the XEP-0092 plugin.
+ """
+ self.xep = "0092"
+ self.description = "Software Version"
+ self.stanza = sleekxmpp.plugins.xep_0092.stanza
+
+ self.name = self.config.get('name', 'SleekXMPP')
+ self.version = self.config.get('version', sleekxmpp.__version__)
+ self.os = self.config.get('os', '')
+
+ self.getVersion = self.get_version
+
+ self.xmpp.register_handler(
+ Callback('Software Version',
+ StanzaPath('iq@type=get/software_version'),
+ self._handle_version))
+
+ register_stanza_plugin(Iq, Version)
+
+ def post_init(self):
+ """
+ Handle cross-plugin dependencies.
+ """
+ base_plugin.post_init(self)
+ self.xmpp.plugin['xep_0030'].add_feature('jabber:iq:version')
+
+ def _handle_version(self, iq):
+ """
+ Respond to a software version query.
+
+ Arguments:
+ iq -- The Iq stanza containing the software version query.
+ """
+ iq.reply()
+ iq['software_version']['name'] = self.name
+ iq['software_version']['version'] = self.version
+ iq['software_version']['os'] = self.os
+ iq.send()
+
+ def get_version(self, jid, ifrom=None):
+ """
+ Retrieve the software version of a remote agent.
+
+ Arguments:
+ jid -- The JID of the entity to query.
+ """
+ iq = self.xmpp.Iq()
+ iq['to'] = jid
+ iq['from'] = ifrom
+ iq['type'] = 'get'
+ iq['query'] = Version.namespace
+
+ result = iq.send()
+
+ if result and result['type'] != 'error':
+ return result['software_version'].values
+ return False
diff --git a/sleekxmpp/plugins/xep_0115/__init__.py b/sleekxmpp/plugins/xep_0115/__init__.py
new file mode 100644
index 00000000..f4892f84
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0115/__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_0115.stanza import Capabilities
+from sleekxmpp.plugins.xep_0115.static import StaticCaps
+from sleekxmpp.plugins.xep_0115.caps import xep_0115
diff --git a/sleekxmpp/plugins/xep_0115/caps.py b/sleekxmpp/plugins/xep_0115/caps.py
new file mode 100644
index 00000000..289bb8d1
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0115/caps.py
@@ -0,0 +1,306 @@
+"""
+ 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 hashlib
+import base64
+
+import sleekxmpp
+from sleekxmpp.stanza import StreamFeatures, Presence, Iq
+from sleekxmpp.xmlstream import register_stanza_plugin, JID
+from sleekxmpp.xmlstream.handler import Callback
+from sleekxmpp.xmlstream.matcher import StanzaPath
+from sleekxmpp.exceptions import XMPPError, IqError, IqTimeout
+from sleekxmpp.plugins.base import base_plugin
+from sleekxmpp.plugins.xep_0115 import stanza, StaticCaps
+
+
+log = logging.getLogger(__name__)
+
+
+class xep_0115(base_plugin):
+
+ """
+ XEP-0115: Entity Capabalities
+ """
+
+ def plugin_init(self):
+ self.xep = '0115'
+ self.description = 'Entity Capabilities'
+ self.stanza = stanza
+
+ self.hashes = {'sha-1': hashlib.sha1,
+ 'md5': hashlib.md5}
+
+ self.hash = self.config.get('hash', 'sha-1')
+ self.caps_node = self.config.get('caps_node', None)
+ self.broadcast = self.config.get('broadcast', True)
+
+ if self.caps_node is None:
+ ver = sleekxmpp.__version__
+ self.caps_node = 'http://sleekxmpp.com/ver/%s' % ver
+
+ register_stanza_plugin(Presence, stanza.Capabilities)
+ register_stanza_plugin(StreamFeatures, stanza.Capabilities)
+
+ self._disco_ops = ['cache_caps',
+ 'get_caps',
+ 'assign_verstring',
+ 'get_verstring',
+ 'supports',
+ 'has_identity']
+
+ self.xmpp.register_handler(
+ Callback('Entity Capabilites',
+ StanzaPath('presence/caps'),
+ self._handle_caps))
+
+ self.xmpp.add_filter('out', self._filter_add_caps)
+
+ self.xmpp.add_event_handler('entity_caps', self._process_caps,
+ threaded=True)
+
+ if not self.xmpp.is_component:
+ self.xmpp.register_feature('caps',
+ self._handle_caps_feature,
+ restart=False,
+ order=10010)
+
+ def post_init(self):
+ base_plugin.post_init(self)
+ self.xmpp['xep_0030'].add_feature(stanza.Capabilities.namespace)
+
+ disco = self.xmpp['xep_0030']
+ self.static = StaticCaps(self.xmpp, disco.static)
+
+ for op in self._disco_ops:
+ disco._add_disco_op(op, getattr(self.static, op))
+
+ self._run_node_handler = disco._run_node_handler
+
+ disco.cache_caps = self.cache_caps
+ disco.update_caps = self.update_caps
+ disco.assign_verstring = self.assign_verstring
+ disco.get_verstring = self.get_verstring
+
+ def _filter_add_caps(self, stanza):
+ if isinstance(stanza, Presence) and self.broadcast:
+ ver = self.get_verstring(stanza['from'])
+ if ver:
+ stanza['caps']['node'] = self.caps_node
+ stanza['caps']['hash'] = self.hash
+ stanza['caps']['ver'] = ver
+ return stanza
+
+ def _handle_caps(self, presence):
+ if not self.xmpp.is_component:
+ if presence['from'] == self.xmpp.boundjid:
+ return
+ self.xmpp.event('entity_caps', presence)
+
+ def _handle_caps_feature(self, features):
+ # We already have a method to process presence with
+ # caps, so wrap things up and use that.
+ p = Presence()
+ p['from'] = self.xmpp.boundjid.domain
+ p.append(features['caps'])
+ self.xmpp.features.add('caps')
+
+ self.xmpp.event('entity_caps', p)
+
+ def _process_caps(self, pres):
+ if not pres['caps']['hash']:
+ log.debug("Received unsupported legacy caps.")
+ self.xmpp.event('entity_caps_legacy', pres)
+ return
+
+ existing_verstring = self.get_verstring(pres['from'].full)
+ if str(existing_verstring) == str(pres['caps']['ver']):
+ return
+
+ if pres['caps']['hash'] not in self.hashes:
+ try:
+ log.debug("Unknown caps hash: %s", pres['caps']['hash'])
+ self.xmpp['xep_003'].get_info(jid=pres['from'].full)
+ return
+ except XMPPError:
+ return
+
+ log.debug("New caps verification string: %s", pres['caps']['ver'])
+ try:
+ caps = self.xmpp['xep_0030'].get_info(
+ jid=pres['from'].full,
+ node='%s#%s' % (pres['caps']['node'],
+ pres['caps']['ver']))
+
+ if self._validate_caps(caps['disco_info'],
+ pres['caps']['hash'],
+ pres['caps']['ver']):
+ self.assign_verstring(pres['from'], pres['caps']['ver'])
+ except XMPPError:
+ log.debug("Could not retrieve disco#info results for caps")
+
+ def _validate_caps(self, caps, hash, check_verstring):
+ # Check Identities
+ full_ids = caps.get_identities(dedupe=False)
+ deduped_ids = caps.get_identities()
+ if len(full_ids) != len(deduped_ids):
+ log.debug("Duplicate disco identities found, invalid for caps")
+ return False
+
+ # Check Features
+
+ full_features = caps.get_features(dedupe=False)
+ deduped_features = caps.get_features()
+ if len(full_features) != len(deduped_features):
+ log.debug("Duplicate disco features found, invalid for caps")
+ return False
+
+ # Check Forms
+ form_types = []
+ deduped_form_types = set()
+ for stanza in caps['substanzas']:
+ if isinstance(stanza, self.xmpp['xep_0004'].stanza.Form):
+ if 'FORM_TYPE' in stanza['fields']:
+ f_type = tuple(stanza['fields']['FORM_TYPE']['value'])
+ form_types.append(f_type)
+ deduped_form_types.add(f_type)
+ if len(form_types) != len(deduped_form_types):
+ log.debug("Duplicated FORM_TYPE values, invalid for caps")
+ return False
+
+ if len(f_type) > 1:
+ deduped_type = set(f_type)
+ if len(f_type) != len(deduped_type):
+ log.debug("Extra FORM_TYPE data, invalid for caps")
+ return False
+
+ if stanza['fields']['FORM_TYPE']['type'] != 'hidden':
+ log.debug("Field FORM_TYPE type not 'hidden', ignoring form for caps")
+ caps.xml.remove(stanza.xml)
+ else:
+ log.debug("No FORM_TYPE found, ignoring form for caps")
+ caps.xml.remove(stanza.xml)
+
+ verstring = self.generate_verstring(caps, hash)
+ if verstring != check_verstring:
+ log.debug("Verification strings do not match: %s, %s" % (
+ verstring, check_verstring))
+ return False
+
+ self.cache_caps(verstring, caps)
+ return True
+
+ def generate_verstring(self, info, hash):
+ hash = self.hashes.get(hash, None)
+ if hash is None:
+ return None
+
+ S = ''
+
+ # Convert None to '' in the identities
+ def clean_identity(id):
+ return map(lambda i: i or '', id)
+ identities = map(clean_identity, info['identities'])
+
+ identities = sorted(('/'.join(i) for i in identities))
+ features = sorted(info['features'])
+
+ S += '<'.join(identities) + '<'
+ S += '<'.join(features) + '<'
+
+ form_types = {}
+
+ for stanza in info['substanzas']:
+ if isinstance(stanza, self.xmpp['xep_0004'].stanza.Form):
+ if 'FORM_TYPE' in stanza['fields']:
+ f_type = stanza['values']['FORM_TYPE']
+ if len(f_type):
+ f_type = f_type[0]
+ if f_type not in form_types:
+ form_types[f_type] = []
+ form_types[f_type].append(stanza)
+
+ sorted_forms = sorted(form_types.keys())
+ for f_type in sorted_forms:
+ for form in form_types[f_type]:
+ S += '%s<' % f_type
+ fields = sorted(form['fields'].keys())
+ fields.remove('FORM_TYPE')
+ for field in fields:
+ S += '%s<' % field
+ vals = form['fields'][field].get_value(convert=False)
+ if vals is None:
+ S += '<'
+ else:
+ if not isinstance(vals, list):
+ vals = [vals]
+ S += '<'.join(sorted(vals)) + '<'
+
+ binary = hash(S.encode('utf8')).digest()
+ return base64.b64encode(binary).decode('utf-8')
+
+ def update_caps(self, jid=None, node=None):
+ try:
+ info = self.xmpp['xep_0030'].get_info(jid, node, local=True)
+ if isinstance(info, Iq):
+ info = info['disco_info']
+ ver = self.generate_verstring(info, self.hash)
+ self.xmpp['xep_0030'].set_info(
+ jid=jid,
+ node='%s#%s' % (self.caps_node, ver),
+ info=info)
+ self.cache_caps(ver, info)
+ self.assign_verstring(jid, ver)
+
+ if self.broadcast:
+ # Check if we've sent directed presence. If we haven't, we
+ # can just send a normal presence stanza. If we have, then
+ # we will send presence to each contact individually so
+ # that we don't clobber existing statuses.
+ directed = False
+ for contact in self.xmpp.roster[jid]:
+ if self.xmpp.roster[jid][contact].last_status is not None:
+ directed = True
+ if not directed:
+ self.xmpp.roster[jid].send_last_presence()
+ else:
+ for contact in self.xmpp.roster[jid]:
+ self.xmpp.roster[jid][contact].send_last_presence()
+ except XMPPError:
+ return
+
+ def get_verstring(self, jid=None):
+ if jid in ('', None):
+ jid = self.xmpp.boundjid.full
+ if isinstance(jid, JID):
+ jid = jid.full
+ return self._run_node_handler('get_verstring', jid)
+
+ def assign_verstring(self, jid=None, verstring=None):
+ if jid in (None, ''):
+ jid = self.xmpp.boundjid.full
+ if isinstance(jid, JID):
+ jid = jid.full
+ return self._run_node_handler('assign_verstring', jid,
+ data={'verstring': verstring})
+
+ def cache_caps(self, verstring=None, info=None):
+ data = {'verstring': verstring, 'info': info}
+ return self._run_node_handler('cache_caps', None, None, data=data)
+
+ def get_caps(self, jid=None, verstring=None):
+ if verstring is None:
+ if jid is not None:
+ verstring = self.get_verstring(jid)
+ else:
+ return None
+ if isinstance(jid, JID):
+ jid = jid.full
+ data = {'verstring': verstring}
+ return self._run_node_handler('get_caps', jid, None, None, data)
diff --git a/sleekxmpp/plugins/xep_0115/stanza.py b/sleekxmpp/plugins/xep_0115/stanza.py
new file mode 100644
index 00000000..af02949b
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0115/stanza.py
@@ -0,0 +1,19 @@
+"""
+ 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 __future__ import unicode_literals
+
+from sleekxmpp.xmlstream import ElementBase, ET
+
+
+class Capabilities(ElementBase):
+
+ namespace = 'http://jabber.org/protocol/caps'
+ name = 'c'
+ plugin_attrib = 'caps'
+ interfaces = set(('hash', 'node', 'ver', 'ext'))
diff --git a/sleekxmpp/plugins/xep_0115/static.py b/sleekxmpp/plugins/xep_0115/static.py
new file mode 100644
index 00000000..204181d5
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0115/static.py
@@ -0,0 +1,147 @@
+"""
+ 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 sleekxmpp
+from sleekxmpp.xmlstream import JID
+from sleekxmpp.plugins.xep_0030 import StaticDisco
+
+
+log = logging.getLogger(__name__)
+
+
+class StaticCaps(object):
+
+ """
+ Extend the default StaticDisco implementation to provide
+ support for extended identity information.
+ """
+
+ def __init__(self, xmpp, static):
+ """
+ Augment the default XEP-0030 static handler object.
+
+ Arguments:
+ static -- The default static XEP-0030 handler object.
+ """
+ self.xmpp = xmpp
+ self.disco = self.xmpp['xep_0030']
+ self.caps = self.xmpp['xep_0115']
+ self.static = static
+ self.ver_cache = {}
+ self.jid_vers = {}
+
+ def supports(self, jid, node, ifrom, data):
+ """
+ Check if a JID supports a given feature.
+
+ The data parameter may provide:
+ feature -- The feature to check for support.
+ local -- If true, then the query is for a JID/node
+ combination handled by this Sleek instance and
+ no stanzas need to be sent.
+ Otherwise, a disco stanza must be sent to the
+ remove JID to retrieve the info.
+ cached -- If true, then look for the disco info data from
+ the local cache system. If no results are found,
+ send the query as usual. The self.use_cache
+ setting must be set to true for this option to
+ be useful. If set to false, then the cache will
+ be skipped, even if a result has already been
+ cached. Defaults to false.
+ """
+ feature = data.get('feature', None)
+
+ data = {'local': data.get('local', False),
+ 'cached': data.get('cached', True)}
+
+ if not feature:
+ return False
+
+ if node in (None, ''):
+ info = self.caps.get_caps(jid)
+ if info and feature in info['features']:
+ return True
+
+ try:
+ info = self.disco.get_info(jid=jid, node=node,
+ ifrom=ifrom, **data)
+ info = self.disco._wrap(ifrom, jid, info, True)
+ return feature in info['disco_info']['features']
+ except IqError:
+ return False
+ except IqTimeout:
+ return None
+
+ def has_identity(self, jid, node, ifrom, data):
+ """
+ Check if a JID has a given identity.
+
+ The data parameter may provide:
+ category -- The category of the identity to check.
+ itype -- The type of the identity to check.
+ lang -- The language of the identity to check.
+ local -- If true, then the query is for a JID/node
+ combination handled by this Sleek instance and
+ no stanzas need to be sent.
+ Otherwise, a disco stanza must be sent to the
+ remove JID to retrieve the info.
+ cached -- If true, then look for the disco info data from
+ the local cache system. If no results are found,
+ send the query as usual. The self.use_cache
+ setting must be set to true for this option to
+ be useful. If set to false, then the cache will
+ be skipped, even if a result has already been
+ cached. Defaults to false.
+ """
+ identity = (data.get('category', None),
+ data.get('itype', None),
+ data.get('lang', None))
+
+ data = {'local': data.get('local', False),
+ 'cached': data.get('cached', True)}
+
+ trunc = lambda i: (i[0], i[1], i[2])
+
+ if node in (None, ''):
+ info = self.caps.get_caps(jid)
+ if info and identity in map(trunc, info['identities']):
+ return True
+
+ try:
+ info = self.disco.get_info(jid=jid, node=node,
+ ifrom=ifrom, **data)
+ info = self.disco._wrap(ifrom, jid, info, True)
+ return identity in map(trunc, info['disco_info']['identities'])
+ except IqError:
+ return False
+ except IqTimeout:
+ return None
+
+ def cache_caps(self, jid, node, ifrom, data):
+ with self.static.lock:
+ verstring = data.get('verstring', None)
+ info = data.get('info', None)
+ if not verstring or not info:
+ return
+ self.ver_cache[verstring] = info
+
+ def assign_verstring(self, jid, node, ifrom, data):
+ with self.static.lock:
+ if isinstance(jid, JID):
+ jid = jid.full
+ self.jid_vers[jid] = data.get('verstring', None)
+
+ def get_verstring(self, jid, node, ifrom, data):
+ with self.static.lock:
+ return self.jid_vers.get(jid, None)
+
+ def get_caps(self, jid, node, ifrom, data):
+ with self.static.lock:
+ return self.ver_cache.get(data.get('verstring', None), None)
diff --git a/sleekxmpp/plugins/xep_0128/__init__.py b/sleekxmpp/plugins/xep_0128/__init__.py
new file mode 100644
index 00000000..3c6379a3
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0128/__init__.py
@@ -0,0 +1,10 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+from sleekxmpp.plugins.xep_0128.static import StaticExtendedDisco
+from sleekxmpp.plugins.xep_0128.extended_disco import xep_0128
diff --git a/sleekxmpp/plugins/xep_0128/extended_disco.py b/sleekxmpp/plugins/xep_0128/extended_disco.py
new file mode 100644
index 00000000..5bb78320
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0128/extended_disco.py
@@ -0,0 +1,101 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+import logging
+
+import sleekxmpp
+from sleekxmpp import Iq
+from sleekxmpp.xmlstream import register_stanza_plugin
+from sleekxmpp.plugins.base import base_plugin
+from sleekxmpp.plugins.xep_0004 import Form
+from sleekxmpp.plugins.xep_0030 import DiscoInfo
+from sleekxmpp.plugins.xep_0128 import StaticExtendedDisco
+
+
+class xep_0128(base_plugin):
+
+ """
+ XEP-0128: Service Discovery Extensions
+
+ Allow the use of data forms to add additional identity
+ information to disco#info results.
+
+ Also see <http://www.xmpp.org/extensions/xep-0128.html>.
+
+ Attributes:
+ disco -- A reference to the XEP-0030 plugin.
+ static -- Object containing the default set of static
+ node handlers.
+ xmpp -- The main SleekXMPP object.
+
+ Methods:
+ set_extended_info -- Set extensions to a disco#info result.
+ add_extended_info -- Add an extension to a disco#info result.
+ del_extended_info -- Remove all extensions from a disco#info result.
+ """
+
+ def plugin_init(self):
+ """Start the XEP-0128 plugin."""
+ self.xep = '0128'
+ self.description = 'Service Discovery Extensions'
+
+ self._disco_ops = ['set_extended_info',
+ 'add_extended_info',
+ 'del_extended_info']
+
+ register_stanza_plugin(DiscoInfo, Form, iterable=True)
+
+ def post_init(self):
+ """Handle cross-plugin dependencies."""
+ base_plugin.post_init(self)
+ self.disco = self.xmpp['xep_0030']
+ self.static = StaticExtendedDisco(self.disco.static)
+
+ self.disco.set_extended_info = self.set_extended_info
+ self.disco.add_extended_info = self.add_extended_info
+ self.disco.del_extended_info = self.del_extended_info
+
+ for op in self._disco_ops:
+ self.disco._add_disco_op(op, getattr(self.static, op))
+
+ def set_extended_info(self, jid=None, node=None, **kwargs):
+ """
+ Set additional, extended identity information to a node.
+
+ Replaces any existing extended information.
+
+ Arguments:
+ jid -- The JID to modify.
+ node -- The node to modify.
+ data -- Either a form, or a list of forms to use
+ as extended information, replacing any
+ existing extensions.
+ """
+ self.disco._run_node_handler('set_extended_info', jid, node, None, kwargs)
+
+ def add_extended_info(self, jid=None, node=None, **kwargs):
+ """
+ Add additional, extended identity information to a node.
+
+ Arguments:
+ jid -- The JID to modify.
+ node -- The node to modify.
+ data -- Either a form, or a list of forms to add
+ as extended information.
+ """
+ self.disco._run_node_handler('add_extended_info', jid, node, None, kwargs)
+
+ def del_extended_info(self, jid=None, node=None, **kwargs):
+ """
+ Remove all extended identity information to a node.
+
+ Arguments:
+ jid -- The JID to modify.
+ node -- The node to modify.
+ """
+ self.disco._run_node_handler('del_extended_info', jid, node, None, kwargs)
diff --git a/sleekxmpp/plugins/xep_0128/static.py b/sleekxmpp/plugins/xep_0128/static.py
new file mode 100644
index 00000000..427011c0
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0128/static.py
@@ -0,0 +1,73 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+import logging
+
+import sleekxmpp
+from sleekxmpp.plugins.xep_0030 import StaticDisco
+
+
+log = logging.getLogger(__name__)
+
+
+class StaticExtendedDisco(object):
+
+ """
+ Extend the default StaticDisco implementation to provide
+ support for extended identity information.
+ """
+
+ def __init__(self, static):
+ """
+ Augment the default XEP-0030 static handler object.
+
+ Arguments:
+ static -- The default static XEP-0030 handler object.
+ """
+ self.static = static
+
+ def set_extended_info(self, jid, node, ifrom, data):
+ """
+ Replace the extended identity data for a JID/node combination.
+
+ The data parameter may provide:
+ data -- Either a single data form, or a list of data forms.
+ """
+ with self.static.lock:
+ self.del_extended_info(jid, node, ifrom, data)
+ self.add_extended_info(jid, node, ifrom, data)
+
+ def add_extended_info(self, jid, node, ifrom, data):
+ """
+ Add additional extended identity data for a JID/node combination.
+
+ The data parameter may provide:
+ data -- Either a single data form, or a list of data forms.
+ """
+ with self.static.lock:
+ self.static.add_node(jid, node)
+
+ forms = data.get('data', [])
+ if not isinstance(forms, list):
+ forms = [forms]
+
+ info = self.static.get_node(jid, node)['info']
+ for form in forms:
+ info.append(form)
+
+ def del_extended_info(self, jid, node, ifrom, data):
+ """
+ Replace the extended identity data for a JID/node combination.
+
+ The data parameter is not used.
+ """
+ with self.static.lock:
+ if self.static.node_exists(jid, node):
+ info = self.static.get_node(jid, node)['info']
+ for form in info['substanza']:
+ info.xml.remove(form.xml)
diff --git a/sleekxmpp/plugins/xep_0199/__init__.py b/sleekxmpp/plugins/xep_0199/__init__.py
new file mode 100644
index 00000000..3444fe94
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0199/__init__.py
@@ -0,0 +1,10 @@
+"""
+ 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.plugins.xep_0199.stanza import Ping
+from sleekxmpp.plugins.xep_0199.ping import xep_0199
diff --git a/sleekxmpp/plugins/xep_0199/ping.py b/sleekxmpp/plugins/xep_0199/ping.py
new file mode 100644
index 00000000..a0f60532
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0199/ping.py
@@ -0,0 +1,175 @@
+"""
+ 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 time
+import logging
+
+import sleekxmpp
+from sleekxmpp import Iq
+from sleekxmpp.exceptions import IqError, IqTimeout
+from sleekxmpp.xmlstream import register_stanza_plugin
+from sleekxmpp.xmlstream.matcher import StanzaPath
+from sleekxmpp.xmlstream.handler import Callback
+from sleekxmpp.plugins.base import base_plugin
+from sleekxmpp.plugins.xep_0199 import stanza, Ping
+
+
+log = logging.getLogger(__name__)
+
+
+class xep_0199(base_plugin):
+
+ """
+ XEP-0199: XMPP Ping
+
+ Given that XMPP is based on TCP connections, it is possible for the
+ underlying connection to be terminated without the application's
+ awareness. Ping stanzas provide an alternative to whitespace based
+ keepalive methods for detecting lost connections.
+
+ Also see <http://www.xmpp.org/extensions/xep-0199.html>.
+
+ Attributes:
+ keepalive -- If True, periodically send ping requests
+ to the server. If a ping is not answered,
+ the connection will be reset.
+ frequency -- Time in seconds between keepalive pings.
+ Defaults to 300 seconds.
+ timeout -- Time in seconds to wait for a ping response.
+ Defaults to 30 seconds.
+ Methods:
+ send_ping -- Send a ping to a given JID, returning the
+ round trip time.
+ """
+
+ def plugin_init(self):
+ """
+ Start the XEP-0199 plugin.
+ """
+ self.description = 'XMPP Ping'
+ self.xep = '0199'
+ self.stanza = stanza
+
+ self.keepalive = self.config.get('keepalive', False)
+ self.frequency = float(self.config.get('frequency', 300))
+ self.timeout = self.config.get('timeout', 30)
+
+ register_stanza_plugin(Iq, Ping)
+
+ self.xmpp.register_handler(
+ Callback('Ping',
+ StanzaPath('iq@type=get/ping'),
+ self._handle_ping))
+
+ if self.keepalive:
+ self.xmpp.add_event_handler('session_start',
+ self._handle_keepalive,
+ threaded=True)
+ self.xmpp.add_event_handler('session_end',
+ self._handle_session_end)
+
+ def post_init(self):
+ """Handle cross-plugin dependencies."""
+ base_plugin.post_init(self)
+ self.xmpp['xep_0030'].add_feature(Ping.namespace)
+
+ def _handle_keepalive(self, event):
+ """
+ Begin periodic pinging of the server. If a ping is not
+ answered, the connection will be restarted.
+
+ The pinging interval can be adjused using self.frequency
+ before beginning processing.
+
+ Arguments:
+ event -- The session_start event.
+ """
+ def scheduled_ping():
+ """Send ping request to the server."""
+ log.debug("Pinging...")
+ try:
+ self.send_ping(self.xmpp.boundjid.host, self.timeout)
+ except IqError:
+ log.debug("Ping response was an error." + \
+ "Requesting Reconnect.")
+ self.xmpp.reconnect()
+ except IqTimeout:
+ log.debug("Did not recieve ping back in time." + \
+ "Requesting Reconnect.")
+ self.xmpp.reconnect()
+
+ self.xmpp.schedule('Ping Keep Alive',
+ self.frequency,
+ scheduled_ping,
+ repeat=True)
+
+ def _handle_session_end(self, event):
+ self.xmpp.scheduler.remove('Ping Keep Alive')
+
+ def _handle_ping(self, iq):
+ """
+ Automatically reply to ping requests.
+
+ Arguments:
+ iq -- The ping request.
+ """
+ log.debug("Pinged by %s", iq['from'])
+ iq.reply().send()
+
+ def send_ping(self, jid, timeout=None, errorfalse=False,
+ ifrom=None, block=True, callback=None):
+ """
+ Send a ping request and calculate the response time.
+
+ Arguments:
+ jid -- The JID that will receive the ping.
+ timeout -- Time in seconds to wait for a response.
+ Defaults to self.timeout.
+ errorfalse -- Indicates if False should be returned
+ if an error stanza is received. Defaults
+ to False.
+ ifrom -- Specifiy the sender JID.
+ block -- Indicate if execution should block until
+ a pong response is received. Defaults
+ to True.
+ callback -- Optional handler to execute when a pong
+ is received. Useful in conjunction with
+ the option block=False.
+ """
+ log.debug("Pinging %s", jid)
+ if timeout is None:
+ timeout = self.timeout
+
+ iq = self.xmpp.Iq()
+ iq['type'] = 'get'
+ iq['to'] = jid
+ iq['from'] = ifrom
+ iq.enable('ping')
+
+ start_time = time.clock()
+
+ try:
+ resp = iq.send(block=block,
+ timeout=timeout,
+ callback=callback)
+ except IqError as err:
+ resp = err.iq
+
+ end_time = time.clock()
+
+ delay = end_time - start_time
+
+ if not block:
+ return None
+
+ log.debug("Pong: %s %f", jid, delay)
+ return delay
+
+
+# Backwards compatibility for names
+xep_0199.sendPing = xep_0199.send_ping
diff --git a/sleekxmpp/plugins/xep_0199/stanza.py b/sleekxmpp/plugins/xep_0199/stanza.py
new file mode 100644
index 00000000..6586a763
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0199/stanza.py
@@ -0,0 +1,36 @@
+"""
+ 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 sleekxmpp
+from sleekxmpp.xmlstream import ElementBase
+
+
+class Ping(ElementBase):
+
+ """
+ Given that XMPP is based on TCP connections, it is possible for the
+ underlying connection to be terminated without the application's
+ awareness. Ping stanzas provide an alternative to whitespace based
+ keepalive methods for detecting lost connections.
+
+ Example ping stanza:
+ <iq type="get">
+ <ping xmlns="urn:xmpp:ping" />
+ </iq>
+
+ Stanza Interface:
+ None
+
+ Methods:
+ None
+ """
+
+ name = 'ping'
+ namespace = 'urn:xmpp:ping'
+ plugin_attrib = 'ping'
+ interfaces = set()
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..2c6faa4b
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0202/time.py
@@ -0,0 +1,91 @@
+"""
+ 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
+ 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..4a3ff368
--- /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/plugins/xep_0249/__init__.py b/sleekxmpp/plugins/xep_0249/__init__.py
new file mode 100644
index 00000000..e88d87ac
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0249/__init__.py
@@ -0,0 +1,10 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2011 Nathanael C. Fritz, Dalek
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+from sleekxmpp.plugins.xep_0249.stanza import Invite
+from sleekxmpp.plugins.xep_0249.invite import xep_0249
diff --git a/sleekxmpp/plugins/xep_0249/invite.py b/sleekxmpp/plugins/xep_0249/invite.py
new file mode 100644
index 00000000..95fcb37c
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0249/invite.py
@@ -0,0 +1,79 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2011 Nathanael C. Fritz, Dalek
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+import logging
+
+import sleekxmpp
+from sleekxmpp import Message
+from sleekxmpp.plugins.base import base_plugin
+from sleekxmpp.xmlstream import register_stanza_plugin
+from sleekxmpp.xmlstream.handler import Callback
+from sleekxmpp.xmlstream.matcher import StanzaPath
+from sleekxmpp.plugins.xep_0249 import Invite
+
+
+log = logging.getLogger(__name__)
+
+
+class xep_0249(base_plugin):
+
+ """
+ XEP-0249: Direct MUC Invitations
+ """
+
+ def plugin_init(self):
+ self.xep = "0249"
+ self.description = "Direct MUC Invitations"
+ self.stanza = sleekxmpp.plugins.xep_0249.stanza
+
+ self.xmpp.register_handler(
+ Callback('Direct MUC Invitations',
+ StanzaPath('message/groupchat_invite'),
+ self._handle_invite))
+
+ register_stanza_plugin(Message, Invite)
+
+ def post_init(self):
+ base_plugin.post_init(self)
+ self.xmpp['xep_0030'].add_feature(Invite.namespace)
+
+ def _handle_invite(self, msg):
+ """
+ Raise an event for all invitations received.
+ """
+ log.debug("Received direct muc invitation from %s to room %s",
+ msg['from'], msg['groupchat_invite']['jid'])
+
+ self.xmpp.event('groupchat_direct_invite', msg)
+
+ def send_invitation(self, jid, roomjid, password=None,
+ reason=None, ifrom=None):
+ """
+ Send a direct MUC invitation to an XMPP entity.
+
+ Arguments:
+ jid -- The JID of the entity that will receive
+ the invitation
+ roomjid -- the address of the groupchat room to be joined
+ password -- a password needed for entry into a
+ password-protected room (OPTIONAL).
+ reason -- a human-readable purpose for the invitation
+ (OPTIONAL).
+ """
+
+ msg = self.xmpp.Message()
+ msg['to'] = jid
+ if ifrom is not None:
+ msg['from'] = ifrom
+ msg['groupchat_invite']['jid'] = roomjid
+ if password is not None:
+ msg['groupchat_invite']['password'] = password
+ if reason is not None:
+ msg['groupchat_invite']['reason'] = reason
+
+ return msg.send()
diff --git a/sleekxmpp/plugins/xep_0249/stanza.py b/sleekxmpp/plugins/xep_0249/stanza.py
new file mode 100644
index 00000000..ba4060d7
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0249/stanza.py
@@ -0,0 +1,39 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2011 Nathanael C. Fritz, Dalek
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+from sleekxmpp.xmlstream import ElementBase
+
+
+class Invite(ElementBase):
+
+ """
+ XMPP allows for an agent in an MUC room to directly invite another
+ user to join the chat room (as opposed to a mediated invitation
+ done through the server).
+
+ Example invite stanza:
+ <message from='crone1@shakespeare.lit/desktop'
+ to='hecate@shakespeare.lit'>
+ <x xmlns='jabber:x:conference'
+ jid='darkcave@macbeth.shakespeare.lit'
+ password='cauldronburn'
+ reason='Hey Hecate, this is the place for all good witches!'/>
+ </message>
+
+ Stanza Interface:
+ jid -- The JID of the groupchat room
+ password -- The password used to gain entry in the room
+ (optional)
+ reason -- The reason for the invitation (optional)
+
+ """
+
+ name = "x"
+ namespace = "jabber:x:conference"
+ plugin_attrib = "groupchat_invite"
+ interfaces = ("jid", "password", "reason")
diff --git a/sleekxmpp/roster/__init__.py b/sleekxmpp/roster/__init__.py
new file mode 100644
index 00000000..4335d367
--- /dev/null
+++ b/sleekxmpp/roster/__init__.py
@@ -0,0 +1,12 @@
+"""
+ 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 JID
+from sleekxmpp.roster.item import RosterItem
+from sleekxmpp.roster.single import RosterNode
+from sleekxmpp.roster.multi import Roster
diff --git a/sleekxmpp/roster/item.py b/sleekxmpp/roster/item.py
new file mode 100644
index 00000000..bd7bbbde
--- /dev/null
+++ b/sleekxmpp/roster/item.py
@@ -0,0 +1,487 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+
+class RosterItem(object):
+
+ """
+ A RosterItem is a single entry in a roster node, and tracks
+ the subscription state and user annotations of a single JID.
+
+ Roster items may use an external datastore to persist roster data
+ across sessions. Client applications will not need to use this
+ functionality, but is intended for components that do not have their
+ roster persisted automatically by the XMPP server.
+
+ Roster items provide many methods for handling incoming presence
+ stanzas that ensure that response stanzas are sent according to
+ RFC 3921.
+
+ The external datastore is accessed through a provided interface
+ object which is stored in self.db. The interface object MUST
+ provide two methods: load and save, both of which are responsible
+ for working with a single roster item. A private dictionary,
+ self._db_state, is used to store any metadata needed by the
+ interface, such as the row ID of a roster item, etc.
+
+ Interface for self.db.load:
+ load(owner_jid, jid, db_state):
+ owner_jid -- The JID that owns the roster.
+ jid -- The JID of the roster item.
+ db_state -- A dictionary containing any data saved
+ by the interface object after a save()
+ call. Will typically have the equivalent
+ of a 'row_id' value.
+
+ Interface for self.db.save:
+ save(owner_jid, jid, item_state, db_state):
+ owner_jid -- The JID that owns the roster.
+ jid -- The JID of the roster item.
+ item_state -- A dictionary containing the fields:
+ 'from', 'to', 'pending_in', 'pending_out',
+ 'whitelisted', 'subscription', 'name',
+ and 'groups'.
+ db_state -- A dictionary provided for persisting
+ datastore specific information. Typically,
+ a value equivalent to 'row_id' will be
+ stored here.
+
+ State Fields:
+ from -- Indicates if a subscription of type 'from'
+ has been authorized.
+ to -- Indicates if a subscription of type 'to' has
+ been authorized.
+ pending_in -- Indicates if a subscription request has been
+ received from this JID and it has not been
+ authorized yet.
+ pending_out -- Indicates if a subscription request has been sent
+ to this JID and it has not been accepted yet.
+ subscription -- Returns one of: 'to', 'from', 'both', or 'none'
+ based on the states of from, to, pending_in,
+ and pending_out. Assignment to this value does
+ not affect the states of the other values.
+ whitelisted -- Indicates if a subscription request from this
+ JID should be automatically accepted.
+ name -- A user supplied alias for the JID.
+ groups -- A list of group names for the JID.
+
+ Attributes:
+ xmpp -- The main SleekXMPP instance.
+ owner -- The JID that owns the roster.
+ jid -- The JID for the roster item.
+ db -- Optional datastore interface object.
+ last_status -- The last presence sent to this JID.
+ resources -- A dictionary of online resources for this JID.
+ Will contain the fields 'show', 'status',
+ and 'priority'.
+
+ Methods:
+ load -- Retrieve the roster item from an
+ external datastore, if one was provided.
+ save -- Save the roster item to an external
+ datastore, if one was provided.
+ remove -- Remove a subscription to the JID and revoke
+ its whitelisted status.
+ subscribe -- Subscribe to the JID.
+ authorize -- Accept a subscription from the JID.
+ unauthorize -- Deny a subscription from the JID.
+ unsubscribe -- Unsubscribe from the JID.
+ send_presence -- Send a directed presence to the JID.
+ send_last_presence -- Resend the last sent presence.
+ handle_available -- Update the JID's resource information.
+ handle_unavailable -- Update the JID's resource information.
+ handle_subscribe -- Handle a subscription request.
+ handle_subscribed -- Handle a notice that a subscription request
+ was authorized by the JID.
+ handle_unsubscribe -- Handle an unsubscribe request.
+ handle_unsubscribed -- Handle a notice that a subscription was
+ removed by the JID.
+ handle_probe -- Handle a presence probe query.
+ """
+
+ def __init__(self, xmpp, jid, owner=None,
+ state=None, db=None, roster=None):
+ """
+ Create a new roster item.
+
+ Arguments:
+ xmpp -- The main SleekXMPP instance.
+ jid -- The item's JID.
+ owner -- The roster owner's JID. Defaults
+ so self.xmpp.boundjid.bare.
+ state -- A dictionary of initial state values.
+ db -- An optional interface to an external datastore.
+ roster -- The roster object containing this entry.
+ """
+ self.xmpp = xmpp
+ self.jid = jid
+ self.owner = owner or self.xmpp.boundjid.bare
+ self.last_status = None
+ self.resources = {}
+ self.roster = roster
+ self.db = db
+ self._state = state or {
+ 'from': False,
+ 'to': False,
+ 'pending_in': False,
+ 'pending_out': False,
+ 'whitelisted': False,
+ 'subscription': 'none',
+ 'name': '',
+ 'groups': []}
+ self._db_state = {}
+ self.load()
+
+ def set_backend(self, db=None):
+ """
+ Set the datastore interface object for the roster item.
+
+ Arguments:
+ db -- The new datastore interface.
+ """
+ self.db = db
+ self.load()
+
+ def load(self):
+ """
+ Load the item's state information from an external datastore,
+ if one has been provided.
+ """
+ if self.db:
+ item = self.db.load(self.owner, self.jid,
+ self._db_state)
+ if item:
+ self['name'] = item['name']
+ self['groups'] = item['groups']
+ self['from'] = item['from']
+ self['to'] = item['to']
+ self['whitelisted'] = item['whitelisted']
+ self['pending_out'] = item['pending_out']
+ self['pending_in'] = item['pending_in']
+ self['subscription'] = self._subscription()
+ return self._state
+ return None
+
+ def save(self):
+ """
+ Save the item's state information to an external datastore,
+ if one has been provided.
+ """
+ self['subscription'] = self._subscription()
+ if self.db:
+ self.db.save(self.owner, self.jid,
+ self._state, self._db_state)
+
+ def __getitem__(self, key):
+ """Return a state field's value."""
+ if key in self._state:
+ if key == 'subscription':
+ return self._subscription()
+ return self._state[key]
+ else:
+ raise KeyError
+
+ def __setitem__(self, key, value):
+ """
+ Set the value of a state field.
+
+ For boolean states, the values True, 'true', '1', 'on',
+ and 'yes' are accepted as True; all others are False.
+
+ Arguments:
+ key -- The state field to modify.
+ value -- The new value of the state field.
+ """
+ if key in self._state:
+ if key in ['name', 'subscription', 'groups']:
+ self._state[key] = value
+ else:
+ value = str(value).lower()
+ self._state[key] = value in ('true', '1', 'on', 'yes')
+ else:
+ raise KeyError
+
+ def _subscription(self):
+ """Return the proper subscription type based on current state."""
+ if self['to'] and self['from']:
+ return 'both'
+ elif self['from']:
+ return 'from'
+ elif self['to']:
+ return 'to'
+ else:
+ return 'none'
+
+ def remove(self):
+ """
+ Remove a JID's whitelisted status and unsubscribe if a
+ subscription exists.
+ """
+ if self['to']:
+ p = self.xmpp.Presence()
+ p['to'] = self.jid
+ p['type'] = 'unsubscribe'
+ if self.xmpp.is_component:
+ p['from'] = self.owner
+ p.send()
+ self['to'] = False
+ self['whitelisted'] = False
+ self.save()
+
+ def subscribe(self):
+ """Send a subscription request to the JID."""
+ p = self.xmpp.Presence()
+ p['to'] = self.jid
+ p['type'] = 'subscribe'
+ if self.xmpp.is_component:
+ p['from'] = self.owner
+ self['pending_out'] = True
+ self.save()
+ p.send()
+
+ def authorize(self):
+ """Authorize a received subscription request from the JID."""
+ self['from'] = True
+ self['pending_in'] = False
+ self.save()
+ self._subscribed()
+ self.send_last_presence()
+
+ def unauthorize(self):
+ """Deny a received subscription request from the JID."""
+ self['from'] = False
+ self['pending_in'] = False
+ self.save()
+ self._unsubscribed()
+ p = self.xmpp.Presence()
+ p['to'] = self.jid
+ p['type'] = 'unavailable'
+ if self.xmpp.is_component:
+ p['from'] = self.owner
+ p.send()
+
+ def _subscribed(self):
+ """Handle acknowledging a subscription."""
+ p = self.xmpp.Presence()
+ p['to'] = self.jid
+ p['type'] = 'subscribed'
+ if self.xmpp.is_component:
+ p['from'] = self.owner
+ p.send()
+
+ def unsubscribe(self):
+ """Unsubscribe from the JID."""
+ p = self.xmpp.Presence()
+ p['to'] = self.jid
+ p['type'] = 'unsubscribe'
+ if self.xmpp.is_component:
+ p['from'] = self.owner
+ self.save()
+ p.send()
+
+ def _unsubscribed(self):
+ """Handle acknowledging an unsubscribe request."""
+ p = self.xmpp.Presence()
+ p['to'] = self.jid
+ p['type'] = 'unsubscribed'
+ if self.xmpp.is_component:
+ p['from'] = self.owner
+ p.send()
+
+ def send_presence(self, ptype=None, pshow=None, pstatus=None,
+ ppriority=None, pnick=None):
+ """
+ Create, initialize, and send a Presence stanza.
+
+ Arguments:
+ pshow -- The presence's show value.
+ pstatus -- The presence's status message.
+ ppriority -- This connections' priority.
+ ptype -- The type of presence, such as 'subscribe'.
+ pnick -- Optional nickname of the presence's sender.
+ """
+ p = self.xmpp.make_presence(pshow=pshow,
+ pstatus=pstatus,
+ ppriority=ppriority,
+ ptype=ptype,
+ pnick=pnick,
+ pto=self.jid)
+ if self.xmpp.is_component:
+ p['from'] = self.owner
+ if p['type'] in p.showtypes or \
+ p['type'] in ['available', 'unavailable']:
+ self.last_status = p
+ p.send()
+
+ if not self.xmpp.sentpresence:
+ self.xmpp.event('sent_presence')
+ self.xmpp.sentpresence = True
+
+ def send_last_presence(self):
+ if self.last_status is None:
+ pres = self.roster.last_status
+ if pres is None:
+ self.send_presence()
+ else:
+ pres['to'] = self.jid
+ if self.xmpp.is_component:
+ pres['from'] = self.owner
+ else:
+ del pres['from']
+ pres.send()
+ else:
+ self.last_status.send()
+
+ def handle_available(self, presence):
+ resource = presence['from'].resource
+ data = {'status': presence['status'],
+ 'show': presence['show'],
+ 'priority': presence['priority']}
+ if not self.resources:
+ self.xmpp.event('got_online', presence)
+ if resource not in self.resources:
+ self.resources[resource] = {}
+ old_status = self.resources[resource].get('status', '')
+ old_show = self.resources[resource].get('show', None)
+ self.resources[resource].update(data)
+ if old_show != presence['show'] or old_status != presence['status']:
+ self.xmpp.event('changed_status', presence)
+
+ def handle_unavailable(self, presence):
+ resource = presence['from'].resource
+ if not self.resources:
+ return
+ if resource in self.resources:
+ del self.resources[resource]
+ self.xmpp.event('changed_status', presence)
+ if not self.resources:
+ self.xmpp.event('got_offline', presence)
+
+ def handle_subscribe(self, presence):
+ """
+ +------------------------------------------------------------------+
+ | EXISTING STATE | DELIVER? | NEW STATE |
+ +------------------------------------------------------------------+
+ | "None" | yes | "None + Pending In" |
+ | "None + Pending Out" | yes | "None + Pending Out/In" |
+ | "None + Pending In" | no | no state change |
+ | "None + Pending Out/In" | no | no state change |
+ | "To" | yes | "To + Pending In" |
+ | "To + Pending In" | no | no state change |
+ | "From" | no * | no state change |
+ | "From + Pending Out" | no * | no state change |
+ | "Both" | no * | no state change |
+ +------------------------------------------------------------------+
+ """
+ if self.xmpp.is_component:
+ if not self['from'] and not self['pending_in']:
+ self['pending_in'] = True
+ self.xmpp.event('roster_subscription_request', presence)
+ elif self['from']:
+ self._subscribed()
+ self.save()
+ else:
+ #server shouldn't send an invalid subscription request
+ self.xmpp.event('roster_subscription_request', presence)
+
+ def handle_subscribed(self, presence):
+ """
+ +------------------------------------------------------------------+
+ | EXISTING STATE | DELIVER? | NEW STATE |
+ +------------------------------------------------------------------+
+ | "None" | no | no state change |
+ | "None + Pending Out" | yes | "To" |
+ | "None + Pending In" | no | no state change |
+ | "None + Pending Out/In" | yes | "To + Pending In" |
+ | "To" | no | no state change |
+ | "To + Pending In" | no | no state change |
+ | "From" | no | no state change |
+ | "From + Pending Out" | yes | "Both" |
+ | "Both" | no | no state change |
+ +------------------------------------------------------------------+
+ """
+ if self.xmpp.is_component:
+ if not self['to'] and self['pending_out']:
+ self['pending_out'] = False
+ self['to'] = True
+ self.xmpp.event('roster_subscription_authorized', presence)
+ self.save()
+ else:
+ self.xmpp.event('roster_subscription_authorized', presence)
+
+ def handle_unsubscribe(self, presence):
+ """
+ +------------------------------------------------------------------+
+ | EXISTING STATE | DELIVER? | NEW STATE |
+ +------------------------------------------------------------------+
+ | "None" | no | no state change |
+ | "None + Pending Out" | no | no state change |
+ | "None + Pending In" | yes * | "None" |
+ | "None + Pending Out/In" | yes * | "None + Pending Out" |
+ | "To" | no | no state change |
+ | "To + Pending In" | yes * | "To" |
+ | "From" | yes * | "None" |
+ | "From + Pending Out" | yes * | "None + Pending Out |
+ | "Both" | yes * | "To" |
+ +------------------------------------------------------------------+
+ """
+ if self.xmpp.is_component:
+ if not self['from'] and self['pending_in']:
+ self['pending_in'] = False
+ self._unsubscribed()
+ elif self['from']:
+ self['from'] = False
+ self._unsubscribed()
+ self.xmpp.event('roster_subscription_remove', presence)
+ self.save()
+ else:
+ self.xmpp.event('roster_subscription_remove', presence)
+
+ def handle_unsubscribed(self, presence):
+ """
+ +------------------------------------------------------------------+
+ | EXISTING STATE | DELIVER? | NEW STATE |
+ +------------------------------------------------------------------+
+ | "None" | no | no state change |
+ | "None + Pending Out" | yes | "None" |
+ | "None + Pending In" | no | no state change |
+ | "None + Pending Out/In" | yes | "None + Pending In" |
+ | "To" | yes | "None" |
+ | "To + Pending In" | yes | "None + Pending In" |
+ | "From" | no | no state change |
+ | "From + Pending Out" | yes | "From" |
+ | "Both" | yes | "From" |
+ +------------------------------------------------------------------
+ """
+ if self.xmpp.is_component:
+ if not self['to'] and self['pending_out']:
+ self['pending_out'] = False
+ elif self['to'] and not self['pending_out']:
+ self['to'] = False
+ self.xmpp.event('roster_subscription_removed', presence)
+ self.save()
+ else:
+ self.xmpp.event('roster_subscription_removed', presence)
+
+ def handle_probe(self, presence):
+ if self['to']:
+ self.send_last_presence()
+ if self['pending_out']:
+ self.subscribe()
+ if not self['to']:
+ self._unsubscribed()
+
+ def reset(self):
+ """
+ Forgot current resource presence information as part of
+ a roster reset request.
+ """
+ self.resources = {}
+
+ def __repr__(self):
+ return repr(self._state)
diff --git a/sleekxmpp/roster/multi.py b/sleekxmpp/roster/multi.py
new file mode 100644
index 00000000..28876814
--- /dev/null
+++ b/sleekxmpp/roster/multi.py
@@ -0,0 +1,189 @@
+"""
+ 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 JID
+from sleekxmpp.roster import RosterNode
+
+
+class Roster(object):
+
+ """
+ SleekXMPP's roster manager.
+
+ The roster is divided into "nodes", where each node is responsible
+ for a single JID. While the distinction is not strictly necessary
+ for client connections, it is a necessity for components that use
+ multiple JIDs.
+
+ Rosters may be stored and persisted in an external datastore. An
+ interface object to the datastore that loads and saves roster items may
+ be provided. See the documentation for the RosterItem class for the
+ methods that the datastore interface object must provide.
+
+ Attributes:
+ xmpp -- The main SleekXMPP instance.
+ db -- Optional interface object to an external datastore.
+ auto_authorize -- Default auto_authorize value for new roster nodes.
+ Defaults to True.
+ auto_subscribe -- Default auto_subscribe value for new roster nodes.
+ Defaults to True.
+
+ Methods:
+ add -- Create a new roster node for a JID.
+ send_presence -- Shortcut for sending a presence stanza.
+ """
+
+ def __init__(self, xmpp, db=None):
+ """
+ Create a new roster.
+
+ Arguments:
+ xmpp -- The main SleekXMPP instance.
+ db -- Optional interface object to a datastore.
+ """
+ self.xmpp = xmpp
+ self.db = db
+ self._auto_authorize = True
+ self._auto_subscribe = True
+ self._rosters = {}
+
+ if self.db:
+ for node in self.db.entries(None, {}):
+ self.add(node)
+
+ def __getitem__(self, key):
+ """
+ Return the roster node for a JID.
+
+ A new roster node will be created if one
+ does not already exist.
+
+ Arguments:
+ key -- Return the roster for this JID.
+ """
+ if isinstance(key, JID):
+ key = key.bare
+ if key is None:
+ key = self.xmpp.boundjid.bare
+ if key not in self._rosters:
+ self.add(key)
+ self._rosters[key].auto_authorize = self.auto_authorize
+ self._rosters[key].auto_subscribe = self.auto_subscribe
+ return self._rosters[key]
+
+ def keys(self):
+ """Return the JIDs managed by the roster."""
+ return self._rosters.keys()
+
+ def __iter__(self):
+ """Iterate over the roster nodes."""
+ return self._rosters.__iter__()
+
+ def add(self, node):
+ """
+ Add a new roster node for the given JID.
+
+ Arguments:
+ node -- The JID for the new roster node.
+ """
+ if isinstance(node, JID):
+ node = node.bare
+ if node not in self._rosters:
+ self._rosters[node] = RosterNode(self.xmpp, node, self.db)
+
+ def set_backend(self, db=None):
+ """
+ Set the datastore interface object for the roster.
+
+ Arguments:
+ db -- The new datastore interface.
+ """
+ self.db = db
+ for node in self.db.entries(None, {}):
+ self.add(node)
+ for node in self._rosters:
+ self._rosters[node].set_backend(db)
+
+ def reset(self):
+ """
+ Reset the state of the roster to forget any current
+ presence information. Useful after a disconnection occurs.
+ """
+ for node in self:
+ self[node].reset()
+
+ def send_presence(self, pshow=None, pstatus=None, ppriority=None,
+ pto=None, pfrom=None, ptype=None, pnick=None):
+ """
+ Create, initialize, and send a Presence stanza.
+
+ Forwards the send request to the appropriate roster to
+ perform the actual sending.
+
+ Arguments:
+ pshow -- The presence's show value.
+ pstatus -- The presence's status message.
+ ppriority -- This connections' priority.
+ pto -- The recipient of a directed presence.
+ ptype -- The type of presence, such as 'subscribe'.
+ pfrom -- The sender of the presence.
+ pnick -- Optional nickname of the presence's sender.
+ """
+ self[pfrom].send_presence(ptype=ptype,
+ pshow=pshow,
+ pstatus=pstatus,
+ ppriority=ppriority,
+ pnick=pnick,
+ pto=pto)
+
+ @property
+ def auto_authorize(self):
+ """
+ Auto accept or deny subscription requests.
+
+ If True, auto accept subscription requests.
+ If False, auto deny subscription requests.
+ If None, don't automatically respond.
+ """
+ return self._auto_authorize
+
+ @auto_authorize.setter
+ def auto_authorize(self, value):
+ """
+ Auto accept or deny subscription requests.
+
+ If True, auto accept subscription requests.
+ If False, auto deny subscription requests.
+ If None, don't automatically respond.
+ """
+ self._auto_authorize = value
+ for node in self._rosters:
+ self._rosters[node].auto_authorize = value
+
+ @property
+ def auto_subscribe(self):
+ """
+ Auto send requests for mutual subscriptions.
+
+ If True, auto send mutual subscription requests.
+ """
+ return self._auto_subscribe
+
+ @auto_subscribe.setter
+ def auto_subscribe(self, value):
+ """
+ Auto send requests for mutual subscriptions.
+
+ If True, auto send mutual subscription requests.
+ """
+ self._auto_subscribe = value
+ for node in self._rosters:
+ self._rosters[node].auto_subscribe = value
+
+ def __repr__(self):
+ return repr(self._rosters)
diff --git a/sleekxmpp/roster/single.py b/sleekxmpp/roster/single.py
new file mode 100644
index 00000000..633f23f6
--- /dev/null
+++ b/sleekxmpp/roster/single.py
@@ -0,0 +1,304 @@
+"""
+ 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 JID
+from sleekxmpp.roster import RosterItem
+
+
+class RosterNode(object):
+
+ """
+ A roster node is a roster for a single JID.
+
+ Attributes:
+ xmpp -- The main SleekXMPP instance.
+ jid -- The JID that owns the roster node.
+ db -- Optional interface to an external datastore.
+ auto_authorize -- Determines how authorizations are handled:
+ True -- Accept all subscriptions.
+ False -- Reject all subscriptions.
+ None -- Subscriptions must be
+ manually authorized.
+ Defaults to True.
+ auto_subscribe -- Determines if bi-directional subscriptions
+ are created after automatically authrorizing
+ a subscription request.
+ Defaults to True
+ last_status -- The last sent presence status that was broadcast
+ to all contact JIDs.
+
+ Methods:
+ add -- Add a JID to the roster.
+ update -- Update a JID's subscription information.
+ subscribe -- Subscribe to a JID.
+ unsubscribe -- Unsubscribe from a JID.
+ remove -- Remove a JID from the roster.
+ presence -- Return presence information for a JID's resources.
+ send_presence -- Shortcut for sending a presence stanza.
+ """
+
+ def __init__(self, xmpp, jid, db=None):
+ """
+ Create a roster node for a JID.
+
+ Arguments:
+ xmpp -- The main SleekXMPP instance.
+ jid -- The JID that owns the roster.
+ db -- Optional interface to an external datastore.
+ """
+ self.xmpp = xmpp
+ self.jid = jid
+ self.db = db
+ self.auto_authorize = True
+ self.auto_subscribe = True
+ self.last_status = None
+ self._jids = {}
+
+ if self.db:
+ for jid in self.db.entries(self.jid):
+ self.add(jid)
+
+ def __getitem__(self, key):
+ """
+ Return the roster item for a subscribed JID.
+
+ A new item entry will be created if one does not already exist.
+ """
+ if isinstance(key, JID):
+ key = key.bare
+ if key not in self._jids:
+ self.add(key, save=True)
+ return self._jids[key]
+
+ def __len__(self):
+ """Return the number of JIDs referenced by the roster."""
+ return len(self._jids)
+
+ def keys(self):
+ """Return a list of all subscribed JIDs."""
+ return self._jids.keys()
+
+ def has_jid(self, jid):
+ """Returns whether the roster has a JID."""
+ return jid in self._jids
+
+ def groups(self):
+ """Return a dictionary mapping group names to JIDs."""
+ result = {}
+ for jid in self._jids:
+ for group in self._jids[jid]['groups']:
+ if group not in result:
+ result[group] = []
+ result[group].append(jid)
+ return result
+
+ def __iter__(self):
+ """Iterate over the roster items."""
+ return self._jids.__iter__()
+
+ def set_backend(self, db=None):
+ """
+ Set the datastore interface object for the roster node.
+
+ Arguments:
+ db -- The new datastore interface.
+ """
+ self.db = db
+ for jid in self.db.entries(self.jid):
+ self.add(jid)
+ for jid in self._jids:
+ self._jids[jid].set_backend(db)
+
+ def add(self, jid, name='', groups=None, afrom=False, ato=False,
+ pending_in=False, pending_out=False, whitelisted=False,
+ save=False):
+ """
+ Add a new roster item entry.
+
+ Arguments:
+ jid -- The JID for the roster item.
+ name -- An alias for the JID.
+ groups -- A list of group names.
+ afrom -- Indicates if the JID has a subscription state
+ of 'from'. Defaults to False.
+ ato -- Indicates if the JID has a subscription state
+ of 'to'. Defaults to False.
+ pending_in -- Indicates if the JID has sent a subscription
+ request to this connection's JID.
+ Defaults to False.
+ pending_out -- Indicates if a subscription request has been sent
+ to this JID.
+ Defaults to False.
+ whitelisted -- Indicates if a subscription request from this JID
+ should be automatically authorized.
+ Defaults to False.
+ save -- Indicates if the item should be persisted
+ immediately to an external datastore,
+ if one is used.
+ Defaults to False.
+ """
+ if isinstance(jid, JID):
+ key = jid.bare
+ else:
+ key = jid
+
+ state = {'name': name,
+ 'groups': groups or [],
+ 'from': afrom,
+ 'to': ato,
+ 'pending_in': pending_in,
+ 'pending_out': pending_out,
+ 'whitelisted': whitelisted,
+ 'subscription': 'none'}
+ self._jids[key] = RosterItem(self.xmpp, jid, self.jid,
+ state=state, db=self.db,
+ roster=self)
+ if save:
+ self._jids[key].save()
+
+ def subscribe(self, jid):
+ """
+ Subscribe to the given JID.
+
+ Arguments:
+ jid -- The JID to subscribe to.
+ """
+ self[jid].subscribe()
+
+ def unsubscribe(self, jid):
+ """
+ Unsubscribe from the given JID.
+
+ Arguments:
+ jid -- The JID to unsubscribe from.
+ """
+ self[jid].unsubscribe()
+
+ def remove(self, jid):
+ """
+ Remove a JID from the roster.
+
+ Arguments:
+ jid -- The JID to remove.
+ """
+ self[jid].remove()
+ if not self.xmpp.is_component:
+ return self.update(jid, subscription='remove')
+
+ def update(self, jid, name=None, subscription=None, groups=[],
+ block=True, timeout=None, callback=None):
+ """
+ Update a JID's subscription information.
+
+ Arguments:
+ jid -- The JID to update.
+ name -- Optional alias for the JID.
+ subscription -- The subscription state. May be one of: 'to',
+ 'from', 'both', 'none', or 'remove'.
+ groups -- A list of group names.
+ block -- Specify if the roster request will block
+ until a response is received, or a timeout
+ occurs. Defaults to True.
+ timeout -- The length of time (in seconds) to wait
+ for a response before continuing if blocking
+ is used. Defaults to self.response_timeout.
+ callback -- Optional reference to a stream handler function.
+ Will be executed when the roster is received.
+ Implies block=False.
+ """
+ self[jid]['name'] = name
+ self[jid]['groups'] = groups
+ self[jid].save()
+
+ if not self.xmpp.is_component:
+ iq = self.xmpp.Iq()
+ iq['type'] = 'set'
+ iq['roster']['items'] = {jid: {'name': name,
+ 'subscription': subscription,
+ 'groups': groups}}
+
+ return iq.send(block, timeout, callback)
+
+ def presence(self, jid, resource=None):
+ """
+ Retrieve the presence information of a JID.
+
+ May return either all online resources' status, or
+ a single resource's status.
+
+ Arguments:
+ jid -- The JID to lookup.
+ resource -- Optional resource for returning
+ only the status of a single connection.
+ """
+ if resource is None:
+ return self[jid].resources
+
+ default_presence = {'status': '',
+ 'priority': 0,
+ 'show': ''}
+ return self[jid].resources.get(resource,
+ default_presence)
+
+ def reset(self):
+ """
+ Reset the state of the roster to forget any current
+ presence information. Useful after a disconnection occurs.
+ """
+ for jid in self:
+ self[jid].reset()
+
+ def send_presence(self, ptype=None, pshow=None, pstatus=None,
+ ppriority=None, pnick=None, pto=None):
+ """
+ Create, initialize, and send a Presence stanza.
+
+ If no recipient is specified, send the presence immediately.
+ Otherwise, forward the send request to the recipient's roster
+ entry for processing.
+
+ Arguments:
+ pshow -- The presence's show value.
+ pstatus -- The presence's status message.
+ ppriority -- This connections' priority.
+ pto -- The recipient of a directed presence.
+ ptype -- The type of presence, such as 'subscribe'.
+ """
+ if pto:
+ self[pto].send_presence(ptype, pshow, pstatus,
+ ppriority, pnick)
+ else:
+ p = self.xmpp.make_presence(pshow=pshow,
+ pstatus=pstatus,
+ ppriority=ppriority,
+ ptype=ptype,
+ pnick=pnick)
+ if self.xmpp.is_component:
+ p['from'] = self.jid
+ if p['type'] in p.showtypes or \
+ p['type'] in ['available', 'unavailable']:
+ self.last_status = p
+ p.send()
+
+ if not self.xmpp.sentpresence:
+ self.xmpp.event('sent_presence')
+ self.xmpp.sentpresence = True
+
+ def send_last_presence(self):
+ if self.last_status is None:
+ self.send_presence()
+ else:
+ pres = self.last_status
+ if self.xmpp.is_component:
+ pres['from'] = self.jid
+ else:
+ del pres['from']
+ pres.send()
+
+ def __repr__(self):
+ return repr(self._jids)
diff --git a/sleekxmpp/stanza/__init__.py b/sleekxmpp/stanza/__init__.py
new file mode 100644
index 00000000..4bd37dc5
--- /dev/null
+++ b/sleekxmpp/stanza/__init__.py
@@ -0,0 +1,15 @@
+"""
+ 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.stanza.error import Error
+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/atom.py b/sleekxmpp/stanza/atom.py
new file mode 100644
index 00000000..244ef315
--- /dev/null
+++ b/sleekxmpp/stanza/atom.py
@@ -0,0 +1,26 @@
+"""
+ 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
+
+
+class AtomEntry(ElementBase):
+
+ """
+ A simple Atom feed entry.
+
+ Stanza Interface:
+ title -- The title of the Atom feed entry.
+ summary -- The summary of the Atom feed entry.
+ """
+
+ namespace = 'http://www.w3.org/2005/Atom'
+ name = 'entry'
+ plugin_attrib = 'entry'
+ interfaces = set(('title', 'summary'))
+ sub_interfaces = set(('title', 'summary'))
diff --git a/sleekxmpp/stanza/error.py b/sleekxmpp/stanza/error.py
new file mode 100644
index 00000000..d985f729
--- /dev/null
+++ b/sleekxmpp/stanza/error.py
@@ -0,0 +1,146 @@
+"""
+ 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, ET, register_stanza_plugin
+
+
+class Error(ElementBase):
+
+ """
+ XMPP stanzas of type 'error' should include an <error> stanza that
+ describes the nature of the error and how it should be handled.
+
+ Use the 'XEP-0086: Error Condition Mappings' plugin to include error
+ codes used in older XMPP versions.
+
+ Example error stanza:
+ <error type="cancel" code="404">
+ <item-not-found xmlns="urn:ietf:params:xml:ns:xmpp-stanzas" />
+ <text xmlns="urn:ietf:params:xml:ns:xmpp-stanzas">
+ The item was not found.
+ </text>
+ </error>
+
+ Stanza Interface:
+ code -- The error code used in older XMPP versions.
+ condition -- The name of the condition element.
+ text -- Human readable description of the error.
+ type -- Error type indicating how the error should be handled.
+
+ Attributes:
+ conditions -- The set of allowable error condition elements.
+ condition_ns -- The namespace for the condition element.
+ types -- A set of values indicating how the error
+ should be treated.
+
+ Methods:
+ setup -- Overrides ElementBase.setup.
+ get_condition -- Retrieve the name of the condition element.
+ set_condition -- Add a condition element.
+ del_condition -- Remove the condition element.
+ get_text -- Retrieve the contents of the <text> element.
+ set_text -- Set the contents of the <text> element.
+ del_text -- Remove the <text> element.
+ """
+
+ namespace = 'jabber:client'
+ name = 'error'
+ plugin_attrib = 'error'
+ interfaces = set(('code', 'condition', 'text', 'type'))
+ sub_interfaces = set(('text',))
+ plugin_attrib_map = {}
+ plugin_tag_map = {}
+ conditions = set(('bad-request', 'conflict', 'feature-not-implemented',
+ 'forbidden', 'gone', 'internal-server-error',
+ 'item-not-found', 'jid-malformed', 'not-acceptable',
+ 'not-allowed', 'not-authorized', 'payment-required',
+ 'recipient-unavailable', 'redirect',
+ 'registration-required', 'remote-server-not-found',
+ 'remote-server-timeout', 'resource-constraint',
+ 'service-unavailable', 'subscription-required',
+ 'undefined-condition', 'unexpected-request'))
+ condition_ns = 'urn:ietf:params:xml:ns:xmpp-stanzas'
+ types = set(('cancel', 'continue', 'modify', 'auth', 'wait'))
+
+ 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.
+ """
+ if ElementBase.setup(self, xml):
+ #If we had to generate XML then set default values.
+ self['type'] = 'cancel'
+ self['condition'] = 'feature-not-implemented'
+ if self.parent is not None:
+ self.parent()['type'] = 'error'
+
+ def get_condition(self):
+ """Return the condition element's name."""
+ for child in self.xml.getchildren():
+ if "{%s}" % self.condition_ns in child.tag:
+ cond = child.tag.split('}', 1)[-1]
+ if cond in self.conditions:
+ return cond
+ return ''
+
+ 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.condition_ns, 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
+
+ def get_text(self):
+ """Retrieve the contents of the <text> element."""
+ return self._get_sub_text('{%s}text' % self.condition_ns)
+
+ def set_text(self, value):
+ """
+ Set the contents of the <text> element.
+
+ Arguments:
+ value -- The new contents for the <text> element.
+ """
+ self._set_sub_text('{%s}text' % self.condition_ns, text=value)
+ return self
+
+ def del_text(self):
+ """Remove the <text> element."""
+ self._del_sub('{%s}text' % self.condition_ns)
+ return self
+
+
+# To comply with PEP8, method names now use underscores.
+# Deprecated method names are re-mapped for backwards compatibility.
+Error.getCondition = Error.get_condition
+Error.setCondition = Error.set_condition
+Error.delCondition = Error.del_condition
+Error.getText = Error.get_text
+Error.setText = Error.set_text
+Error.delText = Error.del_text
diff --git a/sleekxmpp/stanza/htmlim.py b/sleekxmpp/stanza/htmlim.py
new file mode 100644
index 00000000..d21a74e1
--- /dev/null
+++ b/sleekxmpp/stanza/htmlim.py
@@ -0,0 +1,86 @@
+"""
+ 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.stanza import Message
+from sleekxmpp.xmlstream import ElementBase, ET, register_stanza_plugin
+
+
+class HTMLIM(ElementBase):
+
+ """
+ XEP-0071: XHTML-IM defines a method for embedding XHTML content
+ within a <message> stanza so that lightweight markup can be used
+ to format the message contents and to create links.
+
+ Only a subset of XHTML is recommended for use with XHTML-IM.
+ See the full spec at 'http://xmpp.org/extensions/xep-0071.html'
+ for more information.
+
+ Example stanza:
+ <message to="user@example.com">
+ <body>Non-html message content.</body>
+ <html xmlns="http://jabber.org/protocol/xhtml-im">
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <p><b>HTML!</b></p>
+ </body>
+ </html>
+ </message>
+
+ Stanza Interface:
+ body -- The contents of the HTML body tag.
+
+ Methods:
+ setup -- Overrides ElementBase.setup.
+ get_body -- Return the HTML body contents.
+ set_body -- Set the HTML body contents.
+ del_body -- Remove the HTML body contents.
+ """
+
+ namespace = 'http://jabber.org/protocol/xhtml-im'
+ name = 'html'
+ interfaces = set(('body',))
+ plugin_attrib = name
+
+ def set_body(self, html):
+ """
+ Set the contents of the HTML body.
+
+ Arguments:
+ html -- Either a string or XML object. If the top level
+ element is not <body> with a namespace of
+ 'http://www.w3.org/1999/xhtml', it will be wrapped.
+ """
+ if isinstance(html, str):
+ html = ET.XML(html)
+ if html.tag != '{http://www.w3.org/1999/xhtml}body':
+ body = ET.Element('{http://www.w3.org/1999/xhtml}body')
+ body.append(html)
+ self.xml.append(body)
+ else:
+ self.xml.append(html)
+
+ def get_body(self):
+ """Return the contents of the HTML body."""
+ html = self.xml.find('{http://www.w3.org/1999/xhtml}body')
+ if html is None:
+ return ''
+ return html
+
+ def del_body(self):
+ """Remove the HTML body contents."""
+ if self.parent is not None:
+ self.parent().xml.remove(self.xml)
+
+
+register_stanza_plugin(Message, HTMLIM)
+
+# To comply with PEP8, method names now use underscores.
+# Deprecated method names are re-mapped for backwards compatibility.
+HTMLIM.setBody = HTMLIM.set_body
+HTMLIM.getBody = HTMLIM.get_body
+HTMLIM.delBody = HTMLIM.del_body
diff --git a/sleekxmpp/stanza/iq.py b/sleekxmpp/stanza/iq.py
new file mode 100644
index 00000000..f05dad17
--- /dev/null
+++ b/sleekxmpp/stanza/iq.py
@@ -0,0 +1,241 @@
+"""
+ 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.stanza import Error
+from sleekxmpp.stanza.rootstanza import RootStanza
+from sleekxmpp.xmlstream import StanzaBase, ET
+from sleekxmpp.xmlstream.handler import Waiter, Callback
+from sleekxmpp.xmlstream.matcher import MatcherId
+from sleekxmpp.exceptions import IqTimeout, IqError
+
+
+class Iq(RootStanza):
+
+ """
+ XMPP <iq> stanzas, or info/query stanzas, are XMPP's method of
+ requesting and modifying information, similar to HTTP's GET and
+ POST methods.
+
+ Each <iq> stanza must have an 'id' value which associates the
+ stanza with the response stanza. XMPP entities must always
+ be given a response <iq> stanza with a type of 'result' after
+ sending a stanza of type 'get' or 'set'.
+
+ Most uses cases for <iq> stanzas will involve adding a <query>
+ element whose namespace indicates the type of information
+ desired. However, some custom XMPP applications use <iq> stanzas
+ as a carrier stanza for an application-specific protocol instead.
+
+ Example <iq> Stanzas:
+ <iq to="user@example.com" type="get" id="314">
+ <query xmlns="http://jabber.org/protocol/disco#items" />
+ </iq>
+
+ <iq to="user@localhost" type="result" id="17">
+ <query xmlns='jabber:iq:roster'>
+ <item jid='otheruser@example.net'
+ name='John Doe'
+ subscription='both'>
+ <group>Friends</group>
+ </item>
+ </query>
+ </iq>
+
+ Stanza Interface:
+ query -- The namespace of the <query> element if one exists.
+
+ Attributes:
+ types -- May be one of: get, set, result, or error.
+
+ Methods:
+ __init__ -- Overrides StanzaBase.__init__.
+ unhandled -- Send error if there are no handlers.
+ set_payload -- Overrides StanzaBase.set_payload.
+ set_query -- Add or modify a <query> element.
+ get_query -- Return the namespace of the <query> element.
+ del_query -- Remove the <query> element.
+ reply -- Overrides StanzaBase.reply
+ send -- Overrides StanzaBase.send
+ """
+
+ namespace = 'jabber:client'
+ name = 'iq'
+ interfaces = set(('type', 'to', 'from', 'id', 'query'))
+ types = set(('get', 'result', 'set', 'error'))
+ plugin_attrib = name
+
+ def __init__(self, *args, **kwargs):
+ """
+ Initialize a new <iq> stanza with an 'id' value.
+
+ Overrides StanzaBase.__init__.
+ """
+ StanzaBase.__init__(self, *args, **kwargs)
+ if self['id'] == '':
+ if self.stream is not None:
+ self['id'] = self.stream.new_id()
+ else:
+ self['id'] = '0'
+
+ def unhandled(self):
+ """
+ Send a feature-not-implemented error if the stanza is not handled.
+
+ Overrides StanzaBase.unhandled.
+ """
+ if self['type'] in ('get', 'set'):
+ self.reply()
+ self['error']['condition'] = 'feature-not-implemented'
+ self['error']['text'] = 'No handlers registered for this request.'
+ self.send()
+
+ def set_payload(self, value):
+ """
+ Set the XML contents of the <iq> stanza.
+
+ Arguments:
+ value -- An XML object to use as the <iq> stanza's contents
+ """
+ self.clear()
+ StanzaBase.set_payload(self, value)
+ return self
+
+ def set_query(self, value):
+ """
+ Add or modify a <query> element.
+
+ Query elements are differentiated by their namespace.
+
+ Arguments:
+ value -- The namespace of the <query> element.
+ """
+ query = self.xml.find("{%s}query" % value)
+ if query is None and value:
+ self.clear()
+ query = ET.Element("{%s}query" % value)
+ self.xml.append(query)
+ return self
+
+ def get_query(self):
+ """Return the namespace of the <query> element."""
+ for child in self.xml.getchildren():
+ if child.tag.endswith('query'):
+ ns = child.tag.split('}')[0]
+ if '{' in ns:
+ ns = ns[1:]
+ return ns
+ return ''
+
+ def del_query(self):
+ """Remove the <query> element."""
+ for child in self.xml.getchildren():
+ if child.tag.endswith('query'):
+ self.xml.remove(child)
+ return self
+
+ def reply(self, clear=True):
+ """
+ Send a reply <iq> stanza.
+
+ Overrides StanzaBase.reply
+
+ Sets the 'type' to 'result' in addition to the default
+ StanzaBase.reply behavior.
+
+ Arguments:
+ clear -- Indicates if existing content should be
+ removed before replying. Defaults to True.
+ """
+ self['type'] = 'result'
+ StanzaBase.reply(self, clear)
+ return self
+
+ def send(self, block=True, timeout=None, callback=None, now=False):
+ """
+ Send an <iq> stanza over the XML stream.
+
+ The send call can optionally block until a response is received or
+ a timeout occurs. Be aware that using blocking in non-threaded event
+ handlers can drastically impact performance. Otherwise, a callback
+ handler can be provided that will be executed when the Iq stanza's
+ result reply is received. Be aware though that that the callback
+ handler will not be executed in its own thread.
+
+ Using both block and callback is not recommended, and only the
+ callback argument will be used in that case.
+
+ Overrides StanzaBase.send
+
+ Arguments:
+ block -- Specify if the send call will block until a response
+ is received, or a timeout occurs. Defaults to True.
+ timeout -- The length of time (in seconds) to wait for a response
+ before exiting the send call if blocking is used.
+ Defaults to sleekxmpp.xmlstream.RESPONSE_TIMEOUT
+ callback -- Optional reference to a stream handler function. Will
+ be executed when a reply stanza is received.
+ now -- Indicates if the send queue should be skipped and send
+ the stanza immediately. Used during stream
+ initialization. Defaults to False.
+ """
+ if timeout is None:
+ timeout = self.stream.response_timeout
+ if callback is not None and self['type'] in ('get', 'set'):
+ handler_name = 'IqCallback_%s' % self['id']
+ handler = Callback(handler_name,
+ MatcherId(self['id']),
+ callback,
+ once=True)
+ self.stream.register_handler(handler)
+ StanzaBase.send(self, now=now)
+ return handler_name
+ elif block and self['type'] in ('get', 'set'):
+ waitfor = Waiter('IqWait_%s' % self['id'], MatcherId(self['id']))
+ self.stream.register_handler(waitfor)
+ StanzaBase.send(self, now=now)
+ result = waitfor.wait(timeout)
+ if not result:
+ raise IqTimeout(self)
+ if result['type'] == 'error':
+ raise IqError(result)
+ return result
+ else:
+ return StanzaBase.send(self, now=now)
+
+ def _set_stanza_values(self, values):
+ """
+ Set multiple stanza interface values using a dictionary.
+
+ Stanza plugin values may be set usind nested dictionaries.
+
+ If the interface 'query' is given, then it will be set
+ last to avoid duplication of the <query /> element.
+
+ Overrides ElementBase._set_stanza_values.
+
+ Arguments:
+ values -- A dictionary mapping stanza interface with values.
+ Plugin interfaces may accept a nested dictionary that
+ will be used recursively.
+ """
+ query = values.get('query', '')
+ if query:
+ del values['query']
+ StanzaBase._set_stanza_values(self, values)
+ self['query'] = query
+ else:
+ StanzaBase._set_stanza_values(self, values)
+ return self
+
+
+# To comply with PEP8, method names now use underscores.
+# Deprecated method names are re-mapped for backwards compatibility.
+Iq.setPayload = Iq.set_payload
+Iq.getQuery = Iq.get_query
+Iq.setQuery = Iq.set_query
+Iq.delQuery = Iq.del_query
diff --git a/sleekxmpp/stanza/message.py b/sleekxmpp/stanza/message.py
new file mode 100644
index 00000000..19d4d9e2
--- /dev/null
+++ b/sleekxmpp/stanza/message.py
@@ -0,0 +1,157 @@
+"""
+ 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.stanza import Error
+from sleekxmpp.stanza.rootstanza import RootStanza
+from sleekxmpp.xmlstream import StanzaBase, ET
+
+
+class Message(RootStanza):
+
+ """
+ XMPP's <message> stanzas are a "push" mechanism to send information
+ to other XMPP entities without requiring a response.
+
+ Chat clients will typically use <message> stanzas that have a type
+ of either "chat" or "groupchat".
+
+ When handling a message event, be sure to check if the message is
+ an error response.
+
+ Example <message> stanzas:
+ <message to="user1@example.com" from="user2@example.com">
+ <body>Hi!</body>
+ </message>
+
+ <message type="groupchat" to="room@conference.example.com">
+ <body>Hi everyone!</body>
+ </message>
+
+ Stanza Interface:
+ body -- The main contents of the message.
+ subject -- An optional description of the message's contents.
+ mucroom -- (Read-only) The name of the MUC room that sent the message.
+ mucnick -- (Read-only) The MUC nickname of message's sender.
+
+ Attributes:
+ types -- May be one of: normal, chat, headline, groupchat, or error.
+
+ Methods:
+ setup -- Overrides StanzaBase.setup.
+ chat -- Set the message type to 'chat'.
+ normal -- Set the message type to 'normal'.
+ reply -- Overrides StanzaBase.reply
+ get_type -- Overrides StanzaBase interface
+ get_mucroom -- Return the name of the MUC room of the message.
+ set_mucroom -- Dummy method to prevent assignment.
+ del_mucroom -- Dummy method to prevent deletion.
+ get_mucnick -- Return the MUC nickname of the message's sender.
+ set_mucnick -- Dummy method to prevent assignment.
+ del_mucnick -- Dummy method to prevent deletion.
+ """
+
+ namespace = 'jabber:client'
+ name = 'message'
+ interfaces = set(('type', 'to', 'from', 'id', 'body', 'subject',
+ 'mucroom', 'mucnick'))
+ sub_interfaces = set(('body', 'subject'))
+ plugin_attrib = name
+ types = set((None, 'normal', 'chat', 'headline', 'error', 'groupchat'))
+
+ def get_type(self):
+ """
+ Return the message type.
+
+ Overrides default stanza interface behavior.
+
+ Returns 'normal' if no type attribute is present.
+ """
+ return self._get_attr('type', 'normal')
+
+ def chat(self):
+ """Set the message type to 'chat'."""
+ self['type'] = 'chat'
+ return self
+
+ def normal(self):
+ """Set the message type to 'normal'."""
+ self['type'] = 'normal'
+ return self
+
+ def reply(self, body=None, clear=True):
+ """
+ Create a message reply.
+
+ Overrides StanzaBase.reply.
+
+ Sets proper 'to' attribute if the message is from a MUC, and
+ adds a message body if one is given.
+
+ Arguments:
+ body -- Optional text content for the message.
+ clear -- Indicates if existing content should be removed
+ before replying. Defaults to True.
+ """
+ StanzaBase.reply(self, clear)
+ if self['type'] == 'groupchat':
+ self['to'] = self['to'].bare
+
+ del self['id']
+
+ if body is not None:
+ self['body'] = body
+ return self
+
+ def get_mucroom(self):
+ """
+ Return the name of the MUC room where the message originated.
+
+ Read-only stanza interface.
+ """
+ if self['type'] == 'groupchat':
+ return self['from'].bare
+ else:
+ return ''
+
+ def get_mucnick(self):
+ """
+ Return the nickname of the MUC user that sent the message.
+
+ Read-only stanza interface.
+ """
+ if self['type'] == 'groupchat':
+ return self['from'].resource
+ else:
+ return ''
+
+ def set_mucroom(self, value):
+ """Dummy method to prevent modification."""
+ pass
+
+ def del_mucroom(self):
+ """Dummy method to prevent deletion."""
+ pass
+
+ def set_mucnick(self, value):
+ """Dummy method to prevent modification."""
+ pass
+
+ def del_mucnick(self):
+ """Dummy method to prevent deletion."""
+ pass
+
+
+# To comply with PEP8, method names now use underscores.
+# Deprecated method names are re-mapped for backwards compatibility.
+Message.getType = Message.get_type
+Message.getMucroom = Message.get_mucroom
+Message.setMucroom = Message.set_mucroom
+Message.delMucroom = Message.del_mucroom
+Message.getMucnick = Message.get_mucnick
+Message.setMucnick = Message.set_mucnick
+Message.delMucnick = Message.del_mucnick
diff --git a/sleekxmpp/stanza/nick.py b/sleekxmpp/stanza/nick.py
new file mode 100644
index 00000000..1e23d34f
--- /dev/null
+++ b/sleekxmpp/stanza/nick.py
@@ -0,0 +1,78 @@
+"""
+ 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.stanza import Message, Presence
+from sleekxmpp.xmlstream import ElementBase, ET, register_stanza_plugin
+
+
+class Nick(ElementBase):
+
+ """
+ XEP-0172: User Nickname allows the addition of a <nick> element
+ in several stanza types, including <message> and <presence> stanzas.
+
+ The nickname contained in a <nick> should be the global, friendly or
+ informal name chosen by the owner of a bare JID. The <nick> element
+ may be included when establishing communications with new entities,
+ such as normal XMPP users or MUC services.
+
+ The nickname contained in a <nick> element will not necessarily be
+ the same as the nickname used in a MUC.
+
+ Example stanzas:
+ <message to="user@example.com">
+ <nick xmlns="http://jabber.org/nick/nick">The User</nick>
+ <body>...</body>
+ </message>
+
+ <presence to="otheruser@example.com" type="subscribe">
+ <nick xmlns="http://jabber.org/nick/nick">The User</nick>
+ </presence>
+
+ Stanza Interface:
+ nick -- A global, friendly or informal name chosen by a user.
+
+ Methods:
+ setup -- Overrides ElementBase.setup.
+ get_nick -- Return the nickname in the <nick> element.
+ set_nick -- Add a <nick> element with the given nickname.
+ del_nick -- Remove the <nick> element.
+ """
+
+ namespace = 'http://jabber.org/protocol/nick'
+ name = 'nick'
+ plugin_attrib = name
+ interfaces = set(('nick',))
+
+ def set_nick(self, nick):
+ """
+ Add a <nick> element with the given nickname.
+
+ Arguments:
+ nick -- A human readable, informal name.
+ """
+ self.xml.text = nick
+
+ def get_nick(self):
+ """Return the nickname in the <nick> element."""
+ return self.xml.text
+
+ def del_nick(self):
+ """Remove the <nick> element."""
+ if self.parent is not None:
+ self.parent().xml.remove(self.xml)
+
+
+register_stanza_plugin(Message, Nick)
+register_stanza_plugin(Presence, Nick)
+
+# To comply with PEP8, method names now use underscores.
+# Deprecated method names are re-mapped for backwards compatibility.
+Nick.setNick = Nick.set_nick
+Nick.getNick = Nick.get_nick
+Nick.delNick = Nick.del_nick
diff --git a/sleekxmpp/stanza/presence.py b/sleekxmpp/stanza/presence.py
new file mode 100644
index 00000000..c8706233
--- /dev/null
+++ b/sleekxmpp/stanza/presence.py
@@ -0,0 +1,180 @@
+"""
+ 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.stanza import Error
+from sleekxmpp.stanza.rootstanza import RootStanza
+from sleekxmpp.xmlstream import StanzaBase, ET
+
+
+class Presence(RootStanza):
+
+ """
+ XMPP's <presence> stanza allows entities to know the status of other
+ clients and components. Since it is currently the only multi-cast
+ stanza in XMPP, many extensions add more information to <presence>
+ stanzas to broadcast to every entry in the roster, such as
+ capabilities, music choices, or locations (XEP-0115: Entity Capabilities
+ and XEP-0163: Personal Eventing Protocol).
+
+ Since <presence> stanzas are broadcast when an XMPP entity changes
+ its status, the bulk of the traffic in an XMPP network will be from
+ <presence> stanzas. Therefore, do not include more information than
+ necessary in a status message or within a <presence> stanza in order
+ to help keep the network running smoothly.
+
+ Example <presence> stanzas:
+ <presence />
+
+ <presence from="user@example.com">
+ <show>away</show>
+ <status>Getting lunch.</status>
+ <priority>5</priority>
+ </presence>
+
+ <presence type="unavailable" />
+
+ <presence to="user@otherhost.com" type="subscribe" />
+
+ Stanza Interface:
+ priority -- A value used by servers to determine message routing.
+ show -- The type of status, such as away or available for chat.
+ status -- Custom, human readable status message.
+
+ Attributes:
+ types -- One of: available, unavailable, error, probe,
+ subscribe, subscribed, unsubscribe,
+ and unsubscribed.
+ showtypes -- One of: away, chat, dnd, and xa.
+
+ Methods:
+ setup -- Overrides StanzaBase.setup
+ reply -- Overrides StanzaBase.reply
+ set_show -- Set the value of the <show> element.
+ get_type -- Get the value of the type attribute or <show> element.
+ set_type -- Set the value of the type attribute or <show> element.
+ get_priority -- Get the value of the <priority> element.
+ set_priority -- Set the value of the <priority> element.
+ """
+
+ namespace = 'jabber:client'
+ name = 'presence'
+ interfaces = set(('type', 'to', 'from', 'id', 'show',
+ 'status', 'priority'))
+ sub_interfaces = set(('show', 'status', 'priority'))
+ plugin_attrib = name
+
+ types = set(('available', 'unavailable', 'error', 'probe', 'subscribe',
+ 'subscribed', 'unsubscribe', 'unsubscribed'))
+ showtypes = set(('dnd', 'chat', 'xa', 'away'))
+
+ def exception(self, e):
+ """
+ Override exception passback for presence.
+ """
+ pass
+
+ def set_show(self, show):
+ """
+ Set the value of the <show> element.
+
+ Arguments:
+ show -- Must be one of: away, chat, dnd, or xa.
+ """
+ if show is None:
+ self._del_sub('show')
+ elif show in self.showtypes:
+ self._set_sub_text('show', text=show)
+ return self
+
+ def get_type(self):
+ """
+ Return the value of the <presence> stanza's type attribute, or
+ the value of the <show> element.
+ """
+ out = self._get_attr('type')
+ if not out:
+ out = self['show']
+ if not out or out is None:
+ out = 'available'
+ return out
+
+ def set_type(self, value):
+ """
+ Set the type attribute's value, and the <show> element
+ if applicable.
+
+ Arguments:
+ value -- Must be in either self.types or self.showtypes.
+ """
+ if value in self.types:
+ self['show'] = None
+ if value == 'available':
+ value = ''
+ self._set_attr('type', value)
+ elif value in self.showtypes:
+ self['show'] = value
+ return self
+
+ def del_type(self):
+ """
+ Remove both the type attribute and the <show> element.
+ """
+ self._del_attr('type')
+ self._del_sub('show')
+
+ def set_priority(self, value):
+ """
+ Set the entity's priority value. Some server use priority to
+ determine message routing behavior.
+
+ Bot clients should typically use a priority of 0 if the same
+ JID is used elsewhere by a human-interacting client.
+
+ Arguments:
+ value -- An integer value greater than or equal to 0.
+ """
+ self._set_sub_text('priority', text=str(value))
+
+ def get_priority(self):
+ """
+ Return the value of the <presence> element as an integer.
+ """
+ p = self._get_sub_text('priority')
+ if not p:
+ p = 0
+ try:
+ return int(p)
+ except ValueError:
+ # The priority is not a number: we consider it 0 as a default
+ return 0
+
+ def reply(self, clear=True):
+ """
+ Set the appropriate presence reply type.
+
+ Overrides StanzaBase.reply.
+
+ Arguments:
+ clear -- Indicates if the stanza contents should be removed
+ before replying. Defaults to True.
+ """
+ if self['type'] == 'unsubscribe':
+ self['type'] = 'unsubscribed'
+ elif self['type'] == 'subscribe':
+ self['type'] = 'subscribed'
+ return StanzaBase.reply(self, clear)
+
+
+# To comply with PEP8, method names now use underscores.
+# Deprecated method names are re-mapped for backwards compatibility.
+Presence.setShow = Presence.set_show
+Presence.getType = Presence.get_type
+Presence.setType = Presence.set_type
+Presence.delType = Presence.get_type
+Presence.getPriority = Presence.get_priority
+Presence.setPriority = Presence.set_priority
diff --git a/sleekxmpp/stanza/rootstanza.py b/sleekxmpp/stanza/rootstanza.py
new file mode 100644
index 00000000..2ac47d8b
--- /dev/null
+++ b/sleekxmpp/stanza/rootstanza.py
@@ -0,0 +1,87 @@
+"""
+ 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 traceback
+import sys
+
+from sleekxmpp.exceptions import XMPPError, IqError, IqTimeout
+from sleekxmpp.stanza import Error
+from sleekxmpp.xmlstream import ET, StanzaBase, register_stanza_plugin
+
+
+log = logging.getLogger(__name__)
+
+
+class RootStanza(StanzaBase):
+
+ """
+ A top-level XMPP stanza in an XMLStream.
+
+ The RootStanza class provides a more XMPP specific exception
+ handler than provided by the generic StanzaBase class.
+
+ Methods:
+ exception -- Overrides StanzaBase.exception
+ """
+
+ def exception(self, e):
+ """
+ Create and send an error reply.
+
+ Typically called when an event handler raises an exception.
+ The error's type and text content are based on the exception
+ object's type and content.
+
+ Overrides StanzaBase.exception.
+
+ Arguments:
+ e -- Exception object
+ """
+ if isinstance(e, IqError):
+ # We received an Iq error reply, but it wasn't caught
+ # locally. Using the condition/text from that error
+ # response could leak too much information, so we'll
+ # only use a generic error here.
+ self.reply()
+ self['error']['condition'] = 'undefined-condition'
+ self['error']['text'] = 'External error'
+ self['error']['type'] = 'cancel'
+ log.warning('You should catch IqError exceptions')
+ self.send()
+ elif isinstance(e, IqTimeout):
+ self.reply()
+ self['error']['condition'] = 'remote-server-timeout'
+ self['error']['type'] = 'wait'
+ log.warning('You should catch IqTimeout exceptions')
+ self.send()
+ elif isinstance(e, XMPPError):
+ # We raised this deliberately
+ self.reply(clear=e.clear)
+ self['error']['condition'] = e.condition
+ self['error']['text'] = e.text
+ self['error']['type'] = e.etype
+ if e.extension is not None:
+ # Extended error tag
+ extxml = ET.Element("{%s}%s" % (e.extension_ns, e.extension),
+ e.extension_args)
+ self['error'].append(extxml)
+ self.send()
+ else:
+ # We probably didn't raise this on purpose, so send an error stanza
+ self.reply()
+ self['error']['condition'] = 'undefined-condition'
+ self['error']['text'] = "SleekXMPP got into trouble."
+ self['error']['type'] = 'cancel'
+ self.send()
+ # log the error
+ log.exception('Error handling {%s}%s stanza' , self.namespace, self.name)
+ # Finally raise the exception to a global exception handler
+ self.stream.exception(e)
+
+register_stanza_plugin(RootStanza, Error)
diff --git a/sleekxmpp/stanza/roster.py b/sleekxmpp/stanza/roster.py
new file mode 100644
index 00000000..c7ea4147
--- /dev/null
+++ b/sleekxmpp/stanza/roster.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.
+"""
+
+from sleekxmpp.stanza import Iq
+from sleekxmpp.xmlstream import JID
+from sleekxmpp.xmlstream import ET, ElementBase, register_stanza_plugin
+
+
+class Roster(ElementBase):
+
+ """
+ Example roster stanzas:
+ <iq type="set">
+ <query xmlns="jabber:iq:roster">
+ <item jid="user@example.com" subscription="both" name="User">
+ <group>Friends</group>
+ </item>
+ </query>
+ </iq>
+
+ Stanza Inteface:
+ items -- A dictionary of roster entries contained
+ in the stanza.
+
+ Methods:
+ get_items -- Return a dictionary of roster entries.
+ set_items -- Add <item> elements.
+ del_items -- Remove all <item> elements.
+ """
+
+ namespace = 'jabber:iq:roster'
+ name = 'query'
+ plugin_attrib = 'roster'
+ interfaces = set(('items',))
+
+ def set_items(self, items):
+ """
+ Set the roster entries in the <roster> stanza.
+
+ Uses a dictionary using JIDs as keys, where each entry is itself
+ a dictionary that contains:
+ name -- An alias or nickname for the JID.
+ subscription -- The subscription type. Can be one of 'to',
+ 'from', 'both', 'none', or 'remove'.
+ groups -- A list of group names to which the JID
+ has been assigned.
+
+ Arguments:
+ items -- A dictionary of roster entries.
+ """
+ self.del_items()
+ for jid in items:
+ item = RosterItem()
+ item.values = items[jid]
+ item['jid'] = jid
+ self.append(item)
+ return self
+
+ def get_items(self):
+ """
+ Return a dictionary of roster entries.
+
+ Each item is keyed using its JID, and contains:
+ name -- An assigned alias or nickname for the JID.
+ subscription -- The subscription type. Can be one of 'to',
+ 'from', 'both', 'none', or 'remove'.
+ groups -- A list of group names to which the JID has
+ been assigned.
+ """
+ items = {}
+ for item in self['substanzas']:
+ if isinstance(item, RosterItem):
+ items[item['jid']] = item.values
+ # Remove extra JID reference to keep everything
+ # backward compatible
+ del items[item['jid']]['jid']
+ return items
+
+ def del_items(self):
+ """
+ Remove all <item> elements from the roster stanza.
+ """
+ for item in self['substanzas']:
+ if isinstance(item, RosterItem):
+ self.xml.remove(item.xml)
+
+
+class RosterItem(ElementBase):
+ namespace = 'jabber:iq:roster'
+ name = 'item'
+ plugin_attrib = 'item'
+ interfaces = set(('jid', 'name', 'subscription', 'ask',
+ 'approved', 'groups'))
+
+ def get_groups(self):
+ groups = []
+ for group in self.xml.findall('{%s}group' % self.namespace):
+ groups.append(group.text)
+ return groups
+
+ def set_groups(self, values):
+ self.del_groups()
+ for group in values:
+ group_xml = ET.Element('{%s}group' % self.namespace)
+ group_xml.text = group
+ self.xml.append(group_xml)
+
+ def del_groups(self):
+ for group in self.xml.findall('{%s}group' % self.namespace):
+ self.xmp.remove(group)
+
+
+
+
+register_stanza_plugin(Iq, Roster)
+register_stanza_plugin(Roster, RosterItem, iterable=True)
+
+# To comply with PEP8, method names now use underscores.
+# Deprecated method names are re-mapped for backwards compatibility.
+Roster.setItems = Roster.set_items
+Roster.getItems = Roster.get_items
+Roster.delItems = Roster.del_items
diff --git a/sleekxmpp/stanza/stream_error.py b/sleekxmpp/stanza/stream_error.py
new file mode 100644
index 00000000..cf59a7fa
--- /dev/null
+++ b/sleekxmpp/stanza/stream_error.py
@@ -0,0 +1,69 @@
+"""
+ 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.stanza.error import Error
+from sleekxmpp.xmlstream import StanzaBase, ElementBase, ET
+from sleekxmpp.xmlstream import register_stanza_plugin
+
+
+class StreamError(Error, StanzaBase):
+
+ """
+ XMPP stanzas of type 'error' should include an <error> stanza that
+ describes the nature of the error and how it should be handled.
+
+ Use the 'XEP-0086: Error Condition Mappings' plugin to include error
+ codes used in older XMPP versions.
+
+ The stream:error stanza is used to provide more information for
+ error that occur with the underlying XML stream itself, and not
+ a particular stanza.
+
+ Note: The StreamError stanza is mostly the same as the normal
+ Error stanza, but with different namespaces and
+ condition names.
+
+ Example error stanza:
+ <stream:error>
+ <not-well-formed xmlns="urn:ietf:params:xml:ns:xmpp-streams" />
+ <text xmlns="urn:ietf:params:xml:ns:xmpp-streams">
+ XML was not well-formed.
+ </text>
+ </stream:error>
+
+ Stanza Interface:
+ condition -- The name of the condition element.
+ text -- Human readable description of the error.
+
+ Attributes:
+ conditions -- The set of allowable error condition elements.
+ condition_ns -- The namespace for the condition element.
+
+ Methods:
+ setup -- Overrides ElementBase.setup.
+ get_condition -- Retrieve the name of the condition element.
+ set_condition -- Add a condition element.
+ del_condition -- Remove the condition element.
+ get_text -- Retrieve the contents of the <text> element.
+ set_text -- Set the contents of the <text> element.
+ del_text -- Remove the <text> element.
+ """
+
+ namespace = 'http://etherx.jabber.org/streams'
+ interfaces = set(('condition', 'text'))
+ conditions = set((
+ 'bad-format', 'bad-namespace-prefix', 'conflict',
+ 'connection-timeout', 'host-gone', 'host-unknown',
+ 'improper-addressing', 'internal-server-error', 'invalid-from',
+ 'invalid-namespace', 'invalid-xml', 'not-authorized',
+ 'not-well-formed', 'policy-violation', 'remote-connection-failed',
+ 'reset', 'resource-constraint', 'restricted-xml', 'see-other-host',
+ 'system-shutdown', 'undefined-condition', 'unsupported-encoding',
+ 'unsupported-feature', 'unsupported-stanza-type',
+ 'unsupported-version'))
+ condition_ns = 'urn:ietf:params:xml:ns:xmpp-streams'
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/__init__.py b/sleekxmpp/test/__init__.py
new file mode 100644
index 00000000..54d4dc57
--- /dev/null
+++ b/sleekxmpp/test/__init__.py
@@ -0,0 +1,11 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+from sleekxmpp.test.mocksocket import TestSocket
+from sleekxmpp.test.livesocket import TestLiveSocket
+from sleekxmpp.test.sleektest import *
diff --git a/sleekxmpp/test/livesocket.py b/sleekxmpp/test/livesocket.py
new file mode 100644
index 00000000..80d63307
--- /dev/null
+++ b/sleekxmpp/test/livesocket.py
@@ -0,0 +1,174 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+import socket
+import threading
+try:
+ import queue
+except ImportError:
+ import Queue as queue
+
+
+class TestLiveSocket(object):
+
+ """
+ A live test socket that reads and writes to queues in
+ addition to an actual networking socket.
+
+ Methods:
+ next_sent -- Return the next sent stanza.
+ next_recv -- Return the next received stanza.
+ recv_data -- Dummy method to have same interface as TestSocket.
+ recv -- Read the next stanza from the socket.
+ send -- Write a stanza to the socket.
+ makefile -- Dummy call, returns self.
+ read -- Read the next stanza from the socket.
+ """
+
+ def __init__(self, *args, **kwargs):
+ """
+ Create a new, live test socket.
+
+ Arguments:
+ Same as arguments for socket.socket
+ """
+ self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ self.recv_buffer = []
+ self.recv_queue = queue.Queue()
+ self.send_queue = queue.Queue()
+ self.send_queue_lock = threading.Lock()
+ self.recv_queue_lock = threading.Lock()
+ self.is_live = True
+
+ def __getattr__(self, name):
+ """
+ Return attribute values of internal, live socket.
+
+ Arguments:
+ name -- Name of the attribute requested.
+ """
+
+ return getattr(self.socket, name)
+
+ # ------------------------------------------------------------------
+ # Testing Interface
+
+ def disconnect_errror(self):
+ """
+ Used to simulate a socket disconnection error.
+
+ Not used by live sockets.
+ """
+ try:
+ self.socket.shutdown()
+ self.socket.close()
+ except:
+ pass
+
+ def next_sent(self, timeout=None):
+ """
+ Get the next stanza that has been sent.
+
+ Arguments:
+ timeout -- Optional timeout for waiting for a new value.
+ """
+ args = {'block': False}
+ if timeout is not None:
+ args = {'block': True, 'timeout': timeout}
+ try:
+ return self.send_queue.get(**args)
+ except:
+ return None
+
+ def next_recv(self, timeout=None):
+ """
+ Get the next stanza that has been received.
+
+ Arguments:
+ timeout -- Optional timeout for waiting for a new value.
+ """
+ args = {'block': False}
+ if timeout is not None:
+ args = {'block': True, 'timeout': timeout}
+ try:
+ if self.recv_buffer:
+ return self.recv_buffer.pop(0)
+ else:
+ return self.recv_queue.get(**args)
+ except:
+ return None
+
+ def recv_data(self, data):
+ """
+ Add data to a receive buffer for cases when more than a single stanza
+ was received.
+ """
+ self.recv_buffer.append(data)
+
+ # ------------------------------------------------------------------
+ # Socket Interface
+
+ def recv(self, *args, **kwargs):
+ """
+ Read data from the socket.
+
+ Store a copy in the receive queue.
+
+ Arguments:
+ Placeholders. Same as for socket.recv.
+ """
+ data = self.socket.recv(*args, **kwargs)
+ with self.recv_queue_lock:
+ self.recv_queue.put(data)
+ return data
+
+ def send(self, data):
+ """
+ Send data on the socket.
+
+ Store a copy in the send queue.
+
+ Arguments:
+ data -- String value to write.
+ """
+ with self.send_queue_lock:
+ self.send_queue.put(data)
+ return self.socket.send(data)
+
+ # ------------------------------------------------------------------
+ # File Socket
+
+ def makefile(self, *args, **kwargs):
+ """
+ File socket version to use with ElementTree.
+
+ Arguments:
+ Placeholders, same as socket.makefile()
+ """
+ return self
+
+ def read(self, *args, **kwargs):
+ """
+ Implement the file socket read interface.
+
+ Arguments:
+ Placeholders, same as socket.recv()
+ """
+ return self.recv(*args, **kwargs)
+
+ def clear(self):
+ """
+ Empty the send queue, typically done once the session has started to
+ remove the feature negotiation and log in stanzas.
+ """
+ with self.send_queue_lock:
+ for i in range(0, self.send_queue.qsize()):
+ self.send_queue.get(block=False)
+ with self.recv_queue_lock:
+ for i in range(0, self.recv_queue.qsize()):
+ self.recv_queue.get(block=False)
diff --git a/sleekxmpp/test/mocksocket.py b/sleekxmpp/test/mocksocket.py
new file mode 100644
index 00000000..0920b7ea
--- /dev/null
+++ b/sleekxmpp/test/mocksocket.py
@@ -0,0 +1,155 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+import socket
+try:
+ import queue
+except ImportError:
+ import Queue as queue
+
+
+class TestSocket(object):
+
+ """
+ A dummy socket that reads and writes to queues instead
+ of an actual networking socket.
+
+ Methods:
+ next_sent -- Return the next sent stanza.
+ recv_data -- Make a stanza available to read next.
+ recv -- Read the next stanza from the socket.
+ send -- Write a stanza to the socket.
+ makefile -- Dummy call, returns self.
+ read -- Read the next stanza from the socket.
+ """
+
+ def __init__(self, *args, **kwargs):
+ """
+ Create a new test socket.
+
+ Arguments:
+ Same as arguments for socket.socket
+ """
+ self.socket = socket.socket(*args, **kwargs)
+ self.recv_queue = queue.Queue()
+ self.send_queue = queue.Queue()
+ self.is_live = False
+ self.disconnected = False
+
+ def __getattr__(self, name):
+ """
+ Return attribute values of internal, dummy socket.
+
+ Some attributes and methods are disabled to prevent the
+ socket from connecting to the network.
+
+ Arguments:
+ name -- Name of the attribute requested.
+ """
+
+ def dummy(*args):
+ """Method to do nothing and prevent actual socket connections."""
+ return None
+
+ overrides = {'connect': dummy,
+ 'close': dummy,
+ 'shutdown': dummy}
+
+ return overrides.get(name, getattr(self.socket, name))
+
+ # ------------------------------------------------------------------
+ # Testing Interface
+
+ def next_sent(self, timeout=None):
+ """
+ Get the next stanza that has been 'sent'.
+
+ Arguments:
+ timeout -- Optional timeout for waiting for a new value.
+ """
+ args = {'block': False}
+ if timeout is not None:
+ args = {'block': True, 'timeout': timeout}
+ try:
+ return self.send_queue.get(**args)
+ except:
+ return None
+
+ def recv_data(self, data):
+ """
+ Add data to the receiving queue.
+
+ Arguments:
+ data -- String data to 'write' to the socket to be received
+ by the XMPP client.
+ """
+ self.recv_queue.put(data)
+
+ def disconnect_error(self):
+ """
+ Simulate a disconnect error by raising a socket.error exception
+ for any current or further socket operations.
+ """
+ self.disconnected = True
+
+ # ------------------------------------------------------------------
+ # Socket Interface
+
+ def recv(self, *args, **kwargs):
+ """
+ Read a value from the received queue.
+
+ Arguments:
+ Placeholders. Same as for socket.Socket.recv.
+ """
+ if self.disconnected:
+ raise socket.error
+ return self.read(block=True)
+
+ def send(self, data):
+ """
+ Send data by placing it in the send queue.
+
+ Arguments:
+ data -- String value to write.
+ """
+ if self.disconnected:
+ raise socket.error
+ self.send_queue.put(data)
+ return len(data)
+
+ # ------------------------------------------------------------------
+ # File Socket
+
+ def makefile(self, *args, **kwargs):
+ """
+ File socket version to use with ElementTree.
+
+ Arguments:
+ Placeholders, same as socket.Socket.makefile()
+ """
+ return self
+
+ def read(self, block=True, timeout=None, **kwargs):
+ """
+ Implement the file socket interface.
+
+ Arguments:
+ block -- Indicate if the read should block until a
+ value is ready.
+ timeout -- Time in seconds a block should last before
+ returning None.
+ """
+ if self.disconnected:
+ raise socket.error
+ if timeout is not None:
+ block = True
+ try:
+ return self.recv_queue.get(block, timeout)
+ except:
+ return None
diff --git a/sleekxmpp/test/sleektest.py b/sleekxmpp/test/sleektest.py
new file mode 100644
index 00000000..dd3df29a
--- /dev/null
+++ b/sleekxmpp/test/sleektest.py
@@ -0,0 +1,757 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+import unittest
+try:
+ import Queue as queue
+except:
+ import queue
+
+import sleekxmpp
+from sleekxmpp import ClientXMPP, ComponentXMPP
+from sleekxmpp.stanza import Message, Iq, Presence
+from sleekxmpp.test import TestSocket, TestLiveSocket
+from sleekxmpp.exceptions import XMPPError, IqTimeout, IqError
+from sleekxmpp.xmlstream import ET, register_stanza_plugin
+from sleekxmpp.xmlstream import ElementBase, StanzaBase
+from sleekxmpp.xmlstream.tostring import tostring
+from sleekxmpp.xmlstream.matcher import StanzaPath, MatcherId
+from sleekxmpp.xmlstream.matcher import MatchXMLMask, MatchXPath
+
+
+class SleekTest(unittest.TestCase):
+
+ """
+ A SleekXMPP specific TestCase class that provides
+ methods for comparing message, iq, and presence stanzas.
+
+ Methods:
+ Message -- Create a Message stanza object.
+ Iq -- Create an Iq stanza object.
+ Presence -- Create a Presence stanza object.
+ check_jid -- Check a JID and its component parts.
+ check -- Compare a stanza against an XML string.
+ stream_start -- Initialize a dummy XMPP client.
+ stream_close -- Disconnect the XMPP client.
+ make_header -- Create a stream header.
+ send_header -- Check that the given header has been sent.
+ send_feature -- Send a raw XML element.
+ send -- Check that the XMPP client sent the given
+ generic stanza.
+ recv -- Queue data for XMPP client to receive, or
+ verify the data that was received from a
+ live connection.
+ recv_header -- Check that a given stream header
+ was received.
+ recv_feature -- Check that a given, raw XML element
+ was recveived.
+ fix_namespaces -- Add top-level namespace to an XML object.
+ compare -- Compare XML objects against each other.
+ """
+
+ def __init__(self, *args, **kwargs):
+ unittest.TestCase.__init__(self, *args, **kwargs)
+ self.xmpp = None
+
+ def parse_xml(self, xml_string):
+ try:
+ xml = ET.fromstring(xml_string)
+ return xml
+ except SyntaxError as e:
+ if 'unbound' in e.msg:
+ known_prefixes = {
+ 'stream': 'http://etherx.jabber.org/streams'}
+
+ prefix = xml_string.split('<')[1].split(':')[0]
+ if prefix in known_prefixes:
+ xml_string = '<fixns xmlns:%s="%s">%s</fixns>' % (
+ prefix,
+ known_prefixes[prefix],
+ xml_string)
+ xml = self.parse_xml(xml_string)
+ xml = xml.getchildren()[0]
+ return xml
+ else:
+ self.fail("XML data was mal-formed:\n%s" % xml_string)
+
+ # ------------------------------------------------------------------
+ # Shortcut methods for creating stanza objects
+
+ def Message(self, *args, **kwargs):
+ """
+ Create a Message stanza.
+
+ Uses same arguments as StanzaBase.__init__
+
+ Arguments:
+ xml -- An XML object to use for the Message's values.
+ """
+ return Message(self.xmpp, *args, **kwargs)
+
+ def Iq(self, *args, **kwargs):
+ """
+ Create an Iq stanza.
+
+ Uses same arguments as StanzaBase.__init__
+
+ Arguments:
+ xml -- An XML object to use for the Iq's values.
+ """
+ return Iq(self.xmpp, *args, **kwargs)
+
+ def Presence(self, *args, **kwargs):
+ """
+ Create a Presence stanza.
+
+ Uses same arguments as StanzaBase.__init__
+
+ Arguments:
+ xml -- An XML object to use for the Iq's values.
+ """
+ return Presence(self.xmpp, *args, **kwargs)
+
+ def check_jid(self, jid, user=None, domain=None, resource=None,
+ bare=None, full=None, string=None):
+ """
+ Verify the components of a JID.
+
+ Arguments:
+ jid -- The JID object to test.
+ user -- Optional. The user name portion of the JID.
+ domain -- Optional. The domain name portion of the JID.
+ resource -- Optional. The resource portion of the JID.
+ bare -- Optional. The bare JID.
+ full -- Optional. The full JID.
+ string -- Optional. The string version of the JID.
+ """
+ if user is not None:
+ self.assertEqual(jid.user, user,
+ "User does not match: %s" % jid.user)
+ if domain is not None:
+ self.assertEqual(jid.domain, domain,
+ "Domain does not match: %s" % jid.domain)
+ if resource is not None:
+ self.assertEqual(jid.resource, resource,
+ "Resource does not match: %s" % jid.resource)
+ if bare is not None:
+ self.assertEqual(jid.bare, bare,
+ "Bare JID does not match: %s" % jid.bare)
+ if full is not None:
+ self.assertEqual(jid.full, full,
+ "Full JID does not match: %s" % jid.full)
+ if string is not None:
+ self.assertEqual(str(jid), string,
+ "String does not match: %s" % str(jid))
+
+ def check_roster(self, owner, jid, name=None, subscription=None,
+ afrom=None, ato=None, pending_out=None, pending_in=None,
+ groups=None):
+ roster = self.xmpp.roster[owner][jid]
+ if name is not None:
+ self.assertEqual(roster['name'], name,
+ "Incorrect name value: %s" % roster['name'])
+ if subscription is not None:
+ self.assertEqual(roster['subscription'], subscription,
+ "Incorrect subscription: %s" % roster['subscription'])
+ if afrom is not None:
+ self.assertEqual(roster['from'], afrom,
+ "Incorrect from state: %s" % roster['from'])
+ if ato is not None:
+ self.assertEqual(roster['to'], ato,
+ "Incorrect to state: %s" % roster['to'])
+ if pending_out is not None:
+ self.assertEqual(roster['pending_out'], pending_out,
+ "Incorrect pending_out state: %s" % roster['pending_out'])
+ if pending_in is not None:
+ self.assertEqual(roster['pending_in'], pending_out,
+ "Incorrect pending_in state: %s" % roster['pending_in'])
+ if groups is not None:
+ self.assertEqual(roster['groups'], groups,
+ "Incorrect groups: %s" % roster['groups'])
+
+ # ------------------------------------------------------------------
+ # Methods for comparing stanza objects to XML strings
+
+ def check(self, stanza, criteria, method='exact',
+ defaults=None, use_values=True):
+ """
+ Create and compare several stanza objects to a correct XML string.
+
+ If use_values is False, tests using stanza.values will not be used.
+
+ Some stanzas provide default values for some interfaces, but
+ these defaults can be problematic for testing since they can easily
+ be forgotten when supplying the XML string. A list of interfaces that
+ use defaults may be provided and the generated stanzas will use the
+ default values for those interfaces if needed.
+
+ However, correcting the supplied XML is not possible for interfaces
+ that add or remove XML elements. Only interfaces that map to XML
+ attributes may be set using the defaults parameter. The supplied XML
+ must take into account any extra elements that are included by default.
+
+ Arguments:
+ stanza -- The stanza object to test.
+ criteria -- An expression the stanza must match against.
+ method -- The type of matching to use; one of:
+ 'exact', 'mask', 'id', 'xpath', and 'stanzapath'.
+ Defaults to the value of self.match_method.
+ defaults -- A list of stanza interfaces that have default
+ values. These interfaces will be set to their
+ defaults for the given and generated stanzas to
+ prevent unexpected test failures.
+ use_values -- Indicates if testing using stanza.values should
+ be used. Defaults to True.
+ """
+ if method is None and hasattr(self, 'match_method'):
+ method = getattr(self, 'match_method')
+
+ if method != 'exact':
+ matchers = {'stanzapath': StanzaPath,
+ 'xpath': MatchXPath,
+ 'mask': MatchXMLMask,
+ 'id': MatcherId}
+ Matcher = matchers.get(method, None)
+ if Matcher is None:
+ raise ValueError("Unknown matching method.")
+ test = Matcher(criteria)
+ self.failUnless(test.match(stanza),
+ "Stanza did not match using %s method:\n" % method + \
+ "Criteria:\n%s\n" % str(criteria) + \
+ "Stanza:\n%s" % str(stanza))
+ else:
+ stanza_class = stanza.__class__
+ if not isinstance(criteria, ElementBase):
+ xml = self.parse_xml(criteria)
+ else:
+ xml = criteria.xml
+
+ # Ensure that top level namespaces are used, even if they
+ # were not provided.
+ self.fix_namespaces(stanza.xml, 'jabber:client')
+ self.fix_namespaces(xml, 'jabber:client')
+
+ stanza2 = stanza_class(xml=xml)
+
+ if use_values:
+ # Using stanza.values will add XML for any interface that
+ # has a default value. We need to set those defaults on
+ # the existing stanzas and XML so that they will compare
+ # correctly.
+ default_stanza = stanza_class()
+ if defaults is None:
+ known_defaults = {
+ Message: ['type'],
+ Presence: ['priority']
+ }
+ defaults = known_defaults.get(stanza_class, [])
+ for interface in defaults:
+ stanza[interface] = stanza[interface]
+ stanza2[interface] = stanza2[interface]
+ # Can really only automatically add defaults for top
+ # level attribute values. Anything else must be accounted
+ # for in the provided XML string.
+ if interface not in xml.attrib:
+ if interface in default_stanza.xml.attrib:
+ value = default_stanza.xml.attrib[interface]
+ xml.attrib[interface] = value
+
+ values = stanza2.values
+ stanza3 = stanza_class()
+ stanza3.values = values
+
+ debug = "Three methods for creating stanzas do not match.\n"
+ debug += "Given XML:\n%s\n" % tostring(xml)
+ debug += "Given stanza:\n%s\n" % tostring(stanza.xml)
+ debug += "Generated stanza:\n%s\n" % tostring(stanza2.xml)
+ debug += "Second generated stanza:\n%s\n" % tostring(stanza3.xml)
+ result = self.compare(xml, stanza.xml, stanza2.xml, stanza3.xml)
+ else:
+ debug = "Two methods for creating stanzas do not match.\n"
+ debug += "Given XML:\n%s\n" % tostring(xml)
+ debug += "Given stanza:\n%s\n" % tostring(stanza.xml)
+ debug += "Generated stanza:\n%s\n" % tostring(stanza2.xml)
+ result = self.compare(xml, stanza.xml, stanza2.xml)
+
+ self.failUnless(result, debug)
+
+ # ------------------------------------------------------------------
+ # Methods for simulating stanza streams.
+
+ def stream_disconnect(self):
+ """
+ Simulate a stream disconnection.
+ """
+ if self.xmpp:
+ self.xmpp.socket.disconnect_error()
+
+ def stream_start(self, mode='client', skip=True, header=None,
+ socket='mock', jid='tester@localhost',
+ password='test', server='localhost',
+ port=5222, sasl_mech=None,
+ plugins=None, plugin_config={}):
+ """
+ Initialize an XMPP client or component using a dummy XML stream.
+
+ Arguments:
+ mode -- Either 'client' or 'component'. Defaults to 'client'.
+ skip -- Indicates if the first item in the sent queue (the
+ stream header) should be removed. Tests that wish
+ to test initializing the stream should set this to
+ False. Otherwise, the default of True should be used.
+ socket -- Either 'mock' or 'live' to indicate if the socket
+ should be a dummy, mock socket or a live, functioning
+ socket. Defaults to 'mock'.
+ jid -- The JID to use for the connection.
+ Defaults to 'tester@localhost'.
+ password -- The password to use for the connection.
+ Defaults to 'test'.
+ server -- The name of the XMPP server. Defaults to 'localhost'.
+ port -- The port to use when connecting to the server.
+ Defaults to 5222.
+ plugins -- List of plugins to register. By default, all plugins
+ are loaded.
+ """
+ if mode == 'client':
+ self.xmpp = ClientXMPP(jid, password,
+ sasl_mech=sasl_mech,
+ plugin_config=plugin_config)
+ elif mode == 'component':
+ self.xmpp = ComponentXMPP(jid, password,
+ server, port,
+ plugin_config=plugin_config)
+ else:
+ raise ValueError("Unknown XMPP connection mode.")
+
+ # We will use this to wait for the session_start event
+ # for live connections.
+ skip_queue = queue.Queue()
+
+ if socket == 'mock':
+ self.xmpp.set_socket(TestSocket())
+
+ # Simulate connecting for mock sockets.
+ self.xmpp.auto_reconnect = False
+ self.xmpp.state._set_state('connected')
+
+ # Must have the stream header ready for xmpp.process() to work.
+ if not header:
+ header = self.xmpp.stream_header
+ 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)
+ if server is not None:
+ self.xmpp.connect((server, port))
+ else:
+ self.xmpp.connect()
+ else:
+ raise ValueError("Unknown socket type.")
+
+ if plugins is None:
+ self.xmpp.register_plugins()
+ else:
+ for plugin in plugins:
+ self.xmpp.register_plugin(plugin)
+ self.xmpp.process(threaded=True)
+ if skip:
+ if socket != 'live':
+ # Mark send queue as usable
+ self.xmpp.session_started_event.set()
+ # Clear startup stanzas
+ self.xmpp.socket.next_sent(timeout=1)
+ if mode == 'component':
+ self.xmpp.socket.next_sent(timeout=1)
+ else:
+ skip_queue.get(block=True, timeout=10)
+
+ def make_header(self, sto='',
+ sfrom='',
+ sid='',
+ stream_ns="http://etherx.jabber.org/streams",
+ default_ns="jabber:client",
+ version="1.0",
+ xml_header=True):
+ """
+ Create a stream header to be received by the test XMPP agent.
+
+ The header must be saved and passed to stream_start.
+
+ Arguments:
+ sto -- The recipient of the stream header.
+ sfrom -- The agent sending the stream header.
+ sid -- The stream's id.
+ stream_ns -- The namespace of the stream's root element.
+ default_ns -- The default stanza namespace.
+ version -- The stream version.
+ xml_header -- Indicates if the XML version header should be
+ appended before the stream header.
+ """
+ header = '<stream:stream %s>'
+ parts = []
+ if xml_header:
+ header = '<?xml version="1.0"?>' + header
+ if sto:
+ parts.append('to="%s"' % sto)
+ if sfrom:
+ parts.append('from="%s"' % sfrom)
+ if sid:
+ parts.append('id="%s"' % sid)
+ parts.append('version="%s"' % version)
+ parts.append('xmlns:stream="%s"' % stream_ns)
+ parts.append('xmlns="%s"' % default_ns)
+ return header % ' '.join(parts)
+
+ def recv(self, data, defaults=[], method='exact',
+ use_values=True, timeout=1):
+ """
+ Pass data to the dummy XMPP client as if it came from an XMPP server.
+
+ If using a live connection, verify what the server has sent.
+
+ Arguments:
+ data -- If a dummy socket is being used, the XML that is to
+ be received next. Otherwise it is the criteria used
+ to match against live data that is received.
+ defaults -- A list of stanza interfaces with default values that
+ may interfere with comparisons.
+ method -- Select the type of comparison to use for
+ verifying the received stanza. Options are 'exact',
+ 'id', 'stanzapath', 'xpath', and 'mask'.
+ Defaults to the value of self.match_method.
+ use_values -- Indicates if stanza comparisons should test using
+ stanza.values. Defaults to True.
+ timeout -- Time to wait in seconds for data to be received by
+ a live connection.
+ """
+ if self.xmpp.socket.is_live:
+ # we are working with a live connection, so we should
+ # verify what has been received instead of simulating
+ # receiving data.
+ recv_data = self.xmpp.socket.next_recv(timeout)
+ if recv_data is None:
+ self.fail("No stanza was received.")
+ xml = self.parse_xml(recv_data)
+ self.fix_namespaces(xml, 'jabber:client')
+ stanza = self.xmpp._build_stanza(xml, 'jabber:client')
+ self.check(stanza, data,
+ method=method,
+ defaults=defaults,
+ use_values=use_values)
+ else:
+ # place the data in the dummy socket receiving queue.
+ data = str(data)
+ self.xmpp.socket.recv_data(data)
+
+ def recv_header(self, sto='',
+ sfrom='',
+ sid='',
+ stream_ns="http://etherx.jabber.org/streams",
+ default_ns="jabber:client",
+ version="1.0",
+ xml_header=False,
+ timeout=1):
+ """
+ Check that a given stream header was received.
+
+ Arguments:
+ sto -- The recipient of the stream header.
+ sfrom -- The agent sending the stream header.
+ sid -- The stream's id. Set to None to ignore.
+ stream_ns -- The namespace of the stream's root element.
+ default_ns -- The default stanza namespace.
+ version -- The stream version.
+ xml_header -- Indicates if the XML version header should be
+ appended before the stream header.
+ timeout -- Length of time to wait in seconds for a
+ response.
+ """
+ header = self.make_header(sto, sfrom, sid,
+ stream_ns=stream_ns,
+ default_ns=default_ns,
+ version=version,
+ xml_header=xml_header)
+ recv_header = self.xmpp.socket.next_recv(timeout)
+ if recv_header is None:
+ raise ValueError("Socket did not return data.")
+
+ # Apply closing elements so that we can construct
+ # XML objects for comparison.
+ header2 = header + '</stream:stream>'
+ recv_header2 = recv_header + '</stream:stream>'
+
+ xml = self.parse_xml(header2)
+ recv_xml = self.parse_xml(recv_header2)
+
+ if sid is None:
+ # Ignore the id sent by the server since
+ # we can't know in advance what it will be.
+ if 'id' in recv_xml.attrib:
+ del recv_xml.attrib['id']
+
+ # Ignore the xml:lang attribute for now.
+ if 'xml:lang' in recv_xml.attrib:
+ del recv_xml.attrib['xml:lang']
+ xml_ns = 'http://www.w3.org/XML/1998/namespace'
+ if '{%s}lang' % xml_ns in recv_xml.attrib:
+ del recv_xml.attrib['{%s}lang' % xml_ns]
+
+ if recv_xml.getchildren:
+ # We received more than just the header
+ for xml in recv_xml.getchildren():
+ self.xmpp.socket.recv_data(tostring(xml))
+
+ attrib = recv_xml.attrib
+ recv_xml.clear()
+ recv_xml.attrib = attrib
+
+ self.failUnless(
+ self.compare(xml, recv_xml),
+ "Stream headers do not match:\nDesired:\n%s\nReceived:\n%s" % (
+ '%s %s' % (xml.tag, xml.attrib),
+ '%s %s' % (recv_xml.tag, recv_xml.attrib)))
+
+ def recv_feature(self, data, method='mask', use_values=True, timeout=1):
+ """
+ """
+ if method is None and hasattr(self, 'match_method'):
+ method = getattr(self, 'match_method')
+
+ if self.xmpp.socket.is_live:
+ # we are working with a live connection, so we should
+ # verify what has been received instead of simulating
+ # receiving data.
+ recv_data = self.xmpp.socket.next_recv(timeout)
+ xml = self.parse_xml(data)
+ recv_xml = self.parse_xml(recv_data)
+ if recv_data is None:
+ self.fail("No stanza was received.")
+ if method == 'exact':
+ self.failUnless(self.compare(xml, recv_xml),
+ "Features do not match.\nDesired:\n%s\nReceived:\n%s" % (
+ tostring(xml), tostring(recv_xml)))
+ elif method == 'mask':
+ matcher = MatchXMLMask(xml)
+ self.failUnless(matcher.match(recv_xml),
+ "Stanza did not match using %s method:\n" % method + \
+ "Criteria:\n%s\n" % tostring(xml) + \
+ "Stanza:\n%s" % tostring(recv_xml))
+ else:
+ raise ValueError("Uknown matching method: %s" % method)
+ else:
+ # place the data in the dummy socket receiving queue.
+ data = str(data)
+ self.xmpp.socket.recv_data(data)
+
+ def send_header(self, sto='',
+ sfrom='',
+ sid='',
+ stream_ns="http://etherx.jabber.org/streams",
+ default_ns="jabber:client",
+ version="1.0",
+ xml_header=False,
+ timeout=1):
+ """
+ Check that a given stream header was sent.
+
+ Arguments:
+ sto -- The recipient of the stream header.
+ sfrom -- The agent sending the stream header.
+ sid -- The stream's id.
+ stream_ns -- The namespace of the stream's root element.
+ default_ns -- The default stanza namespace.
+ version -- The stream version.
+ xml_header -- Indicates if the XML version header should be
+ appended before the stream header.
+ timeout -- Length of time to wait in seconds for a
+ response.
+ """
+ header = self.make_header(sto, sfrom, sid,
+ stream_ns=stream_ns,
+ default_ns=default_ns,
+ version=version,
+ xml_header=xml_header)
+ sent_header = self.xmpp.socket.next_sent(timeout)
+ if sent_header is None:
+ raise ValueError("Socket did not return data.")
+
+ # Apply closing elements so that we can construct
+ # XML objects for comparison.
+ header2 = header + '</stream:stream>'
+ sent_header2 = sent_header + b'</stream:stream>'
+
+ xml = self.parse_xml(header2)
+ sent_xml = self.parse_xml(sent_header2)
+
+ self.failUnless(
+ self.compare(xml, sent_xml),
+ "Stream headers do not match:\nDesired:\n%s\nSent:\n%s" % (
+ header, sent_header))
+
+ def send_feature(self, data, method='mask', use_values=True, timeout=1):
+ """
+ """
+ sent_data = self.xmpp.socket.next_sent(timeout)
+ xml = self.parse_xml(data)
+ sent_xml = self.parse_xml(sent_data)
+ if sent_data is None:
+ self.fail("No stanza was sent.")
+ if method == 'exact':
+ self.failUnless(self.compare(xml, sent_xml),
+ "Features do not match.\nDesired:\n%s\nReceived:\n%s" % (
+ tostring(xml), tostring(sent_xml)))
+ elif method == 'mask':
+ matcher = MatchXMLMask(xml)
+ self.failUnless(matcher.match(sent_xml),
+ "Stanza did not match using %s method:\n" % method + \
+ "Criteria:\n%s\n" % tostring(xml) + \
+ "Stanza:\n%s" % tostring(sent_xml))
+ else:
+ raise ValueError("Uknown matching method: %s" % method)
+
+ def send(self, data, defaults=None, use_values=True,
+ timeout=.5, method='exact'):
+ """
+ Check that the XMPP client sent the given stanza XML.
+
+ Extracts the next sent stanza and compares it with the given
+ XML using check.
+
+ Arguments:
+ stanza_class -- The class of the sent stanza object.
+ data -- The XML string of the expected Message stanza,
+ or an equivalent stanza object.
+ use_values -- Modifies the type of tests used by check_message.
+ defaults -- A list of stanza interfaces that have defaults
+ values which may interfere with comparisons.
+ timeout -- Time in seconds to wait for a stanza before
+ failing the check.
+ method -- Select the type of comparison to use for
+ verifying the sent stanza. Options are 'exact',
+ 'id', 'stanzapath', 'xpath', and 'mask'.
+ Defaults to the value of self.match_method.
+ """
+ sent = self.xmpp.socket.next_sent(timeout)
+ if data is None and sent is None:
+ return
+ if data is None and sent is not None:
+ self.fail("Stanza data was sent: %s" % sent)
+ if sent is None:
+ self.fail("No stanza was sent.")
+
+ xml = self.parse_xml(sent)
+ self.fix_namespaces(xml, 'jabber:client')
+ sent = self.xmpp._build_stanza(xml, 'jabber:client')
+ self.check(sent, data,
+ method=method,
+ defaults=defaults,
+ use_values=use_values)
+
+ def stream_close(self):
+ """
+ Disconnect the dummy XMPP client.
+
+ Can be safely called even if stream_start has not been called.
+
+ Must be placed in the tearDown method of a test class to ensure
+ that the XMPP client is disconnected after an error.
+ """
+ if hasattr(self, 'xmpp') and self.xmpp is not None:
+ self.xmpp.socket.recv_data(self.xmpp.stream_footer)
+ self.xmpp.disconnect()
+
+ # ------------------------------------------------------------------
+ # XML Comparison and Cleanup
+
+ def fix_namespaces(self, xml, ns):
+ """
+ Assign a namespace to an element and any children that
+ don't have a namespace.
+
+ Arguments:
+ xml -- The XML object to fix.
+ ns -- The namespace to add to the XML object.
+ """
+ if xml.tag.startswith('{'):
+ return
+ xml.tag = '{%s}%s' % (ns, xml.tag)
+ for child in xml.getchildren():
+ self.fix_namespaces(child, ns)
+
+ def compare(self, xml, *other):
+ """
+ Compare XML objects.
+
+ Arguments:
+ xml -- The XML object to compare against.
+ *other -- The list of XML objects to compare.
+ """
+ if not other:
+ return False
+
+ # Compare multiple objects
+ if len(other) > 1:
+ for xml2 in other:
+ if not self.compare(xml, xml2):
+ return False
+ return True
+
+ other = other[0]
+
+ # Step 1: Check tags
+ if xml.tag != other.tag:
+ return False
+
+ # Step 2: Check attributes
+ if xml.attrib != other.attrib:
+ return False
+
+ # Step 3: Check text
+ if xml.text is None:
+ xml.text = ""
+ if other.text is None:
+ other.text = ""
+ xml.text = xml.text.strip()
+ other.text = other.text.strip()
+
+ if xml.text != other.text:
+ return False
+
+ # Step 4: Check children count
+ if len(xml.getchildren()) != len(other.getchildren()):
+ return False
+
+ # Step 5: Recursively check children
+ for child in xml:
+ child2s = other.findall("%s" % child.tag)
+ if child2s is None:
+ return False
+ for child2 in child2s:
+ if self.compare(child, child2):
+ break
+ else:
+ return False
+
+ # Step 6: Recursively check children the other way.
+ for child in other:
+ child2s = xml.findall("%s" % child.tag)
+ if child2s is None:
+ return False
+ for child2 in child2s:
+ if self.compare(child, child2):
+ break
+ else:
+ return False
+
+ # Everything matches
+ return True
diff --git a/sleekxmpp/thirdparty/__init__.py b/sleekxmpp/thirdparty/__init__.py
new file mode 100644
index 00000000..1c7bf651
--- /dev/null
+++ b/sleekxmpp/thirdparty/__init__.py
@@ -0,0 +1,7 @@
+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/ordereddict.py b/sleekxmpp/thirdparty/ordereddict.py
new file mode 100644
index 00000000..5b0303f5
--- /dev/null
+++ b/sleekxmpp/thirdparty/ordereddict.py
@@ -0,0 +1,127 @@
+# 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.
+
+from UserDict import DictMixin
+
+class OrderedDict(dict, DictMixin):
+
+ def __init__(self, *args, **kwds):
+ if len(args) > 1:
+ raise TypeError('expected at most 1 arguments, got %d' % len(args))
+ try:
+ self.__end
+ except AttributeError:
+ self.clear()
+ self.update(*args, **kwds)
+
+ def clear(self):
+ self.__end = end = []
+ end += [None, end, end] # sentinel node for doubly linked list
+ self.__map = {} # key --> [key, prev, next]
+ dict.clear(self)
+
+ def __setitem__(self, key, value):
+ if key not in self:
+ end = self.__end
+ curr = end[1]
+ curr[2] = end[1] = self.__map[key] = [key, curr, end]
+ dict.__setitem__(self, key, value)
+
+ def __delitem__(self, key):
+ dict.__delitem__(self, key)
+ key, prev, next = self.__map.pop(key)
+ prev[2] = next
+ next[1] = prev
+
+ def __iter__(self):
+ end = self.__end
+ curr = end[2]
+ while curr is not end:
+ yield curr[0]
+ curr = curr[2]
+
+ def __reversed__(self):
+ end = self.__end
+ curr = end[1]
+ while curr is not end:
+ yield curr[0]
+ curr = curr[1]
+
+ def popitem(self, last=True):
+ if not self:
+ raise KeyError('dictionary is empty')
+ if last:
+ key = reversed(self).next()
+ else:
+ key = iter(self).next()
+ value = self.pop(key)
+ return key, value
+
+ def __reduce__(self):
+ items = [[k, self[k]] for k in self]
+ tmp = self.__map, self.__end
+ del self.__map, self.__end
+ inst_dict = vars(self).copy()
+ self.__map, self.__end = tmp
+ if inst_dict:
+ return (self.__class__, (items,), inst_dict)
+ return self.__class__, (items,)
+
+ def keys(self):
+ return list(self)
+
+ setdefault = DictMixin.setdefault
+ update = DictMixin.update
+ pop = DictMixin.pop
+ values = DictMixin.values
+ items = DictMixin.items
+ iterkeys = DictMixin.iterkeys
+ itervalues = DictMixin.itervalues
+ iteritems = DictMixin.iteritems
+
+ def __repr__(self):
+ if not self:
+ return '%s()' % (self.__class__.__name__,)
+ return '%s(%r)' % (self.__class__.__name__, self.items())
+
+ def copy(self):
+ return self.__class__(self)
+
+ @classmethod
+ def fromkeys(cls, iterable, value=None):
+ d = cls()
+ for key in iterable:
+ d[key] = value
+ return d
+
+ def __eq__(self, other):
+ if isinstance(other, OrderedDict):
+ if len(self) != len(other):
+ return False
+ for p, q in zip(self.items(), other.items()):
+ if p != q:
+ return False
+ return True
+ return dict.__eq__(self, other)
+
+ def __ne__(self, other):
+ return not self == other
diff --git a/sleekxmpp/thirdparty/statemachine.py b/sleekxmpp/thirdparty/statemachine.py
new file mode 100644
index 00000000..8a7324b5
--- /dev/null
+++ b/sleekxmpp/thirdparty/statemachine.py
@@ -0,0 +1,287 @@
+"""
+ 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 threading
+import time
+import logging
+
+log = logging.getLogger(__name__)
+
+
+class StateMachine(object):
+
+ def __init__(self, states=[]):
+ self.lock = threading.Lock()
+ self.notifier = threading.Event()
+ self.__states = []
+ self.addStates(states)
+ self.__default_state = self.__states[0]
+ self.__current_state = self.__default_state
+
+ def addStates(self, states):
+ self.lock.acquire()
+ try:
+ for state in states:
+ if state in self.__states:
+ raise IndexError("The state '%s' is already in the StateMachine." % state)
+ self.__states.append(state)
+ finally: self.lock.release()
+
+
+ def transition(self, from_state, to_state, wait=0.0, func=None, args=[], kwargs={}):
+ '''
+ Transition from the given `from_state` to the given `to_state`.
+ This method will return `True` if the state machine is now in `to_state`. It
+ will return `False` if a timeout occurred the transition did not occur.
+ If `wait` is 0 (the default,) this method returns immediately if the state machine
+ is not in `from_state`.
+
+ If you want the thread to block and transition once the state machine to enters
+ `from_state`, set `wait` to a non-negative value. Note there is no 'block
+ indefinitely' flag since this leads to deadlock. If you want to wait indefinitely,
+ choose a reasonable value for `wait` (e.g. 20 seconds) and do so in a while loop like so:
+
+ ::
+
+ while not thread_should_exit and not state_machine.transition('disconnected', 'connecting', wait=20 ):
+ pass # timeout will occur every 20s unless transition occurs
+ if thread_should_exit: return
+ # perform actions here after successful transition
+
+ This allows the thread to be responsive by setting `thread_should_exit=True`.
+
+ The optional `func` argument allows the user to pass a callable operation which occurs
+ within the context of the state transition (e.g. while the state machine is locked.)
+ If `func` returns a True value, the transition will occur. If `func` returns a non-
+ True value or if an exception is thrown, the transition will not occur. Any thrown
+ exception is not caught by the state machine and is the caller's responsibility to handle.
+ If `func` completes normally, this method will return the value returned by `func.` If
+ values for `args` and `kwargs` are provided, they are expanded and passed like so:
+ `func( *args, **kwargs )`.
+ '''
+
+ return self.transition_any((from_state,), to_state, wait=wait,
+ func=func, args=args, kwargs=kwargs)
+
+
+ def transition_any(self, from_states, to_state, wait=0.0, func=None, args=[], kwargs={}):
+ '''
+ Transition from any of the given `from_states` to the given `to_state`.
+ '''
+
+ if not (isinstance(from_states,tuple) or isinstance(from_states,list)):
+ raise ValueError("from_states should be a list or tuple")
+
+ for state in from_states:
+ if not state in self.__states:
+ raise ValueError("StateMachine does not contain from_state %s." % state)
+ if not to_state in self.__states:
+ raise ValueError("StateMachine does not contain to_state %s." % to_state)
+
+ start = time.time()
+ while not self.lock.acquire(False):
+ time.sleep(.001)
+ if (start + wait - time.time()) <= 0.0:
+ log.debug("Could not acquire lock")
+ return False
+
+ while not self.__current_state in from_states:
+ # detect timeout:
+ remainder = start + wait - time.time()
+ if remainder > 0:
+ self.notifier.wait(remainder)
+ else:
+ log.debug("State was not ready")
+ self.lock.release()
+ return False
+
+ try: # lock is acquired; all other threads will return false or wait until notify/timeout
+ if self.__current_state in from_states: # should always be True due to lock
+
+ # Note that func might throw an exception, but that's OK, it aborts the transition
+ return_val = func(*args,**kwargs) if func is not None else True
+
+ # some 'false' value returned from func,
+ # indicating that transition should not occur:
+ if not return_val: return return_val
+
+ log.debug(' ==== TRANSITION %s -> %s', self.__current_state, to_state)
+ self._set_state(to_state)
+ return return_val # some 'true' value returned by func or True if func was None
+ else:
+ log.error("StateMachine bug!! The lock should ensure this doesn't happen!")
+ return False
+ finally:
+ self.notifier.set() # notify any waiting threads that the state has changed.
+ self.notifier.clear()
+ self.lock.release()
+
+
+ def transition_ctx(self, from_state, to_state, wait=0.0):
+ '''
+ Use the state machine as a context manager. The transition occurs on /exit/ from
+ the `with` context, so long as no exception is thrown. For example:
+
+ ::
+
+ with state_machine.transition_ctx('one','two', wait=5) as locked:
+ if locked:
+ # the state machine is currently locked in state 'one', and will
+ # transition to 'two' when the 'with' statement ends, so long as
+ # no exception is thrown.
+ print 'Currently locked in state one: %s' % state_machine['one']
+
+ else:
+ # The 'wait' timed out, and no lock has been acquired
+ print 'Timed out before entering state "one"'
+
+ print 'Since no exception was thrown, we are now in state "two": %s' % state_machine['two']
+
+
+ The other main difference between this method and `transition()` is that the
+ state machine is locked for the duration of the `with` statement. Normally,
+ after a `transition()` occurs, the state machine is immediately unlocked and
+ available to another thread to call `transition()` again.
+ '''
+
+ if not from_state in self.__states:
+ raise ValueError("StateMachine does not contain from_state %s." % from_state)
+ if not to_state in self.__states:
+ raise ValueError("StateMachine does not contain to_state %s." % to_state)
+
+ return _StateCtx(self, from_state, to_state, wait)
+
+
+ def ensure(self, state, wait=0.0, block_on_transition=False):
+ '''
+ Ensure the state machine is currently in `state`, or wait until it enters `state`.
+ '''
+ return self.ensure_any((state,), wait=wait, block_on_transition=block_on_transition)
+
+
+ def ensure_any(self, states, wait=0.0, block_on_transition=False):
+ '''
+ Ensure we are currently in one of the given `states` or wait until
+ we enter one of those states.
+
+ Note that due to the nature of the function, you cannot guarantee that
+ the entirety of some operation completes while you remain in a given
+ state. That would require acquiring and holding a lock, which
+ would mean no other threads could do the same. (You'd essentially
+ be serializing all of the threads that are 'ensuring' their tasks
+ occurred in some state.
+ '''
+ if not (isinstance(states,tuple) or isinstance(states,list)):
+ raise ValueError('states arg should be a tuple or list')
+
+ for state in states:
+ if not state in self.__states:
+ raise ValueError("StateMachine does not contain state '%s'" % state)
+
+ # if we're in the middle of a transition, determine whether we should
+ # 'fall back' to the 'current' state, or wait for the new state, in order to
+ # avoid an operation occurring in the wrong state.
+ # TODO another option would be an ensure_ctx that uses a semaphore to allow
+ # threads to indicate they want to remain in a particular state.
+
+ # will return immediately if no transition is in process.
+ if block_on_transition:
+ # we're not in the middle of a transition; don't hold the lock
+ if self.lock.acquire(False): self.lock.release()
+ # wait for the transition to complete
+ else: self.notifier.wait()
+
+ start = time.time()
+ while not self.__current_state in states:
+ # detect timeout:
+ remainder = start + wait - time.time()
+ if remainder > 0: self.notifier.wait(remainder)
+ else: return False
+ return True
+
+
+ def reset(self):
+ # TODO need to lock before calling this?
+ self.transition(self.__current_state, self.__default_state)
+
+
+ def _set_state(self, state): #unsynchronized, only call internally after lock is acquired
+ self.__current_state = state
+ return state
+
+
+ def current_state(self):
+ '''
+ Return the current state name.
+ '''
+ return self.__current_state
+
+
+ def __getitem__(self, state):
+ '''
+ Non-blocking, non-synchronized test to determine if we are in the given state.
+ Use `StateMachine.ensure(state)` to wait until the machine enters a certain state.
+ '''
+ return self.__current_state == state
+
+ def __str__(self):
+ return "".join(("StateMachine(", ','.join(self.__states), "): ", self.__current_state))
+
+
+
+class _StateCtx:
+
+ def __init__(self, state_machine, from_state, to_state, wait):
+ self.state_machine = state_machine
+ self.from_state = from_state
+ self.to_state = to_state
+ self.wait = wait
+ self._locked = False
+
+ def __enter__(self):
+ start = time.time()
+ while not self.state_machine[self.from_state] or not self.state_machine.lock.acquire(False):
+ # detect timeout:
+ remainder = start + self.wait - time.time()
+ if remainder > 0: self.state_machine.notifier.wait(remainder)
+ else:
+ log.debug('StateMachine timeout while waiting for state: %s', self.from_state)
+ return False
+
+ self._locked = True # lock has been acquired at this point
+ self.state_machine.notifier.clear()
+ log.debug('StateMachine entered context in state: %s',
+ self.state_machine.current_state())
+ return True
+
+ def __exit__(self, exc_type, exc_val, exc_tb):
+ if exc_val is not None:
+ log.exception("StateMachine exception in context, remaining in state: %s\n%s:%s",
+ self.state_machine.current_state(), exc_type.__name__, exc_val)
+
+ if self._locked:
+ if exc_val is None:
+ log.debug(' ==== TRANSITION %s -> %s',
+ self.state_machine.current_state(), self.to_state)
+ self.state_machine._set_state(self.to_state)
+
+ self.state_machine.notifier.set()
+ self.state_machine.lock.release()
+
+ return False # re-raise any exception
+
+if __name__ == '__main__':
+
+ def callback(s, s2):
+ print((1, s.transition('on', 'off', wait=0.0, func=callback, args=[s,s2])))
+ print((2, s2.transition('off', 'on', func=callback, args=[s,s2])))
+ return True
+
+ s = StateMachine(('off', 'on'))
+ s2 = StateMachine(('off', 'on'))
+ print((3, s.transition('off', 'on', wait=0.0, func=callback, args=[s,s2]),))
+ print((s.current_state(), s2.current_state()))
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..e115e5d5
--- /dev/null
+++ b/sleekxmpp/thirdparty/suelta/mechanisms/__init__.py
@@ -0,0 +1,6 @@
+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
+from sleekxmpp.thirdparty.suelta.mechanisms.messenger_oauth2 import X_MESSENGER_OAUTH2
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/messenger_oauth2.py b/sleekxmpp/thirdparty/suelta/mechanisms/messenger_oauth2.py
new file mode 100644
index 00000000..f5b0ddec
--- /dev/null
+++ b/sleekxmpp/thirdparty/suelta/mechanisms/messenger_oauth2.py
@@ -0,0 +1,17 @@
+from sleekxmpp.thirdparty.suelta.util import bytes
+from sleekxmpp.thirdparty.suelta.sasl import Mechanism, register_mechanism
+
+
+class X_MESSENGER_OAUTH2(Mechanism):
+
+ def __init__(self, sasl, name):
+ super(X_MESSENGER_OAUTH2, self).__init__(sasl, name)
+ self.check_values(['access_token'])
+
+ def process(self, challenge=None):
+ return bytes(self.values['access_token'])
+
+ def okay(self):
+ return True
+
+register_mechanism('X-MESSENGER-OAUTH2', 10, X_MESSENGER_OAUTH2, use_hashes=False)
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..b70ac9a4
--- /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(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/version.py b/sleekxmpp/version.py
new file mode 100644
index 00000000..037c6463
--- /dev/null
+++ b/sleekxmpp/version.py
@@ -0,0 +1,13 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+# We don't want to have to import the entire library
+# just to get the version info for setup.py
+
+__version__ = '1.0'
+__version_info__ = (1, 0, 0, '', 0)
diff --git a/sleekxmpp/xmlstream/__init__.py b/sleekxmpp/xmlstream/__init__.py
new file mode 100644
index 00000000..67b20c56
--- /dev/null
+++ b/sleekxmpp/xmlstream/__init__.py
@@ -0,0 +1,19 @@
+"""
+ 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.jid import JID
+from sleekxmpp.xmlstream.scheduler import Scheduler
+from sleekxmpp.xmlstream.stanzabase import StanzaBase, ElementBase, ET
+from sleekxmpp.xmlstream.stanzabase import register_stanza_plugin
+from sleekxmpp.xmlstream.tostring import tostring
+from sleekxmpp.xmlstream.xmlstream import XMLStream, RESPONSE_TIMEOUT
+from sleekxmpp.xmlstream.xmlstream import RestartStream
+
+__all__ = ['JID', 'Scheduler', 'StanzaBase', 'ElementBase',
+ 'ET', 'StateMachine', 'tostring', 'XMLStream',
+ 'RESPONSE_TIMEOUT', 'RestartStream']
diff --git a/sleekxmpp/xmlstream/filesocket.py b/sleekxmpp/xmlstream/filesocket.py
new file mode 100644
index 00000000..56554c73
--- /dev/null
+++ b/sleekxmpp/xmlstream/filesocket.py
@@ -0,0 +1,47 @@
+# -*- coding: utf-8 -*-
+"""
+ sleekxmpp.xmlstream.filesocket
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+ This module is a shim for correcting deficiencies in the file
+ socket implementation of Python2.6.
+
+ Part of SleekXMPP: The Sleek XMPP Library
+
+ :copyright: (c) 2011 Nathanael C. Fritz
+ :license: MIT, see LICENSE for more details
+"""
+
+from socket import _fileobject
+import socket
+
+
+class FileSocket(_fileobject):
+
+ """Create a file object wrapper for a socket to work around
+ issues present in Python 2.6 when using sockets as file objects.
+
+ The parser for :class:`~xml.etree.cElementTree` requires a file, but
+ we will be reading from the XMPP connection socket instead.
+ """
+
+ def read(self, size=4096):
+ """Read data from the socket as if it were a file."""
+ if self._sock is None:
+ return None
+ data = self._sock.recv(size)
+ if data is not None:
+ return data
+
+
+class Socket26(socket._socketobject):
+
+ """A custom socket implementation that uses our own FileSocket class
+ to work around issues in Python 2.6 when using sockets as files.
+ """
+
+ def makefile(self, mode='r', bufsize=-1):
+ """makefile([mode[, bufsize]]) -> file object
+ Return a regular file object corresponding to the socket. The mode
+ and bufsize arguments are as for the built-in open() function."""
+ return FileSocket(self._sock, mode, bufsize)
diff --git a/sleekxmpp/xmlstream/handler/__init__.py b/sleekxmpp/xmlstream/handler/__init__.py
new file mode 100644
index 00000000..7bcf0b71
--- /dev/null
+++ b/sleekxmpp/xmlstream/handler/__init__.py
@@ -0,0 +1,14 @@
+"""
+ 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.handler.callback import Callback
+from sleekxmpp.xmlstream.handler.waiter import Waiter
+from sleekxmpp.xmlstream.handler.xmlcallback import XMLCallback
+from sleekxmpp.xmlstream.handler.xmlwaiter import XMLWaiter
+
+__all__ = ['Callback', 'Waiter', 'XMLCallback', 'XMLWaiter']
diff --git a/sleekxmpp/xmlstream/handler/base.py b/sleekxmpp/xmlstream/handler/base.py
new file mode 100644
index 00000000..59dcb306
--- /dev/null
+++ b/sleekxmpp/xmlstream/handler/base.py
@@ -0,0 +1,84 @@
+# -*- coding: utf-8 -*-
+"""
+ sleekxmpp.xmlstream.handler.base
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+ Part of SleekXMPP: The Sleek XMPP Library
+
+ :copyright: (c) 2011 Nathanael C. Fritz
+ :license: MIT, see LICENSE for more details
+"""
+
+import weakref
+
+
+class BaseHandler(object):
+
+ """
+ Base class for stream handlers. Stream handlers are matched with
+ incoming stanzas so that the stanza may be processed in some way.
+ Stanzas may be matched with multiple handlers.
+
+ Handler execution may take place in two phases: during the incoming
+ stream processing, and in the main event loop. The :meth:`prerun()`
+ method is executed in the first case, and :meth:`run()` is called
+ during the second.
+
+ :param string name: The name of the handler.
+ :param matcher: A :class:`~sleekxmpp.xmlstream.matcher.base.MatcherBase`
+ derived object that will be used to determine if a
+ stanza should be accepted by this handler.
+ :param stream: The :class:`~sleekxmpp.xmlstream.xmlstream.XMLStream`
+ instance that the handle will respond to.
+ """
+
+ def __init__(self, name, matcher, stream=None):
+ #: The name of the handler
+ self.name = name
+
+ #: The XML stream this handler is assigned to
+ self.stream = None
+ if stream is not None:
+ self.stream = weakref.ref(stream)
+ stream.register_handler(self)
+
+ self._destroy = False
+ self._payload = None
+ self._matcher = matcher
+
+ def match(self, xml):
+ """Compare a stanza or XML object with the handler's matcher.
+
+ :param xml: An XML or
+ :class:`~sleekxmpp.xmlstream.stanzabase.ElementBase` object
+ """
+ return self._matcher.match(xml)
+
+ def prerun(self, payload):
+ """Prepare the handler for execution while the XML
+ stream is being processed.
+
+ :param payload: A :class:`~sleekxmpp.xmlstream.stanzabase.ElementBase`
+ object.
+ """
+ self._payload = payload
+
+ def run(self, payload):
+ """Execute the handler after XML stream processing and during the
+ main event loop.
+
+ :param payload: A :class:`~sleekxmpp.xmlstream.stanzabase.ElementBase`
+ object.
+ """
+ self._payload = payload
+
+ def check_delete(self):
+ """Check if the handler should be removed from the list
+ of stream handlers.
+ """
+ return self._destroy
+
+
+# To comply with PEP8, method names now use underscores.
+# Deprecated method names are re-mapped for backwards compatibility.
+BaseHandler.checkDelete = BaseHandler.check_delete
diff --git a/sleekxmpp/xmlstream/handler/callback.py b/sleekxmpp/xmlstream/handler/callback.py
new file mode 100644
index 00000000..37f53335
--- /dev/null
+++ b/sleekxmpp/xmlstream/handler/callback.py
@@ -0,0 +1,79 @@
+# -*- coding: utf-8 -*-
+"""
+ sleekxmpp.xmlstream.handler.callback
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+ Part of SleekXMPP: The Sleek XMPP Library
+
+ :copyright: (c) 2011 Nathanael C. Fritz
+ :license: MIT, see LICENSE for more details
+"""
+
+from sleekxmpp.xmlstream.handler.base import BaseHandler
+
+
+class Callback(BaseHandler):
+
+ """
+ The Callback handler will execute a callback function with
+ matched stanzas.
+
+ The handler may execute the callback either during stream
+ processing or during the main event loop.
+
+ Callback functions are all executed in the same thread, so be aware if
+ you are executing functions that will block for extended periods of
+ time. Typically, you should signal your own events using the SleekXMPP
+ object's :meth:`~sleekxmpp.xmlstream.xmlstream.XMLStream.event()`
+ method to pass the stanza off to a threaded event handler for further
+ processing.
+
+
+ :param string name: The name of the handler.
+ :param matcher: A :class:`~sleekxmpp.xmlstream.matcher.base.MatcherBase`
+ derived object for matching stanza objects.
+ :param pointer: The function to execute during callback.
+ :param bool thread: **DEPRECATED.** Remains only for
+ backwards compatibility.
+ :param bool once: Indicates if the handler should be used only
+ once. Defaults to False.
+ :param bool instream: Indicates if the callback should be executed
+ during stream processing instead of in the
+ main event loop.
+ :param stream: The :class:`~sleekxmpp.xmlstream.xmlstream.XMLStream`
+ instance this handler should monitor.
+ """
+
+ def __init__(self, name, matcher, pointer, thread=False,
+ once=False, instream=False, stream=None):
+ BaseHandler.__init__(self, name, matcher, stream)
+ self._pointer = pointer
+ self._once = once
+ self._instream = instream
+
+ def prerun(self, payload):
+ """Execute the callback during stream processing, if
+ the callback was created with ``instream=True``.
+
+ :param payload: The matched
+ :class:`~sleekxmpp.xmlstream.stanzabase.ElementBase` object.
+ """
+ if self._once:
+ self._destroy = True
+ if self._instream:
+ self.run(payload, True)
+
+ def run(self, payload, instream=False):
+ """Execute the callback function with the matched stanza payload.
+
+ :param payload: The matched
+ :class:`~sleekxmpp.xmlstream.stanzabase.ElementBase` object.
+ :param bool instream: Force the handler to execute during stream
+ processing. This should only be used by
+ :meth:`prerun()`. Defaults to ``False``.
+ """
+ if not self._instream or instream:
+ self._pointer(payload)
+ if self._once:
+ self._destroy = True
+ del self._pointer
diff --git a/sleekxmpp/xmlstream/handler/waiter.py b/sleekxmpp/xmlstream/handler/waiter.py
new file mode 100644
index 00000000..01ff5d67
--- /dev/null
+++ b/sleekxmpp/xmlstream/handler/waiter.py
@@ -0,0 +1,87 @@
+# -*- coding: utf-8 -*-
+"""
+ sleekxmpp.xmlstream.handler.waiter
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+ Part of SleekXMPP: The Sleek XMPP Library
+
+ :copyright: (c) 2011 Nathanael C. Fritz
+ :license: MIT, see LICENSE for more details
+"""
+
+import logging
+try:
+ import queue
+except ImportError:
+ import Queue as queue
+
+from sleekxmpp.xmlstream import StanzaBase
+from sleekxmpp.xmlstream.handler.base import BaseHandler
+
+
+log = logging.getLogger(__name__)
+
+
+class Waiter(BaseHandler):
+
+ """
+ The Waiter handler allows an event handler to block until a
+ particular stanza has been received. The handler will either be
+ given the matched stanza, or ``False`` if the waiter has timed out.
+
+ :param string name: The name of the handler.
+ :param matcher: A :class:`~sleekxmpp.xmlstream.matcher.base.MatcherBase`
+ derived object for matching stanza objects.
+ :param stream: The :class:`~sleekxmpp.xmlstream.xmlstream.XMLStream`
+ instance this handler should monitor.
+ """
+
+ def __init__(self, name, matcher, stream=None):
+ BaseHandler.__init__(self, name, matcher, stream=stream)
+ self._payload = queue.Queue()
+
+ def prerun(self, payload):
+ """Store the matched stanza when received during processing.
+
+ :param payload: The matched
+ :class:`~sleekxmpp.xmlstream.stanzabase.ElementBase` object.
+ """
+ self._payload.put(payload)
+
+ def run(self, payload):
+ """Do not process this handler during the main event loop."""
+ pass
+
+ def wait(self, timeout=None):
+ """Block an event handler while waiting for a stanza to arrive.
+
+ Be aware that this will impact performance if called from a
+ non-threaded event handler.
+
+ Will return either the received stanza, or ``False`` if the
+ waiter timed out.
+
+ :param int timeout: The number of seconds to wait for the stanza
+ to arrive. Defaults to the the stream's
+ :class:`~sleekxmpp.xmlstream.xmlstream.XMLStream.response_timeout`
+ value.
+ """
+ if timeout is None:
+ timeout = self.stream().response_timeout
+
+ elapsed_time = 0
+ stanza = False
+ while elapsed_time < timeout and not self.stream().stop.is_set():
+ try:
+ stanza = self._payload.get(True, 1)
+ break
+ except queue.Empty:
+ elapsed_time += 1
+ if elapsed_time >= timeout:
+ log.warning("Timed out waiting for %s", self.name)
+ self.stream().remove_handler(self.name)
+ return stanza
+
+ def check_delete(self):
+ """Always remove waiters after use."""
+ return True
diff --git a/sleekxmpp/xmlstream/handler/xmlcallback.py b/sleekxmpp/xmlstream/handler/xmlcallback.py
new file mode 100644
index 00000000..11607ffb
--- /dev/null
+++ b/sleekxmpp/xmlstream/handler/xmlcallback.py
@@ -0,0 +1,36 @@
+"""
+ 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.handler import Callback
+
+
+class XMLCallback(Callback):
+
+ """
+ The XMLCallback class is identical to the normal Callback class,
+ except that XML contents of matched stanzas will be processed instead
+ of the stanza objects themselves.
+
+ Methods:
+ run -- Overrides Callback.run
+ """
+
+ def run(self, payload, instream=False):
+ """
+ Execute the callback function with the matched stanza's
+ XML contents, instead of the stanza itself.
+
+ Overrides BaseHandler.run
+
+ Arguments:
+ payload -- The matched stanza object.
+ instream -- Force the handler to execute during
+ stream processing. Used only by prerun.
+ Defaults to False.
+ """
+ Callback.run(self, payload.xml, instream)
diff --git a/sleekxmpp/xmlstream/handler/xmlwaiter.py b/sleekxmpp/xmlstream/handler/xmlwaiter.py
new file mode 100644
index 00000000..5201caf3
--- /dev/null
+++ b/sleekxmpp/xmlstream/handler/xmlwaiter.py
@@ -0,0 +1,33 @@
+"""
+ 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.handler import Waiter
+
+
+class XMLWaiter(Waiter):
+
+ """
+ The XMLWaiter class is identical to the normal Waiter class
+ except that it returns the XML contents of the stanza instead
+ of the full stanza object itself.
+
+ Methods:
+ prerun -- Overrides Waiter.prerun
+ """
+
+ def prerun(self, payload):
+ """
+ Store the XML contents of the stanza to return to the
+ waiting event handler.
+
+ Overrides Waiter.prerun
+
+ Arguments:
+ payload -- The matched stanza object.
+ """
+ Waiter.prerun(self, payload.xml)
diff --git a/sleekxmpp/xmlstream/jid.py b/sleekxmpp/xmlstream/jid.py
new file mode 100644
index 00000000..281bf4ee
--- /dev/null
+++ b/sleekxmpp/xmlstream/jid.py
@@ -0,0 +1,145 @@
+# -*- coding: utf-8 -*-
+"""
+ sleekxmpp.xmlstream.jid
+ ~~~~~~~~~~~~~~~~~~~~~~~
+
+ This module allows for working with Jabber IDs (JIDs) by
+ providing accessors for the various components of a JID.
+
+ Part of SleekXMPP: The Sleek XMPP Library
+
+ :copyright: (c) 2011 Nathanael C. Fritz
+ :license: MIT, see LICENSE for more details
+"""
+
+from __future__ import unicode_literals
+
+
+class JID(object):
+
+ """
+ A representation of a Jabber ID, or JID.
+
+ Each JID may have three components: a user, a domain, and an optional
+ resource. For example: user@domain/resource
+
+ When a resource is not used, the JID is called a bare JID.
+ The JID is a full JID otherwise.
+
+ **JID Properties:**
+ :jid: Alias for ``full``.
+ :full: The value of the full JID.
+ :bare: The value of the bare JID.
+ :user: The username portion of the JID.
+ :domain: The domain name portion of the JID.
+ :server: Alias for ``domain``.
+ :resource: The resource portion of the JID.
+
+ :param string jid: A string of the form ``'[user@]domain[/resource]'``.
+ """
+
+ def __init__(self, jid):
+ """Initialize a new JID"""
+ self.reset(jid)
+
+ def reset(self, jid):
+ """Start fresh from a new JID string.
+
+ :param string jid: A string of the form ``'[user@]domain[/resource]'``.
+ """
+ if isinstance(jid, JID):
+ jid = jid.full
+ self._full = self._jid = jid
+ self._domain = None
+ self._resource = None
+ self._user = None
+ self._bare = None
+
+ def __getattr__(self, name):
+ """Handle getting the JID values, using cache if available.
+
+ :param name: One of: user, server, domain, resource,
+ full, or bare.
+ """
+ if name == 'resource':
+ if self._resource is None and '/' in self._jid:
+ self._resource = self._jid.split('/', 1)[-1]
+ return self._resource or ""
+ elif name == 'user':
+ if self._user is None:
+ if '@' in self._jid:
+ self._user = self._jid.split('@', 1)[0]
+ else:
+ self._user = self._user
+ return self._user or ""
+ elif name in ('server', 'domain', 'host'):
+ if self._domain is None:
+ self._domain = self._jid.split('@', 1)[-1].split('/', 1)[0]
+ return self._domain or ""
+ elif name in ('full', 'jid'):
+ return self._jid or ""
+ elif name == 'bare':
+ if self._bare is None:
+ self._bare = self._jid.split('/', 1)[0]
+ return self._bare or ""
+
+ def __setattr__(self, name, value):
+ """Edit a JID by updating it's individual values, resetting the
+ generated JID in the end.
+
+ Arguments:
+ name -- The name of the JID part. One of: user, domain,
+ server, resource, full, jid, or bare.
+ value -- The new value for the JID part.
+ """
+ if name in ('resource', 'user', 'domain'):
+ object.__setattr__(self, "_%s" % name, value)
+ self.regenerate()
+ elif name in ('server', 'domain', 'host'):
+ self.domain = value
+ elif name in ('full', 'jid'):
+ self.reset(value)
+ self.regenerate()
+ elif name == 'bare':
+ if '@' in value:
+ u, d = value.split('@', 1)
+ object.__setattr__(self, "_user", u)
+ object.__setattr__(self, "_domain", d)
+ else:
+ object.__setattr__(self, "_user", '')
+ object.__setattr__(self, "_domain", value)
+ self.regenerate()
+ else:
+ object.__setattr__(self, name, value)
+
+ def regenerate(self):
+ """Generate a new JID based on current values, useful after editing."""
+ jid = ""
+ if self.user:
+ jid = "%s@" % self.user
+ jid += self.domain
+ if self.resource:
+ jid += "/%s" % self.resource
+ self.reset(jid)
+
+ def __str__(self):
+ """Use the full JID as the string value."""
+ return self.full
+
+ def __repr__(self):
+ return self.full
+
+ def __eq__(self, other):
+ """
+ Two JIDs are considered equal if they have the same full JID value.
+ """
+ other = JID(other)
+ return self.full == other.full
+
+ def __ne__(self, other):
+ """Two JIDs are considered unequal if they are not equal."""
+ return not self == other
+
+ def __hash__(self):
+ """Hash a JID based on the string version of its full JID."""
+ return hash(self.full)
diff --git a/sleekxmpp/xmlstream/matcher/__init__.py b/sleekxmpp/xmlstream/matcher/__init__.py
new file mode 100644
index 00000000..1038d1bd
--- /dev/null
+++ b/sleekxmpp/xmlstream/matcher/__init__.py
@@ -0,0 +1,16 @@
+"""
+ 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.matcher.id import MatcherId
+from sleekxmpp.xmlstream.matcher.many import MatchMany
+from sleekxmpp.xmlstream.matcher.stanzapath import StanzaPath
+from sleekxmpp.xmlstream.matcher.xmlmask import MatchXMLMask
+from sleekxmpp.xmlstream.matcher.xpath import MatchXPath
+
+__all__ = ['MatcherId', 'MatchMany', 'StanzaPath',
+ 'MatchXMLMask', 'MatchXPath']
diff --git a/sleekxmpp/xmlstream/matcher/base.py b/sleekxmpp/xmlstream/matcher/base.py
new file mode 100644
index 00000000..83c26688
--- /dev/null
+++ b/sleekxmpp/xmlstream/matcher/base.py
@@ -0,0 +1,31 @@
+# -*- coding: utf-8 -*-
+"""
+ sleekxmpp.xmlstream.matcher.base
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+ Part of SleekXMPP: The Sleek XMPP Library
+
+ :copyright: (c) 2011 Nathanael C. Fritz
+ :license: MIT, see LICENSE for more details
+"""
+
+
+class MatcherBase(object):
+
+ """
+ Base class for stanza matchers. Stanza matchers are used to pick
+ stanzas out of the XML stream and pass them to the appropriate
+ stream handlers.
+
+ :param criteria: Object to compare some aspect of a stanza against.
+ """
+
+ def __init__(self, criteria):
+ self._criteria = criteria
+
+ def match(self, xml):
+ """Check if a stanza matches the stored criteria.
+
+ Meant to be overridden.
+ """
+ return False
diff --git a/sleekxmpp/xmlstream/matcher/id.py b/sleekxmpp/xmlstream/matcher/id.py
new file mode 100644
index 00000000..11ab70bb
--- /dev/null
+++ b/sleekxmpp/xmlstream/matcher/id.py
@@ -0,0 +1,29 @@
+# -*- coding: utf-8 -*-
+"""
+ sleekxmpp.xmlstream.matcher.id
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+ Part of SleekXMPP: The Sleek XMPP Library
+
+ :copyright: (c) 2011 Nathanael C. Fritz
+ :license: MIT, see LICENSE for more details
+"""
+
+from sleekxmpp.xmlstream.matcher.base import MatcherBase
+
+
+class MatcherId(MatcherBase):
+
+ """
+ The ID matcher selects stanzas that have the same stanza 'id'
+ interface value as the desired ID.
+ """
+
+ def match(self, xml):
+ """Compare the given stanza's ``'id'`` attribute to the stored
+ ``id`` value.
+
+ :param xml: The :class:`~sleekxmpp.xmlstream.stanzabase.ElementBase`
+ stanza to compare against.
+ """
+ return xml['id'] == self._criteria
diff --git a/sleekxmpp/xmlstream/matcher/many.py b/sleekxmpp/xmlstream/matcher/many.py
new file mode 100644
index 00000000..f470ec9c
--- /dev/null
+++ b/sleekxmpp/xmlstream/matcher/many.py
@@ -0,0 +1,40 @@
+"""
+ 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.matcher.base import MatcherBase
+
+
+class MatchMany(MatcherBase):
+
+ """
+ The MatchMany matcher may compare a stanza against multiple
+ criteria. It is essentially an OR relation combining multiple
+ matchers.
+
+ Each of the criteria must implement a match() method.
+
+ Methods:
+ match -- Overrides MatcherBase.match.
+ """
+
+ def match(self, xml):
+ """
+ Match a stanza against multiple criteria. The match is successful
+ if one of the criteria matches.
+
+ Each of the criteria must implement a match() method.
+
+ Overrides MatcherBase.match.
+
+ Arguments:
+ xml -- The stanza object to compare against.
+ """
+ for m in self._criteria:
+ if m.match(xml):
+ return True
+ return False
diff --git a/sleekxmpp/xmlstream/matcher/stanzapath.py b/sleekxmpp/xmlstream/matcher/stanzapath.py
new file mode 100644
index 00000000..a4c0fda0
--- /dev/null
+++ b/sleekxmpp/xmlstream/matcher/stanzapath.py
@@ -0,0 +1,43 @@
+# -*- coding: utf-8 -*-
+"""
+ sleekxmpp.xmlstream.matcher.stanzapath
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+ Part of SleekXMPP: The Sleek XMPP Library
+
+ :copyright: (c) 2011 Nathanael C. Fritz
+ :license: MIT, see LICENSE for more details
+"""
+
+from sleekxmpp.xmlstream.matcher.base import MatcherBase
+from sleekxmpp.xmlstream.stanzabase import fix_ns
+
+
+class StanzaPath(MatcherBase):
+
+ """
+ The StanzaPath matcher selects stanzas that match a given "stanza path",
+ which is similar to a normal XPath except that it uses the interfaces and
+ plugins of the stanza instead of the actual, underlying XML.
+
+ :param criteria: Object to compare some aspect of a stanza against.
+ """
+
+ def __init__(self, criteria):
+ self._criteria = fix_ns(criteria, split=True,
+ propagate_ns=False,
+ default_ns='jabber:client')
+ self._raw_criteria = criteria
+
+ def match(self, stanza):
+ """
+ Compare a stanza against a "stanza path". A stanza path is similar to
+ an XPath expression, but uses the stanza's interfaces and plugins
+ instead of the underlying XML. See the documentation for the stanza
+ :meth:`~sleekxmpp.xmlstream.stanzabase.ElementBase.match()` method
+ for more information.
+
+ :param stanza: The :class:`~sleekxmpp.xmlstream.stanzabase.ElementBase`
+ stanza to compare against.
+ """
+ return stanza.match(self._criteria) or stanza.match(self._raw_criteria)
diff --git a/sleekxmpp/xmlstream/matcher/xmlmask.py b/sleekxmpp/xmlstream/matcher/xmlmask.py
new file mode 100644
index 00000000..7977e767
--- /dev/null
+++ b/sleekxmpp/xmlstream/matcher/xmlmask.py
@@ -0,0 +1,158 @@
+"""
+ 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 xml.parsers.expat import ExpatError
+
+from sleekxmpp.xmlstream.stanzabase import ET
+from sleekxmpp.xmlstream.matcher.base import MatcherBase
+
+
+# Flag indicating if the builtin XPath matcher should be used, which
+# uses namespaces, or a custom matcher that ignores namespaces.
+# Changing this will affect ALL XMLMask matchers.
+IGNORE_NS = False
+
+
+log = logging.getLogger(__name__)
+
+
+class MatchXMLMask(MatcherBase):
+
+ """
+ The XMLMask matcher selects stanzas whose XML matches a given
+ XML pattern, or mask. For example, message stanzas with body elements
+ could be matched using the mask:
+
+ .. code-block:: xml
+
+ <message xmlns="jabber:client"><body /></message>
+
+ Use of XMLMask is discouraged, and
+ :class:`~sleekxmpp.xmlstream.matcher.xpath.MatchXPath` or
+ :class:`~sleekxmpp.xmlstream.matcher.stanzapath.StanzaPath`
+ should be used instead.
+
+ The use of namespaces in the mask comparison is controlled by
+ ``IGNORE_NS``. Setting ``IGNORE_NS`` to ``True`` will disable namespace
+ based matching for ALL XMLMask matchers.
+
+ :param criteria: Either an :class:`~xml.etree.ElementTree.Element` XML
+ object or XML string to use as a mask.
+ """
+
+ def __init__(self, criteria):
+ MatcherBase.__init__(self, criteria)
+ if isinstance(criteria, str):
+ self._criteria = ET.fromstring(self._criteria)
+ self.default_ns = 'jabber:client'
+
+ def setDefaultNS(self, ns):
+ """Set the default namespace to use during comparisons.
+
+ :param ns: The new namespace to use as the default.
+ """
+ self.default_ns = ns
+
+ def match(self, xml):
+ """Compare a stanza object or XML object against the stored XML mask.
+
+ Overrides MatcherBase.match.
+
+ :param xml: The stanza object or XML object to compare against.
+ """
+ if hasattr(xml, 'xml'):
+ xml = xml.xml
+ return self._mask_cmp(xml, self._criteria, True)
+
+ def _mask_cmp(self, source, mask, use_ns=False, default_ns='__no_ns__'):
+ """Compare an XML object against an XML mask.
+
+ :param source: The :class:`~xml.etree.ElementTree.Element` XML object
+ to compare against the mask.
+ :param mask: The :class:`~xml.etree.ElementTree.Element` XML object
+ serving as the mask.
+ :param use_ns: Indicates if namespaces should be respected during
+ the comparison.
+ :default_ns: The default namespace to apply to elements that
+ do not have a specified namespace.
+ Defaults to ``"__no_ns__"``.
+ """
+ use_ns = not IGNORE_NS
+
+ if source is None:
+ # If the element was not found. May happend during recursive calls.
+ return False
+
+ # Convert the mask to an XML object if it is a string.
+ if not hasattr(mask, 'attrib'):
+ try:
+ mask = ET.fromstring(mask)
+ except ExpatError:
+ log.warning("Expat error: %s\nIn parsing: %s", '', mask)
+ if not use_ns:
+ # Compare the element without using namespaces.
+ source_tag = source.tag.split('}', 1)[-1]
+ mask_tag = mask.tag.split('}', 1)[-1]
+ if source_tag != mask_tag:
+ return False
+ else:
+ # Compare the element using namespaces
+ mask_ns_tag = "{%s}%s" % (self.default_ns, mask.tag)
+ if source.tag not in [mask.tag, mask_ns_tag]:
+ return False
+
+ # If the mask includes text, compare it.
+ if mask.text and source.text and \
+ source.text.strip() != mask.text.strip():
+ return False
+
+ # Compare attributes. The stanza must include the attributes
+ # defined by the mask, but may include others.
+ for name, value in mask.attrib.items():
+ if source.attrib.get(name, "__None__") != value:
+ return False
+
+ # Recursively check subelements.
+ matched_elements = {}
+ for subelement in mask:
+ if use_ns:
+ matched = False
+ for other in source.findall(subelement.tag):
+ matched_elements[other] = False
+ if self._mask_cmp(other, subelement, use_ns):
+ if not matched_elements.get(other, False):
+ matched_elements[other] = True
+ matched = True
+ if not matched:
+ return False
+ else:
+ if not self._mask_cmp(self._get_child(source, subelement.tag),
+ subelement, use_ns):
+ return False
+
+ # Everything matches.
+ return True
+
+ def _get_child(self, xml, tag):
+ """Return a child element given its tag, ignoring namespace values.
+
+ Returns ``None`` if the child was not found.
+
+ :param xml: The :class:`~xml.etree.ElementTree.Element` XML object
+ to search for the given child tag.
+ :param tag: The name of the subelement to find.
+ """
+ tag = tag.split('}')[-1]
+ try:
+ children = [c.tag.split('}')[-1] for c in xml.getchildren()]
+ index = children.index(tag)
+ except ValueError:
+ return None
+ return xml.getchildren()[index]
diff --git a/sleekxmpp/xmlstream/matcher/xpath.py b/sleekxmpp/xmlstream/matcher/xpath.py
new file mode 100644
index 00000000..b6af0609
--- /dev/null
+++ b/sleekxmpp/xmlstream/matcher/xpath.py
@@ -0,0 +1,86 @@
+# -*- coding: utf-8 -*-
+"""
+ sleekxmpp.xmlstream.matcher.xpath
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+ Part of SleekXMPP: The Sleek XMPP Library
+
+ :copyright: (c) 2011 Nathanael C. Fritz
+ :license: MIT, see LICENSE for more details
+"""
+
+from sleekxmpp.xmlstream.stanzabase import ET
+from sleekxmpp.xmlstream.matcher.base import MatcherBase
+
+
+# Flag indicating if the builtin XPath matcher should be used, which
+# uses namespaces, or a custom matcher that ignores namespaces.
+# Changing this will affect ALL XPath matchers.
+IGNORE_NS = False
+
+
+class MatchXPath(MatcherBase):
+
+ """
+ The XPath matcher selects stanzas whose XML contents matches a given
+ XPath expression.
+
+ .. warning::
+
+ Using this matcher may not produce expected behavior when using
+ attribute selectors. For Python 2.6 and 3.1, the ElementTree
+ :meth:`~xml.etree.ElementTree.Element.find()` method does
+ not support the use of attribute selectors. If you need to
+ support Python 2.6 or 3.1, it might be more useful to use a
+ :class:`~sleekxmpp.xmlstream.matcher.stanzapath.StanzaPath` matcher.
+
+ If the value of :data:`IGNORE_NS` is set to ``True``, then XPath
+ expressions will be matched without using namespaces.
+ """
+
+ def match(self, xml):
+ """
+ Compare a stanza's XML contents to an XPath expression.
+
+ If the value of :data:`IGNORE_NS` is set to ``True``, then XPath
+ expressions will be matched without using namespaces.
+
+ .. warning::
+
+ In Python 2.6 and 3.1 the ElementTree
+ :meth:`~xml.etree.ElementTree.Element.find()` method does not
+ support attribute selectors in the XPath expression.
+
+ :param xml: The :class:`~sleekxmpp.xmlstream.stanzabase.ElementBase`
+ stanza to compare against.
+ """
+ if hasattr(xml, 'xml'):
+ xml = xml.xml
+ x = ET.Element('x')
+ x.append(xml)
+
+ if not IGNORE_NS:
+ # Use builtin, namespace respecting, XPath matcher.
+ if x.find(self._criteria) is not None:
+ return True
+ return False
+ else:
+ # Remove namespaces from the XPath expression.
+ criteria = []
+ for ns_block in self._criteria.split('{'):
+ criteria.extend(ns_block.split('}')[-1].split('/'))
+
+ # Walk the XPath expression.
+ xml = x
+ for tag in criteria:
+ if not tag:
+ # Skip empty tag name artifacts from the cleanup phase.
+ continue
+
+ children = [c.tag.split('}')[-1] for c in xml.getchildren()]
+ try:
+ index = children.index(tag)
+ except ValueError:
+ return False
+ xml = xml.getchildren()[index]
+ return True
diff --git a/sleekxmpp/xmlstream/scheduler.py b/sleekxmpp/xmlstream/scheduler.py
new file mode 100644
index 00000000..4a6f073f
--- /dev/null
+++ b/sleekxmpp/xmlstream/scheduler.py
@@ -0,0 +1,228 @@
+# -*- coding: utf-8 -*-
+"""
+ sleekxmpp.xmlstream.scheduler
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+ This module provides a task scheduler that works better
+ with SleekXMPP's threading usage than the stock version.
+
+ Part of SleekXMPP: The Sleek XMPP Library
+
+ :copyright: (c) 2011 Nathanael C. Fritz
+ :license: MIT, see LICENSE for more details
+"""
+
+import time
+import threading
+import logging
+try:
+ import queue
+except ImportError:
+ import Queue as queue
+
+
+log = logging.getLogger(__name__)
+
+
+class Task(object):
+
+ """
+ A scheduled task that will be executed by the scheduler
+ after a given time interval has passed.
+
+ :param string name: The name of the task.
+ :param int seconds: The number of seconds to wait before executing.
+ :param callback: The function to execute.
+ :param tuple args: The arguments to pass to the callback.
+ :param dict kwargs: The keyword arguments to pass to the callback.
+ :param bool repeat: Indicates if the task should repeat.
+ Defaults to ``False``.
+ :param pointer: A pointer to an event queue for queuing callback
+ execution instead of executing immediately.
+ """
+
+ def __init__(self, name, seconds, callback, args=None,
+ kwargs=None, repeat=False, qpointer=None):
+ #: The name of the task.
+ self.name = name
+
+ #: The number of seconds to wait before executing.
+ self.seconds = seconds
+
+ #: The function to execute once enough time has passed.
+ self.callback = callback
+
+ #: The arguments to pass to :attr:`callback`.
+ self.args = args or tuple()
+
+ #: The keyword arguments to pass to :attr:`callback`.
+ self.kwargs = kwargs or {}
+
+ #: Indicates if the task should repeat after executing,
+ #: using the same :attr:`seconds` delay.
+ self.repeat = repeat
+
+ #: The time when the task should execute next.
+ self.next = time.time() + self.seconds
+
+ #: The main event queue, which allows for callbacks to
+ #: be queued for execution instead of executing immediately.
+ self.qpointer = qpointer
+
+ def run(self):
+ """Execute the task's callback.
+
+ If an event queue was supplied, place the callback in the queue;
+ otherwise, execute the callback immediately.
+ """
+ if self.qpointer is not None:
+ self.qpointer.put(('schedule', self.callback,
+ self.args, self.name))
+ else:
+ self.callback(*self.args, **self.kwargs)
+ self.reset()
+ return self.repeat
+
+ def reset(self):
+ """Reset the task's timer so that it will repeat."""
+ self.next = time.time() + self.seconds
+
+
+class Scheduler(object):
+
+ """
+ A threaded scheduler that allows for updates mid-execution unlike the
+ scheduler in the standard library.
+
+ Based on: http://docs.python.org/library/sched.html#module-sched
+
+ :param parentstop: An :class:`~threading.Event` to signal stopping
+ the scheduler.
+ """
+
+ def __init__(self, parentstop=None):
+ #: A queue for storing tasks
+ self.addq = queue.Queue()
+
+ #: A list of tasks in order of execution time.
+ self.schedule = []
+
+ #: If running in threaded mode, this will be the thread processing
+ #: the schedule.
+ self.thread = None
+
+ #: A flag indicating that the scheduler is running.
+ self.run = False
+
+ #: An :class:`~threading.Event` instance for signalling to stop
+ #: the scheduler.
+ self.stop = parentstop
+
+ #: Lock for accessing the task queue.
+ self.schedule_lock = threading.RLock()
+
+ def process(self, threaded=True):
+ """Begin accepting and processing scheduled tasks.
+
+ :param bool threaded: Indicates if the scheduler should execute
+ in its own thread. Defaults to ``True``.
+ """
+ if threaded:
+ self.thread = threading.Thread(name='scheduler_process',
+ target=self._process)
+ self.thread.start()
+ else:
+ self._process()
+
+ def _process(self):
+ """Process scheduled tasks."""
+ self.run = True
+ try:
+ while self.run and not self.stop.isSet():
+ wait = 1
+ updated = False
+ if self.schedule:
+ wait = self.schedule[0].next - time.time()
+ try:
+ if wait <= 0.0:
+ newtask = self.addq.get(False)
+ else:
+ if wait >= 3.0:
+ wait = 3.0
+ newtask = self.addq.get(True, wait)
+ except queue.Empty:
+ cleanup = []
+ self.schedule_lock.acquire()
+ for task in self.schedule:
+ if time.time() >= task.next:
+ updated = True
+ if not task.run():
+ cleanup.append(task)
+ else:
+ break
+ for task in cleanup:
+ x = self.schedule.pop(self.schedule.index(task))
+ else:
+ updated = True
+ self.schedule_lock.acquire()
+ self.schedule.append(newtask)
+ finally:
+ if updated:
+ self.schedule = sorted(self.schedule,
+ key=lambda task: task.next)
+ self.schedule_lock.release()
+ except KeyboardInterrupt:
+ self.run = False
+ except SystemExit:
+ self.run = False
+ log.debug("Quitting Scheduler thread")
+
+ def add(self, name, seconds, callback, args=None,
+ kwargs=None, repeat=False, qpointer=None):
+ """Schedule a new task.
+
+ :param string name: The name of the task.
+ :param int seconds: The number of seconds to wait before executing.
+ :param callback: The function to execute.
+ :param tuple args: The arguments to pass to the callback.
+ :param dict kwargs: The keyword arguments to pass to the callback.
+ :param bool repeat: Indicates if the task should repeat.
+ Defaults to ``False``.
+ :param pointer: A pointer to an event queue for queuing callback
+ execution instead of executing immediately.
+ """
+ try:
+ self.schedule_lock.acquire()
+ for task in self.schedule:
+ if task.name == name:
+ raise ValueError("Key %s already exists" % name)
+
+ self.addq.put(Task(name, seconds, callback, args,
+ kwargs, repeat, qpointer))
+ except:
+ raise
+ finally:
+ self.schedule_lock.release()
+
+ def remove(self, name):
+ """Remove a scheduled task ahead of schedule, and without
+ executing it.
+
+ :param string name: The name of the task to remove.
+ """
+ try:
+ self.schedule_lock.acquire()
+ the_task = None
+ for task in self.schedule:
+ if task.name == name:
+ the_task = task
+ if the_task is not None:
+ self.schedule.remove(the_task)
+ except:
+ raise
+ finally:
+ self.schedule_lock.release()
+
+ def quit(self):
+ """Shutdown the scheduler."""
+ self.run = False
diff --git a/sleekxmpp/xmlstream/stanzabase.py b/sleekxmpp/xmlstream/stanzabase.py
new file mode 100644
index 00000000..dff8c997
--- /dev/null
+++ b/sleekxmpp/xmlstream/stanzabase.py
@@ -0,0 +1,1323 @@
+# -*- coding: utf-8 -*-
+"""
+ sleekxmpp.xmlstream.stanzabase
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+ This module implements a wrapper layer for XML objects
+ that allows them to be treated like dictionaries.
+
+ Part of SleekXMPP: The Sleek XMPP Library
+
+ :copyright: (c) 2011 Nathanael C. Fritz
+ :license: MIT, see LICENSE for more details
+"""
+
+import copy
+import logging
+import sys
+import weakref
+from xml.etree import cElementTree as ET
+
+from sleekxmpp.xmlstream import JID
+from sleekxmpp.xmlstream.tostring import tostring
+from sleekxmpp.thirdparty import OrderedDict
+
+
+log = logging.getLogger(__name__)
+
+
+# Used to check if an argument is an XML object.
+XML_TYPE = type(ET.Element('xml'))
+
+
+def register_stanza_plugin(stanza, plugin, iterable=False, overrides=False):
+ """
+ Associate a stanza object as a plugin for another stanza.
+
+ >>> from sleekxmpp.xmlstream import register_stanza_plugin
+ >>> register_stanza_plugin(Iq, CustomStanza)
+
+ :param class stanza: The class of the parent stanza.
+ :param class plugin: The class of the plugin stanza.
+ :param bool iterable: Indicates if the plugin stanza should be
+ included in the parent stanza's iterable
+ ``'substanzas'`` interface results.
+ :param bool overrides: Indicates if the plugin should be allowed
+ to override the interface handlers for
+ the parent stanza, based on the plugin's
+ ``overrides`` field.
+
+ .. versionadded:: 1.0-Beta1
+ Made ``register_stanza_plugin`` the default name. The prior
+ ``registerStanzaPlugin`` function name remains as an alias.
+ """
+ tag = "{%s}%s" % (plugin.namespace, plugin.name)
+
+ # Prevent weird memory reference gotchas by ensuring
+ # that the parent stanza class has its own set of
+ # plugin info maps and is not using the mappings from
+ # an ancestor class (like ElementBase).
+ plugin_info = ('plugin_attrib_map', 'plugin_tag_map',
+ 'plugin_iterables', 'plugin_overrides')
+ for attr in plugin_info:
+ info = getattr(stanza, attr)
+ setattr(stanza, attr, info.copy())
+
+ stanza.plugin_attrib_map[plugin.plugin_attrib] = plugin
+ stanza.plugin_tag_map[tag] = plugin
+
+ if iterable:
+ stanza.plugin_iterables.add(plugin)
+ if overrides:
+ for interface in plugin.overrides:
+ stanza.plugin_overrides[interface] = plugin.plugin_attrib
+
+
+# To maintain backwards compatibility for now, preserve the camel case name.
+registerStanzaPlugin = register_stanza_plugin
+
+
+def fix_ns(xpath, split=False, propagate_ns=True, default_ns=''):
+ """Apply the stanza's namespace to elements in an XPath expression.
+
+ :param string xpath: The XPath expression to fix with namespaces.
+ :param bool split: Indicates if the fixed XPath should be left as a
+ list of element names with namespaces. Defaults to
+ False, which returns a flat string path.
+ :param bool propagate_ns: Overrides propagating parent element
+ namespaces to child elements. Useful if
+ you wish to simply split an XPath that has
+ non-specified namespaces, and child and
+ parent namespaces are known not to always
+ match. Defaults to True.
+ """
+ fixed = []
+ # Split the XPath into a series of blocks, where a block
+ # is started by an element with a namespace.
+ ns_blocks = xpath.split('{')
+ for ns_block in ns_blocks:
+ if '}' in ns_block:
+ # Apply the found namespace to following elements
+ # that do not have namespaces.
+ namespace = ns_block.split('}')[0]
+ elements = ns_block.split('}')[1].split('/')
+ else:
+ # Apply the stanza's namespace to the following
+ # elements since no namespace was provided.
+ namespace = default_ns
+ elements = ns_block.split('/')
+
+ for element in elements:
+ if element:
+ # Skip empty entry artifacts from splitting.
+ if propagate_ns:
+ tag = '{%s}%s' % (namespace, element)
+ else:
+ tag = element
+ fixed.append(tag)
+ if split:
+ return fixed
+ return '/'.join(fixed)
+
+
+class ElementBase(object):
+
+ """
+ The core of SleekXMPP's stanza XML manipulation and handling is provided
+ by ElementBase. ElementBase wraps XML cElementTree objects and enables
+ access to the XML contents through dictionary syntax, similar in style
+ to the Ruby XMPP library Blather's stanza implementation.
+
+ Stanzas are defined by their name, namespace, and interfaces. For
+ example, a simplistic Message stanza could be defined as::
+
+ >>> class Message(ElementBase):
+ ... name = "message"
+ ... namespace = "jabber:client"
+ ... interfaces = set(('to', 'from', 'type', 'body'))
+ ... sub_interfaces = set(('body',))
+
+ The resulting Message stanza's contents may be accessed as so::
+
+ >>> message['to'] = "user@example.com"
+ >>> message['body'] = "Hi!"
+ >>> message['body']
+ "Hi!"
+ >>> del message['body']
+ >>> message['body']
+ ""
+
+ The interface values map to either custom access methods, stanza
+ XML attributes, or (if the interface is also in sub_interfaces) the
+ text contents of a stanza's subelement.
+
+ Custom access methods may be created by adding methods of the
+ form "getInterface", "setInterface", or "delInterface", where
+ "Interface" is the titlecase version of the interface name.
+
+ Stanzas may be extended through the use of plugins. A plugin
+ is simply a stanza that has a plugin_attrib value. For example::
+
+ >>> class MessagePlugin(ElementBase):
+ ... name = "custom_plugin"
+ ... namespace = "custom"
+ ... interfaces = set(('useful_thing', 'custom'))
+ ... plugin_attrib = "custom"
+
+ The plugin stanza class must be associated with its intended
+ container stanza by using register_stanza_plugin as so::
+
+ >>> register_stanza_plugin(Message, MessagePlugin)
+
+ The plugin may then be accessed as if it were built-in to the parent
+ stanza::
+
+ >>> message['custom']['useful_thing'] = 'foo'
+
+ If a plugin provides an interface that is the same as the plugin's
+ plugin_attrib value, then the plugin's interface may be assigned
+ directly from the parent stanza, as shown below, but retrieving
+ information will require all interfaces to be used, as so::
+
+ >>> message['custom'] = 'bar' # Same as using message['custom']['custom']
+ >>> message['custom']['custom'] # Must use all interfaces
+ 'bar'
+
+ If the plugin sets :attr:`is_extension` to ``True``, then both setting
+ and getting an interface value that is the same as the plugin's
+ plugin_attrib value will work, as so::
+
+ >>> message['custom'] = 'bar' # Using is_extension=True
+ >>> message['custom']
+ 'bar'
+
+
+ :param xml: Initialize the stanza object with an existing XML object.
+ :param parent: Optionally specify a parent stanza object will will
+ contain this substanza.
+ """
+
+ #: The XML tag name of the element, not including any namespace
+ #: prefixes. For example, an :class:`ElementBase` object for ``<message />``
+ #: would use ``name = 'message'``.
+ name = 'stanza'
+
+ #: The XML namespace for the element. Given ``<foo xmlns="bar" />``,
+ #: then ``namespace = "bar"`` should be used. The default namespace
+ #: is ``jabber:client`` since this is being used in an XMPP library.
+ namespace = 'jabber:client'
+
+ #: For :class:`ElementBase` subclasses which are intended to be used
+ #: as plugins, the ``plugin_attrib`` value defines the plugin name.
+ #: Plugins may be accessed by using the ``plugin_attrib`` value as
+ #: the interface. An example using ``plugin_attrib = 'foo'``::
+ #:
+ #: register_stanza_plugin(Message, FooPlugin)
+ #: msg = Message()
+ #: msg['foo']['an_interface_from_the_foo_plugin']
+ plugin_attrib = 'plugin'
+
+ #: The set of keys that the stanza provides for accessing and
+ #: manipulating the underlying XML object. This set may be augmented
+ #: with the :attr:`plugin_attrib` value of any registered
+ #: stanza plugins.
+ interfaces = set(('type', 'to', 'from', 'id', 'payload'))
+
+ #: A subset of :attr:`interfaces` which maps interfaces to direct
+ #: subelements of the underlying XML object. Using this set, the text
+ #: of these subelements may be set, retrieved, or removed without
+ #: needing to define custom methods.
+ sub_interfaces = tuple()
+
+ #: In some cases you may wish to override the behaviour of one of the
+ #: parent stanza's interfaces. The ``overrides`` list specifies the
+ #: interface name and access method to be overridden. For example,
+ #: to override setting the parent's ``'condition'`` interface you
+ #: would use::
+ #:
+ #: overrides = ['set_condition']
+ #:
+ #: Getting and deleting the ``'condition'`` interface would not
+ #: be affected.
+ #:
+ #: .. versionadded:: 1.0-Beta5
+ overrides = []
+
+ #: If you need to add a new interface to an existing stanza, you
+ #: can create a plugin and set ``is_extension = True``. Be sure
+ #: to set the :attr:`plugin_attrib` value to the desired interface
+ #: name, and that it is the only interface listed in
+ #: :attr:`interfaces`. Requests for the new interface from the
+ #: parent stanza will be passed to the plugin directly.
+ #:
+ #: .. versionadded:: 1.0-Beta5
+ is_extension = False
+
+ #: A map of interface operations to the overriding functions.
+ #: For example, after overriding the ``set`` operation for
+ #: the interface ``body``, :attr:`plugin_overrides` would be::
+ #:
+ #: {'set_body': <some function>}
+ #:
+ #: .. versionadded: 1.0-Beta5
+ plugin_overrides = {}
+
+ #: A mapping of the :attr:`plugin_attrib` values of registered
+ #: plugins to their respective classes.
+ plugin_attrib_map = {}
+
+ #: A mapping of root element tag names (in ``'{namespace}elementname'``
+ #: format) to the plugin classes responsible for them.
+ plugin_tag_map = {}
+
+ #: The set of stanza classes that can be iterated over using
+ #: the 'substanzas' interface. Classes are added to this set
+ #: when registering a plugin with ``iterable=True``::
+ #:
+ #: register_stanza_plugin(DiscoInfo, DiscoItem, iterable=True)
+ #:
+ #: .. versionadded:: 1.0-Beta5
+ plugin_iterables = set()
+
+ #: A deprecated version of :attr:`plugin_iterables` that remains
+ #: for backward compatibility. It required a parent stanza to
+ #: know beforehand what stanza classes would be iterable::
+ #:
+ #: class DiscoItem(ElementBase):
+ #: ...
+ #:
+ #: class DiscoInfo(ElementBase):
+ #: subitem = (DiscoItem, )
+ #: ...
+ #:
+ #: .. deprecated:: 1.0-Beta5
+ subitem = set()
+
+ #: The default XML namespace: ``http://www.w3.org/XML/1998/namespace``.
+ xml_ns = 'http://www.w3.org/XML/1998/namespace'
+
+ def __init__(self, xml=None, parent=None):
+ self._index = 0
+
+ #: The underlying XML object for the stanza. It is a standard
+ #: :class:`xml.etree.cElementTree` object.
+ self.xml = xml
+
+ #: An ordered dictionary of plugin stanzas, mapped by their
+ #: :attr:`plugin_attrib` value.
+ self.plugins = OrderedDict()
+
+ #: A list of child stanzas whose class is included in
+ #: :attr:`plugin_iterables`.
+ self.iterables = []
+
+ #: The name of the tag for the stanza's root element. It is the
+ #: same as calling :meth:`tag_name()` and is formatted as
+ #: ``'{namespace}elementname'``.
+ self.tag = self.tag_name()
+
+ #: A :class:`weakref.weakref` to the parent stanza, if there is one.
+ #: If not, then :attr:`parent` is ``None``.
+ self.parent = None
+ if parent is not None:
+ self.parent = weakref.ref(parent)
+
+ if self.subitem is not None:
+ for sub in self.subitem:
+ self.plugin_iterables.add(sub)
+
+ if self.setup(xml):
+ # If we generated our own XML, then everything is ready.
+ return
+
+ # Initialize values using provided XML
+ for child in self.xml.getchildren():
+ if child.tag in self.plugin_tag_map:
+ plugin_class = self.plugin_tag_map[child.tag]
+ plugin = plugin_class(child, self)
+ self.plugins[plugin.plugin_attrib] = plugin
+ if plugin_class in self.plugin_iterables:
+ self.iterables.append(plugin)
+
+ def setup(self, xml=None):
+ """Initialize the stanza's XML contents.
+
+ Will return ``True`` if XML was generated according to the stanza's
+ definition instead of building a stanza object from an existing
+ XML object.
+
+ :param xml: An existing XML object to use for the stanza's content
+ instead of generating new XML.
+ """
+ if self.xml is None:
+ self.xml = xml
+
+ if self.xml is None:
+ # Generate XML from the stanza definition
+ for ename in self.name.split('/'):
+ new = ET.Element("{%s}%s" % (self.namespace, ename))
+ if self.xml is None:
+ self.xml = new
+ else:
+ last_xml.append(new)
+ last_xml = new
+ if self.parent is not None:
+ self.parent().xml.append(self.xml)
+
+ # We had to generate XML
+ return True
+ else:
+ # We did not generate XML
+ return False
+
+ def enable(self, attrib):
+ """Enable and initialize a stanza plugin.
+
+ Alias for :meth:`init_plugin`.
+
+ :param string attrib: The :attr:`plugin_attrib` value of the
+ plugin to enable.
+ """
+ return self.init_plugin(attrib)
+
+ def init_plugin(self, attrib):
+ """Enable and initialize a stanza plugin.
+
+ :param string attrib: The :attr:`plugin_attrib` value of the
+ plugin to enable.
+ """
+ if attrib not in self.plugins:
+ plugin_class = self.plugin_attrib_map[attrib]
+ existing_xml = self.xml.find(plugin_class.tag_name())
+ plugin = plugin_class(parent=self, xml=existing_xml)
+ self.plugins[attrib] = plugin
+ if plugin_class in self.plugin_iterables:
+ self.iterables.append(plugin)
+ return self
+
+ def _get_stanza_values(self):
+ """Return A JSON/dictionary version of the XML content
+ exposed through the stanza's interfaces::
+
+ >>> msg = Message()
+ >>> msg.values
+ {'body': '', 'from': , 'mucnick': '', 'mucroom': '',
+ 'to': , 'type': 'normal', 'id': '', 'subject': ''}
+
+ Likewise, assigning to :attr:`values` will change the XML
+ content::
+
+ >>> msg = Message()
+ >>> msg.values = {'body': 'Hi!', 'to': 'user@example.com'}
+ >>> msg
+ '<message to="user@example.com"><body>Hi!</body></message>'
+
+ .. versionadded:: 1.0-Beta1
+ """
+ values = {}
+ for interface in self.interfaces:
+ values[interface] = self[interface]
+ for plugin, stanza in self.plugins.items():
+ values[plugin] = stanza.values
+ if self.iterables:
+ iterables = []
+ for stanza in self.iterables:
+ iterables.append(stanza.values)
+ iterables[-1]['__childtag__'] = stanza.tag
+ values['substanzas'] = iterables
+ return values
+
+ def _set_stanza_values(self, values):
+ """Set multiple stanza interface values using a dictionary.
+
+ Stanza plugin values may be set using nested dictionaries.
+
+ :param values: A dictionary mapping stanza interface with values.
+ Plugin interfaces may accept a nested dictionary that
+ will be used recursively.
+
+ .. versionadded:: 1.0-Beta1
+ """
+ iterable_interfaces = [p.plugin_attrib for \
+ p in self.plugin_iterables]
+
+ for interface, value in values.items():
+ if interface == 'substanzas':
+ # Remove existing substanzas
+ for stanza in self.iterables:
+ self.xml.remove(stanza.xml)
+ self.iterables = []
+
+ # Add new substanzas
+ for subdict in value:
+ if '__childtag__' in subdict:
+ for subclass in self.plugin_iterables:
+ child_tag = "{%s}%s" % (subclass.namespace,
+ subclass.name)
+ if subdict['__childtag__'] == child_tag:
+ sub = subclass(parent=self)
+ sub.values = subdict
+ self.iterables.append(sub)
+ break
+ elif interface in self.interfaces:
+ self[interface] = value
+ elif interface in self.plugin_attrib_map:
+ if interface not in iterable_interfaces:
+ if interface not in self.plugins:
+ self.init_plugin(interface)
+ self.plugins[interface].values = value
+ return self
+
+ def __getitem__(self, attrib):
+ """Return the value of a stanza interface using dict-like syntax.
+
+ Example::
+
+ >>> msg['body']
+ 'Message contents'
+
+ Stanza interfaces are typically mapped directly to the underlying XML
+ object, but can be overridden by the presence of a ``get_attrib``
+ method (or ``get_foo`` where the interface is named ``'foo'``, etc).
+
+ The search order for interface value retrieval for an interface
+ named ``'foo'`` is:
+
+ 1. The list of substanzas (``'substanzas'``)
+ 2. The result of calling the ``get_foo`` override handler.
+ 3. The result of calling ``get_foo``.
+ 4. The result of calling ``getFoo``.
+ 5. The contents of the ``foo`` subelement, if ``foo`` is listed
+ in :attr:`sub_interfaces`.
+ 6. The value of the ``foo`` attribute of the XML object.
+ 7. The plugin named ``'foo'``
+ 8. An empty string.
+
+ :param string attrib: The name of the requested stanza interface.
+ """
+ if attrib == 'substanzas':
+ return self.iterables
+ elif attrib in self.interfaces:
+ get_method = "get_%s" % attrib.lower()
+ get_method2 = "get%s" % attrib.title()
+
+ if self.plugin_overrides:
+ plugin = self.plugin_overrides.get(get_method, None)
+ if plugin:
+ if plugin not in self.plugins:
+ self.init_plugin(plugin)
+ handler = getattr(self.plugins[plugin], get_method, None)
+ if handler:
+ return handler()
+
+ if hasattr(self, get_method):
+ return getattr(self, get_method)()
+ elif hasattr(self, get_method2):
+ return getattr(self, get_method2)()
+ else:
+ if attrib in self.sub_interfaces:
+ return self._get_sub_text(attrib)
+ else:
+ return self._get_attr(attrib)
+ elif attrib in self.plugin_attrib_map:
+ if attrib not in self.plugins:
+ self.init_plugin(attrib)
+ if self.plugins[attrib].is_extension:
+ return self.plugins[attrib][attrib]
+ return self.plugins[attrib]
+ else:
+ return ''
+
+ def __setitem__(self, attrib, value):
+ """Set the value of a stanza interface using dictionary-like syntax.
+
+ Example::
+
+ >>> msg['body'] = "Hi!"
+ >>> msg['body']
+ 'Hi!'
+
+ Stanza interfaces are typically mapped directly to the underlying XML
+ object, but can be overridden by the presence of a ``set_attrib``
+ method (or ``set_foo`` where the interface is named ``'foo'``, etc).
+
+ The effect of interface value assignment for an interface
+ named ``'foo'`` will be one of:
+
+ 1. Delete the interface's contents if the value is None.
+ 2. Call the ``set_foo`` override handler, if it exists.
+ 3. Call ``set_foo``, if it exists.
+ 4. Call ``setFoo``, if it exists.
+ 5. Set the text of a ``foo`` element, if ``'foo'`` is
+ in :attr:`sub_interfaces`.
+ 6. Set the value of a top level XML attribute named ``foo``.
+ 7. Attempt to pass the value to a plugin named ``'foo'`` using
+ the plugin's ``'foo'`` interface.
+ 8. Do nothing.
+
+ :param string attrib: The name of the stanza interface to modify.
+ :param value: The new value of the stanza interface.
+ """
+ if attrib in self.interfaces:
+ if value is not None:
+ set_method = "set_%s" % attrib.lower()
+ set_method2 = "set%s" % attrib.title()
+
+ if self.plugin_overrides:
+ plugin = self.plugin_overrides.get(set_method, None)
+ if plugin:
+ if plugin not in self.plugins:
+ self.init_plugin(plugin)
+ handler = getattr(self.plugins[plugin],
+ set_method, None)
+ if handler:
+ return handler(value)
+
+ if hasattr(self, set_method):
+ getattr(self, set_method)(value,)
+ elif hasattr(self, set_method2):
+ getattr(self, set_method2)(value,)
+ else:
+ if attrib in self.sub_interfaces:
+ return self._set_sub_text(attrib, text=value)
+ else:
+ self._set_attr(attrib, value)
+ else:
+ self.__delitem__(attrib)
+ elif attrib in self.plugin_attrib_map:
+ if attrib not in self.plugins:
+ self.init_plugin(attrib)
+ self.plugins[attrib][attrib] = value
+ return self
+
+ def __delitem__(self, attrib):
+ """Delete the value of a stanza interface using dict-like syntax.
+
+ Example::
+
+ >>> msg['body'] = "Hi!"
+ >>> msg['body']
+ 'Hi!'
+ >>> del msg['body']
+ >>> msg['body']
+ ''
+
+ Stanza interfaces are typically mapped directly to the underlyig XML
+ object, but can be overridden by the presence of a ``del_attrib``
+ method (or ``del_foo`` where the interface is named ``'foo'``, etc).
+
+ The effect of deleting a stanza interface value named ``foo`` will be
+ one of:
+
+ 1. Call ``del_foo`` override handler, if it exists.
+ 2. Call ``del_foo``, if it exists.
+ 3. Call ``delFoo``, if it exists.
+ 4. Delete ``foo`` element, if ``'foo'`` is in
+ :attr:`sub_interfaces`.
+ 5. Delete top level XML attribute named ``foo``.
+ 6. Remove the ``foo`` plugin, if it was loaded.
+ 7. Do nothing.
+
+ :param attrib: The name of the affected stanza interface.
+ """
+ if attrib in self.interfaces:
+ del_method = "del_%s" % attrib.lower()
+ del_method2 = "del%s" % attrib.title()
+
+ if self.plugin_overrides:
+ plugin = self.plugin_overrides.get(del_method, None)
+ if plugin:
+ if plugin not in self.plugins:
+ self.init_plugin(plugin)
+ handler = getattr(self.plugins[plugin], del_method, None)
+ if handler:
+ return handler()
+
+ if hasattr(self, del_method):
+ getattr(self, del_method)()
+ elif hasattr(self, del_method2):
+ getattr(self, del_method2)()
+ else:
+ if attrib in self.sub_interfaces:
+ return self._del_sub(attrib)
+ else:
+ self._del_attr(attrib)
+ elif attrib in self.plugin_attrib_map:
+ if attrib in self.plugins:
+ xml = self.plugins[attrib].xml
+ if self.plugins[attrib].is_extension:
+ del self.plugins[attrib][attrib]
+ del self.plugins[attrib]
+ try:
+ self.xml.remove(xml)
+ except:
+ pass
+ return self
+
+ def _set_attr(self, name, value):
+ """Set the value of a top level attribute of the XML object.
+
+ If the new value is None or an empty string, then the attribute will
+ be removed.
+
+ :param name: The name of the attribute.
+ :param value: The new value of the attribute, or None or '' to
+ remove it.
+ """
+ if value is None or value == '':
+ self.__delitem__(name)
+ else:
+ self.xml.attrib[name] = value
+
+ def _del_attr(self, name):
+ """Remove a top level attribute of the XML object.
+
+ :param name: The name of the attribute.
+ """
+ if name in self.xml.attrib:
+ del self.xml.attrib[name]
+
+ def _get_attr(self, name, default=''):
+ """Return the value of a top level attribute of the XML object.
+
+ In case the attribute has not been set, a default value can be
+ returned instead. An empty string is returned if no other default
+ is supplied.
+
+ :param name: The name of the attribute.
+ :param default: Optional value to return if the attribute has not
+ been set. An empty string is returned otherwise.
+ """
+ return self.xml.attrib.get(name, default)
+
+ def _get_sub_text(self, name, default=''):
+ """Return the text contents of a sub element.
+
+ In case the element does not exist, or it has no textual content,
+ a default value can be returned instead. An empty string is returned
+ if no other default is supplied.
+
+ :param name: The name or XPath expression of the element.
+ :param default: Optional default to return if the element does
+ not exists. An empty string is returned otherwise.
+ """
+ name = self._fix_ns(name)
+ stanza = self.xml.find(name)
+ if stanza is None or stanza.text is None:
+ return default
+ else:
+ return stanza.text
+
+ def _set_sub_text(self, name, text=None, keep=False):
+ """Set the text contents of a sub element.
+
+ In case the element does not exist, a element will be created,
+ and its text contents will be set.
+
+ If the text is set to an empty string, or None, then the
+ element will be removed, unless keep is set to True.
+
+ :param name: The name or XPath expression of the element.
+ :param text: The new textual content of the element. If the text
+ is an empty string or None, the element will be removed
+ unless the parameter keep is True.
+ :param keep: Indicates if the element should be kept if its text is
+ removed. Defaults to False.
+ """
+ path = self._fix_ns(name, split=True)
+ element = self.xml.find(name)
+
+ if not text and not keep:
+ return self._del_sub(name)
+
+ if element is None:
+ # We need to add the element. If the provided name was
+ # an XPath expression, some of the intermediate elements
+ # may already exist. If so, we want to use those instead
+ # of generating new elements.
+ last_xml = self.xml
+ walked = []
+ for ename in path:
+ walked.append(ename)
+ element = self.xml.find("/".join(walked))
+ if element is None:
+ element = ET.Element(ename)
+ last_xml.append(element)
+ last_xml = element
+ element = last_xml
+
+ element.text = text
+ return element
+
+ def _del_sub(self, name, all=False):
+ """Remove sub elements that match the given name or XPath.
+
+ If the element is in a path, then any parent elements that become
+ empty after deleting the element may also be deleted if requested
+ by setting all=True.
+
+ :param name: The name or XPath expression for the element(s) to remove.
+ :param bool all: If True, remove all empty elements in the path to the
+ deleted element. Defaults to False.
+ """
+ path = self._fix_ns(name, split=True)
+ original_target = path[-1]
+
+ for level, _ in enumerate(path):
+ # Generate the paths to the target elements and their parent.
+ element_path = "/".join(path[:len(path) - level])
+ parent_path = "/".join(path[:len(path) - level - 1])
+
+ elements = self.xml.findall(element_path)
+ parent = self.xml.find(parent_path)
+
+ if elements:
+ if parent is None:
+ parent = self.xml
+ for element in elements:
+ if element.tag == original_target or \
+ not element.getchildren():
+ # Only delete the originally requested elements, and
+ # any parent elements that have become empty.
+ parent.remove(element)
+ if not all:
+ # If we don't want to delete elements up the tree, stop
+ # after deleting the first level of elements.
+ return
+
+ def match(self, xpath):
+ """Compare a stanza object with an XPath-like expression.
+
+ If the XPath matches the contents of the stanza object, the match
+ is successful.
+
+ The XPath expression may include checks for stanza attributes.
+ For example::
+
+ 'presence@show=xa@priority=2/status'
+
+ Would match a presence stanza whose show value is set to ``'xa'``,
+ has a priority value of ``'2'``, and has a status element.
+
+ :param string xpath: The XPath expression to check against. It
+ may be either a string or a list of element
+ names with attribute checks.
+ """
+ if not isinstance(xpath, list):
+ xpath = self._fix_ns(xpath, split=True, propagate_ns=False)
+
+ # Extract the tag name and attribute checks for the first XPath node.
+ components = xpath[0].split('@')
+ tag = components[0]
+ attributes = components[1:]
+
+ if tag not in (self.name, "{%s}%s" % (self.namespace, self.name)) and \
+ tag not in self.plugins and tag not in self.plugin_attrib:
+ # The requested tag is not in this stanza, so no match.
+ return False
+
+ # Check the rest of the XPath against any substanzas.
+ matched_substanzas = False
+ for substanza in self.iterables:
+ if xpath[1:] == []:
+ break
+ matched_substanzas = substanza.match(xpath[1:])
+ if matched_substanzas:
+ break
+
+ # Check attribute values.
+ for attribute in attributes:
+ name, value = attribute.split('=')
+ if self[name] != value:
+ return False
+
+ # Check sub interfaces.
+ if len(xpath) > 1:
+ next_tag = xpath[1]
+ if next_tag in self.sub_interfaces and self[next_tag]:
+ return True
+
+ # Attempt to continue matching the XPath using the stanza's plugins.
+ if not matched_substanzas and len(xpath) > 1:
+ # Convert {namespace}tag@attribs to just tag
+ next_tag = xpath[1].split('@')[0].split('}')[-1]
+ if next_tag in self.plugins:
+ return self.plugins[next_tag].match(xpath[1:])
+ else:
+ return False
+
+ # Everything matched.
+ return True
+
+ def find(self, xpath):
+ """Find an XML object in this stanza given an XPath expression.
+
+ Exposes ElementTree interface for backwards compatibility.
+
+ .. note::
+
+ Matching on attribute values is not supported in Python 2.6
+ or Python 3.1
+
+ :param string xpath: An XPath expression matching a single
+ desired element.
+ """
+ return self.xml.find(xpath)
+
+ def findall(self, xpath):
+ """Find multiple XML objects in this stanza given an XPath expression.
+
+ Exposes ElementTree interface for backwards compatibility.
+
+ .. note::
+
+ Matching on attribute values is not supported in Python 2.6
+ or Python 3.1.
+
+ :param string xpath: An XPath expression matching multiple
+ desired elements.
+ """
+ return self.xml.findall(xpath)
+
+ def get(self, key, default=None):
+ """Return the value of a stanza interface.
+
+ If the found value is None or an empty string, return the supplied
+ default value.
+
+ Allows stanza objects to be used like dictionaries.
+
+ :param string key: The name of the stanza interface to check.
+ :param default: Value to return if the stanza interface has a value
+ of ``None`` or ``""``. Will default to returning None.
+ """
+ value = self[key]
+ if value is None or value == '':
+ return default
+ return value
+
+ def keys(self):
+ """Return the names of all stanza interfaces provided by the
+ stanza object.
+
+ Allows stanza objects to be used like dictionaries.
+ """
+ out = []
+ out += [x for x in self.interfaces]
+ out += [x for x in self.plugins]
+ if self.iterables:
+ out.append('substanzas')
+ return out
+
+ def append(self, item):
+ """Append either an XML object or a substanza to this stanza object.
+
+ If a substanza object is appended, it will be added to the list
+ of iterable stanzas.
+
+ Allows stanza objects to be used like lists.
+
+ :param item: Either an XML object or a stanza object to add to
+ this stanza's contents.
+ """
+ if not isinstance(item, ElementBase):
+ if type(item) == XML_TYPE:
+ return self.appendxml(item)
+ else:
+ raise TypeError
+ self.xml.append(item.xml)
+ self.iterables.append(item)
+ return self
+
+ def appendxml(self, xml):
+ """Append an XML object to the stanza's XML.
+
+ The added XML will not be included in the list of
+ iterable substanzas.
+
+ :param XML xml: The XML object to add to the stanza.
+ """
+ self.xml.append(xml)
+ return self
+
+ def pop(self, index=0):
+ """Remove and return the last substanza in the list of
+ iterable substanzas.
+
+ Allows stanza objects to be used like lists.
+
+ :param int index: The index of the substanza to remove.
+ """
+ substanza = self.iterables.pop(index)
+ self.xml.remove(substanza.xml)
+ return substanza
+
+ def next(self):
+ """Return the next iterable substanza."""
+ return self.__next__()
+
+ def clear(self):
+ """Remove all XML element contents and plugins.
+
+ Any attribute values will be preserved.
+ """
+ for child in self.xml.getchildren():
+ self.xml.remove(child)
+ for plugin in list(self.plugins.keys()):
+ del self.plugins[plugin]
+ return self
+
+ @classmethod
+ def tag_name(cls):
+ """Return the namespaced name of the stanza's root element.
+
+ The format for the tag name is::
+
+ '{namespace}elementname'
+
+ For example, for the stanza ``<foo xmlns="bar" />``,
+ ``stanza.tag_name()`` would return ``"{bar}foo"``.
+ """
+ return "{%s}%s" % (cls.namespace, cls.name)
+
+ @property
+ def attrib(self):
+ """Return the stanza object itself.
+
+ Older implementations of stanza objects used XML objects directly,
+ requiring the use of ``.attrib`` to access attribute values.
+
+ Use of the dictionary syntax with the stanza object itself for
+ accessing stanza interfaces is preferred.
+
+ .. deprecated:: 1.0
+ """
+ return self
+
+ def _fix_ns(self, xpath, split=False, propagate_ns=True):
+ return fix_ns(xpath, split=split,
+ propagate_ns=propagate_ns,
+ default_ns=self.namespace)
+
+ def __eq__(self, other):
+ """Compare the stanza object with another to test for equality.
+
+ Stanzas are equal if their interfaces return the same values,
+ and if they are both instances of ElementBase.
+
+ :param ElementBase other: The stanza object to compare against.
+ """
+ if not isinstance(other, ElementBase):
+ return False
+
+ # Check that this stanza is a superset of the other stanza.
+ values = self.values
+ for key in other.keys():
+ if key not in values or values[key] != other[key]:
+ return False
+
+ # Check that the other stanza is a superset of this stanza.
+ values = other.values
+ for key in self.keys():
+ if key not in values or values[key] != self[key]:
+ return False
+
+ # Both stanzas are supersets of each other, therefore they
+ # must be equal.
+ return True
+
+ def __ne__(self, other):
+ """Compare the stanza object with another to test for inequality.
+
+ Stanzas are not equal if their interfaces return different values,
+ or if they are not both instances of ElementBase.
+
+ :param ElementBase other: The stanza object to compare against.
+ """
+ return not self.__eq__(other)
+
+ def __bool__(self):
+ """Stanza objects should be treated as True in boolean contexts.
+
+ Python 3.x version.
+ """
+ return True
+
+ def __nonzero__(self):
+ """Stanza objects should be treated as True in boolean contexts.
+
+ Python 2.x version.
+ """
+ return True
+
+ def __len__(self):
+ """Return the number of iterable substanzas in this stanza."""
+ return len(self.iterables)
+
+ def __iter__(self):
+ """Return an iterator object for the stanza's substanzas.
+
+ The iterator is the stanza object itself. Attempting to use two
+ iterators on the same stanza at the same time is discouraged.
+ """
+ self._index = 0
+ return self
+
+ def __next__(self):
+ """Return the next iterable substanza."""
+ self._index += 1
+ if self._index > len(self.iterables):
+ self._index = 0
+ raise StopIteration
+ return self.iterables[self._index - 1]
+
+ def __copy__(self):
+ """Return a copy of the stanza object that does not share the same
+ underlying XML object.
+ """
+ return self.__class__(xml=copy.deepcopy(self.xml), parent=self.parent)
+
+ def __str__(self, top_level_ns=True):
+ """Return a string serialization of the underlying XML object.
+
+ .. seealso:: :ref:`tostring`
+
+ :param bool top_level_ns: Display the top-most namespace.
+ Defaults to True.
+ """
+ stanza_ns = '' if top_level_ns else self.namespace
+ return tostring(self.xml, xmlns='',
+ stanza_ns=stanza_ns,
+ top_level=not top_level_ns)
+
+ def __repr__(self):
+ """Use the stanza's serialized XML as its representation."""
+ return self.__str__()
+
+
+class StanzaBase(ElementBase):
+
+ """
+ StanzaBase provides the foundation for all other stanza objects used
+ by SleekXMPP, and defines a basic set of interfaces common to nearly
+ all stanzas. These interfaces are the ``'id'``, ``'type'``, ``'to'``,
+ and ``'from'`` attributes. An additional interface, ``'payload'``, is
+ available to access the XML contents of the stanza. Most stanza objects
+ will provided more specific interfaces, however.
+
+ **Stanza Interfaces:**
+
+ :id: An optional id value that can be used to associate stanzas
+ :to: A JID object representing the recipient's JID.
+ :from: A JID object representing the sender's JID.
+ with their replies.
+ :type: The type of stanza, typically will be ``'normal'``,
+ ``'error'``, ``'get'``, or ``'set'``, etc.
+ :payload: The XML contents of the stanza.
+
+ :param XMLStream stream: Optional :class:`sleekxmpp.xmlstream.XMLStream`
+ object responsible for sending this stanza.
+ :param XML xml: Optional XML contents to initialize stanza values.
+ :param string stype: Optional stanza type value.
+ :param sto: Optional string or :class:`sleekxmpp.xmlstream.JID`
+ object of the recipient's JID.
+ :param sfrom: Optional string or :class:`sleekxmpp.xmlstream.JID`
+ object of the sender's JID.
+ :param string sid: Optional ID value for the stanza.
+ """
+
+ #: The default XMPP client namespace
+ namespace = 'jabber:client'
+
+ #: There is a small set of attributes which apply to all XMPP stanzas:
+ #: the stanza type, the to and from JIDs, the stanza ID, and, especially
+ #: in the case of an Iq stanza, a payload.
+ interfaces = set(('type', 'to', 'from', 'id', 'payload'))
+
+ #: A basic set of allowed values for the ``'type'`` interface.
+ types = set(('get', 'set', 'error', None, 'unavailable', 'normal', 'chat'))
+
+ def __init__(self, stream=None, xml=None, stype=None,
+ sto=None, sfrom=None, sid=None):
+ self.stream = stream
+ if stream is not None:
+ self.namespace = stream.default_ns
+ ElementBase.__init__(self, xml)
+ if stype is not None:
+ self['type'] = stype
+ if sto is not None:
+ self['to'] = sto
+ if sfrom is not None:
+ self['from'] = sfrom
+ self.tag = "{%s}%s" % (self.namespace, self.name)
+
+ def set_type(self, value):
+ """Set the stanza's ``'type'`` attribute.
+
+ Only type values contained in :attr:`types` are accepted.
+
+ :param string value: One of the values contained in :attr:`types`
+ """
+ if value in self.types:
+ self.xml.attrib['type'] = value
+ return self
+
+ def get_to(self):
+ """Return the value of the stanza's ``'to'`` attribute."""
+ return JID(self._get_attr('to'))
+
+ def set_to(self, value):
+ """Set the ``'to'`` attribute of the stanza.
+
+ :param value: A string or :class:`sleekxmpp.xmlstream.JID` object
+ representing the recipient's JID.
+ """
+ return self._set_attr('to', str(value))
+
+ def get_from(self):
+ """Return the value of the stanza's ``'from'`` attribute."""
+ return JID(self._get_attr('from'))
+
+ def set_from(self, value):
+ """Set the 'from' attribute of the stanza.
+
+ Arguments:
+ from -- A string or JID object representing the sender's JID.
+ """
+ return self._set_attr('from', str(value))
+
+ def get_payload(self):
+ """Return a list of XML objects contained in the stanza."""
+ return self.xml.getchildren()
+
+ def set_payload(self, value):
+ """Add XML content to the stanza.
+
+ :param value: Either an XML or a stanza object, or a list
+ of XML or stanza objects.
+ """
+ if not isinstance(value, list):
+ value = [value]
+ for val in value:
+ self.append(val)
+ return self
+
+ def del_payload(self):
+ """Remove the XML contents of the stanza."""
+ self.clear()
+ return self
+
+ def reply(self, clear=True):
+ """Prepare the stanza for sending a reply.
+
+ Swaps the ``'from'`` and ``'to'`` attributes.
+
+ If ``clear=True``, then also remove the stanza's
+ contents to make room for the reply content.
+
+ For client streams, the ``'from'`` attribute is removed.
+
+ :param bool clear: Indicates if the stanza's contents should be
+ removed. Defaults to ``True``.
+ """
+ # if it's a component, use from
+ if self.stream and hasattr(self.stream, "is_component") and \
+ self.stream.is_component:
+ self['from'], self['to'] = self['to'], self['from']
+ else:
+ self['to'] = self['from']
+ del self['from']
+ if clear:
+ self.clear()
+ return self
+
+ def error(self):
+ """Set the stanza's type to ``'error'``."""
+ self['type'] = 'error'
+ return self
+
+ def unhandled(self):
+ """Called if no handlers have been registered to process this stanza.
+
+ Meant to be overridden.
+ """
+ pass
+
+ def exception(self, e):
+ """Handle exceptions raised during stanza processing.
+
+ Meant to be overridden.
+ """
+ log.exception('Error handling {%s}%s stanza', self.namespace,
+ self.name)
+
+ def send(self, now=False):
+ """Queue the stanza to be sent on the XML stream.
+
+ :param bool now: Indicates if the queue should be skipped and the
+ stanza sent immediately. Useful for stream
+ initialization. Defaults to ``False``.
+ """
+ self.stream.send(self, now=now)
+
+ def __copy__(self):
+ """Return a copy of the stanza object that does not share the
+ same underlying XML object, but does share the same XML stream.
+ """
+ return self.__class__(xml=copy.deepcopy(self.xml),
+ stream=self.stream)
+
+ def __str__(self, top_level_ns=False):
+ """Serialize the stanza's XML to a string.
+
+ :param bool top_level_ns: Display the top-most namespace.
+ Defaults to ``False``.
+ """
+ stanza_ns = '' if top_level_ns else self.namespace
+ return tostring(self.xml, xmlns='',
+ stanza_ns=stanza_ns,
+ stream=self.stream,
+ top_level=not top_level_ns)
+
+
+#: A JSON/dictionary version of the XML content exposed through
+#: the stanza interfaces::
+#:
+#: >>> msg = Message()
+#: >>> msg.values
+#: {'body': '', 'from': , 'mucnick': '', 'mucroom': '',
+#: 'to': , 'type': 'normal', 'id': '', 'subject': ''}
+#:
+#: Likewise, assigning to the :attr:`values` will change the XML
+#: content::
+#:
+#: >>> msg = Message()
+#: >>> msg.values = {'body': 'Hi!', 'to': 'user@example.com'}
+#: >>> msg
+#: '<message to="user@example.com"><body>Hi!</body></message>'
+#:
+#: Child stanzas are exposed as nested dictionaries.
+ElementBase.values = property(ElementBase._get_stanza_values,
+ ElementBase._set_stanza_values)
+
+
+# To comply with PEP8, method names now use underscores.
+# Deprecated method names are re-mapped for backwards compatibility.
+ElementBase.initPlugin = ElementBase.init_plugin
+ElementBase._getAttr = ElementBase._get_attr
+ElementBase._setAttr = ElementBase._set_attr
+ElementBase._delAttr = ElementBase._del_attr
+ElementBase._getSubText = ElementBase._get_sub_text
+ElementBase._setSubText = ElementBase._set_sub_text
+ElementBase._delSub = ElementBase._del_sub
+ElementBase.getStanzaValues = ElementBase._get_stanza_values
+ElementBase.setStanzaValues = ElementBase._set_stanza_values
+
+StanzaBase.setType = StanzaBase.set_type
+StanzaBase.getTo = StanzaBase.get_to
+StanzaBase.setTo = StanzaBase.set_to
+StanzaBase.getFrom = StanzaBase.get_from
+StanzaBase.setFrom = StanzaBase.set_from
+StanzaBase.getPayload = StanzaBase.get_payload
+StanzaBase.setPayload = StanzaBase.set_payload
+StanzaBase.delPayload = StanzaBase.del_payload
diff --git a/sleekxmpp/xmlstream/test.py b/sleekxmpp/xmlstream/test.py
new file mode 100644
index 00000000..a45fb8b4
--- /dev/null
+++ b/sleekxmpp/xmlstream/test.py
@@ -0,0 +1,23 @@
+import xmlstream
+import time
+import socket
+from handler.callback import Callback
+from matcher.xpath import MatchXPath
+
+def server():
+ s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+ s.bind(('localhost', 5228))
+ s.listen(1)
+ servers = []
+ while True:
+ conn, addr = s.accept()
+ server = xmlstream.XMLStream(conn, 'localhost', 5228)
+ server.registerHandler(Callback('test', MatchXPath('test'), testHandler))
+ server.process()
+ servers.append(server)
+
+def testHandler(xml):
+ print("weeeeeeeee!")
+
+server()
diff --git a/sleekxmpp/xmlstream/test.xml b/sleekxmpp/xmlstream/test.xml
new file mode 100644
index 00000000..d20dd82c
--- /dev/null
+++ b/sleekxmpp/xmlstream/test.xml
@@ -0,0 +1,2 @@
+<stream>
+</stream>
diff --git a/sleekxmpp/xmlstream/testclient.py b/sleekxmpp/xmlstream/testclient.py
new file mode 100644
index 00000000..50eb6c50
--- /dev/null
+++ b/sleekxmpp/xmlstream/testclient.py
@@ -0,0 +1,13 @@
+import socket
+import time
+
+s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+s.connect(('localhost', 5228))
+s.send("<stream>")
+#s.flush()
+s.send("<test/>")
+s.send("<test/>")
+s.send("<test/>")
+s.send("</stream>")
+#s.flush()
+s.close()
diff --git a/sleekxmpp/xmlstream/tostring.py b/sleekxmpp/xmlstream/tostring.py
new file mode 100644
index 00000000..8e729f79
--- /dev/null
+++ b/sleekxmpp/xmlstream/tostring.py
@@ -0,0 +1,131 @@
+# -*- coding: utf-8 -*-
+"""
+ sleekxmpp.xmlstream.tostring
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+ This module converts XML objects into Unicode strings and
+ intelligently includes namespaces only when necessary to
+ keep the output readable.
+
+ Part of SleekXMPP: The Sleek XMPP Library
+
+ :copyright: (c) 2011 Nathanael C. Fritz
+ :license: MIT, see LICENSE for more details
+"""
+
+import sys
+
+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.
+
+ If namespaces are provided using ``xmlns`` or ``stanza_ns``, then
+ elements that use those namespaces will not include the xmlns attribute
+ in the output.
+
+ :param XML xml: The XML object to serialize.
+ :param string xmlns: Optional namespace of an element wrapping the XML
+ object.
+ :param string stanza_ns: The namespace of the stanza object that contains
+ the XML object.
+ :param stream: The XML stream that generated the XML object.
+ :param string outbuffer: Optional buffer for storing serializations
+ during recursive calls.
+ :param bool top_level: Indicates that the element is the outermost
+ element.
+
+
+ :type xml: :py:class:`~xml.etree.ElementTree.Element`
+ :type stream: :class:`~sleekxmpp.xmlstream.xmlstream.XMLStream`
+
+ :rtype: Unicode string
+ """
+ # 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 = ''
+
+ 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 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)
+ output.append("<%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(">")
+ 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("</%s>" % tag_name)
+ elif xml.text:
+ # If we only have text content.
+ output.append(">%s</%s>" % (xml_escape(xml.text), tag_name))
+ else:
+ # Empty element.
+ output.append(" />")
+ if xml.tail:
+ # If there is additional text after the element.
+ output.append(xml_escape(xml.tail))
+ return ''.join(output)
+
+
+def xml_escape(text):
+ """Convert special characters in XML to escape sequences.
+
+ :param string text: The XML text to convert.
+ :rtype: Unicode string
+ """
+ if sys.version_info < (3, 0):
+ if type(text) != types.UnicodeType:
+ text = unicode(text, 'utf-8', 'ignore')
+
+ text = list(text)
+ escapes = {'&': '&amp;',
+ '<': '&lt;',
+ '>': '&gt;',
+ "'": '&apos;',
+ '"': '&quot;'}
+ for i, c in enumerate(text):
+ text[i] = escapes.get(c, c)
+ return ''.join(text)
diff --git a/sleekxmpp/xmlstream/xmlstream.py b/sleekxmpp/xmlstream/xmlstream.py
new file mode 100644
index 00000000..4c8696b3
--- /dev/null
+++ b/sleekxmpp/xmlstream/xmlstream.py
@@ -0,0 +1,1479 @@
+# -*- coding: utf-8 -*-
+"""
+ sleekxmpp.xmlstream.xmlstream
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+ This module provides the module for creating and
+ interacting with generic XML streams, along with
+ the necessary eventing infrastructure.
+
+ Part of SleekXMPP: The Sleek XMPP Library
+
+ :copyright: (c) 2011 Nathanael C. Fritz
+ :license: MIT, see LICENSE for more details
+"""
+
+from __future__ import with_statement, unicode_literals
+
+import base64
+import copy
+import logging
+import signal
+import socket as Socket
+import ssl
+import sys
+import threading
+import time
+import types
+import random
+import weakref
+try:
+ import queue
+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, ElementBase
+from sleekxmpp.xmlstream.handler import Waiter, XMLCallback
+from sleekxmpp.xmlstream.matcher import MatchXMLMask
+
+# In Python 2.x, file socket objects are broken. A patched socket
+# wrapper is provided for this case in filesocket.py.
+if sys.version_info < (3, 0):
+ from sleekxmpp.xmlstream.filesocket import FileSocket, Socket26
+
+try:
+ import dns.resolver
+except ImportError:
+ DNSPYTHON = False
+else:
+ DNSPYTHON = True
+
+
+#: The time in seconds to wait before timing out waiting for response stanzas.
+RESPONSE_TIMEOUT = 30
+
+#: The time in seconds to wait for events from the event queue, and also the
+#: time between checks for the process stop signal.
+WAIT_TIMEOUT = 1
+
+#: The number of threads to use to handle XML stream events. This is not the
+#: same as the number of custom event handling threads.
+#: :data:`HANDLER_THREADS` must be at least 1. For Python implementations
+#: with a GIL, this should be left at 1, but for implemetnations without
+#: a GIL increasing this value can provide better performance.
+HANDLER_THREADS = 1
+
+#: Flag indicating if the SSL library is available for use.
+SSL_SUPPORT = True
+
+#: The time in seconds to delay between attempts to resend data
+#: after an SSL error.
+SSL_RETRY_DELAY = 0.5
+
+#: The maximum number of times to attempt resending data due to
+#: an SSL error.
+SSL_RETRY_MAX = 10
+
+#: Maximum time to delay between connection attempts is one hour.
+RECONNECT_MAX_DELAY = 600
+
+
+log = logging.getLogger(__name__)
+
+
+class RestartStream(Exception):
+ """
+ Exception to restart stream processing, including
+ resending the stream header.
+ """
+
+
+class XMLStream(object):
+ """
+ An XML stream connection manager and event dispatcher.
+
+ The XMLStream class abstracts away the issues of establishing a
+ connection with a server and sending and receiving XML "stanzas".
+ A stanza is a complete XML element that is a direct child of a root
+ document element. Two streams are used, one for each communication
+ direction, over the same socket. Once the connection is closed, both
+ streams should be complete and valid XML documents.
+
+ Three types of events are provided to manage the stream:
+ :Stream: Triggered based on received stanzas, similar in concept
+ to events in a SAX XML parser.
+ :Custom: Triggered manually.
+ :Scheduled: Triggered based on time delays.
+
+ Typically, stanzas are first processed by a stream event handler which
+ will then trigger custom events to continue further processing,
+ especially since custom event handlers may run in individual threads.
+
+ :param socket: Use an existing socket for the stream. Defaults to
+ ``None`` to generate a new socket.
+ :param string host: The name of the target server.
+ :param int port: The port to use for the connection. Defaults to 0.
+ """
+
+ def __init__(self, socket=None, host='', port=0):
+ #: Flag indicating if the SSL library is available for use.
+ self.ssl_support = SSL_SUPPORT
+
+ #: Most XMPP servers support TLSv1, but OpenFire in particular
+ #: does not work well with it. For OpenFire, set
+ #: :attr:`ssl_version` to use ``SSLv23``::
+ #:
+ #: import ssl
+ #: xmpp.ssl_version = ssl.PROTOCOL_SSLv23
+ self.ssl_version = ssl.PROTOCOL_TLSv1
+
+ #: Path to a file containing certificates for verifying the
+ #: server SSL certificate. A non-``None`` value will trigger
+ #: certificate checking.
+ #:
+ #: .. note::
+ #:
+ #: On Mac OS X, certificates in the system keyring will
+ #: be consulted, even if they are not in the provided file.
+ self.ca_certs = None
+
+ #: The time in seconds to wait for events from the event queue,
+ #: and also the time between checks for the process stop signal.
+ self.wait_timeout = WAIT_TIMEOUT
+
+ #: The time in seconds to wait before timing out waiting
+ #: for response stanzas.
+ self.response_timeout = RESPONSE_TIMEOUT
+
+ #: The current amount to time to delay attempting to reconnect.
+ #: This value doubles (with some jitter) with each failed
+ #: connection attempt up to :attr:`reconnect_max_delay` seconds.
+ self.reconnect_delay = None
+
+ #: Maximum time to delay between connection attempts is one hour.
+ self.reconnect_max_delay = RECONNECT_MAX_DELAY
+
+ #: The time in seconds to delay between attempts to resend data
+ #: after an SSL error.
+ self.ssl_retry_max = SSL_RETRY_MAX
+
+ #: The maximum number of times to attempt resending data due to
+ #: an SSL error.
+ self.ssl_retry_delay = SSL_RETRY_DELAY
+
+ #: The connection state machine tracks if the stream is
+ #: ``'connected'`` or ``'disconnected'``.
+ self.state = StateMachine(('disconnected', 'connected'))
+ self.state._set_state('disconnected')
+
+ #: The default port to return when querying DNS records.
+ self.default_port = int(port)
+
+ #: The domain to try when querying DNS records.
+ self.default_domain = ''
+
+ #: The desired, or actual, address of the connected server.
+ self.address = (host, int(port))
+
+ #: A file-like wrapper for the socket for use with the
+ #: :mod:`~xml.etree.ElementTree` module.
+ self.filesocket = None
+ self.set_socket(socket)
+
+ if sys.version_info < (3, 0):
+ self.socket_class = Socket26
+ else:
+ self.socket_class = Socket.socket
+
+ #: Enable connecting to the server directly over SSL, in
+ #: particular when the service provides two ports: one for
+ #: non-SSL traffic and another for SSL traffic.
+ self.use_ssl = False
+
+ #: Enable connecting to the service without using SSL
+ #: immediately, but allow upgrading the connection later
+ #: to use SSL.
+ self.use_tls = False
+
+ #: If set to ``True``, attempt to connect through an HTTP
+ #: proxy based on the settings in :attr:`proxy_config`.
+ self.use_proxy = False
+
+ #: An optional dictionary of proxy settings. It may provide:
+ #: :host: The host offering proxy services.
+ #: :port: The port for the proxy service.
+ #: :username: Optional username for accessing the proxy.
+ #: :password: Optional password for accessing the proxy.
+ self.proxy_config = {}
+
+ #: The default namespace of the stream content, not of the
+ #: stream wrapper itself.
+ self.default_ns = ''
+
+ #: The namespace of the enveloping stream element.
+ self.stream_ns = ''
+
+ #: The default opening tag for the stream element.
+ self.stream_header = "<stream>"
+
+ #: The default closing tag for the stream element.
+ self.stream_footer = "</stream>"
+
+ #: If ``True``, periodically send a whitespace character over the
+ #: wire to keep the connection alive. Mainly useful for connections
+ #: traversing NAT.
+ self.whitespace_keepalive = True
+
+ #: The default interval between keepalive signals when
+ #: :attr:`whitespace_keepalive` is enabled.
+ self.whitespace_keepalive_interval = 300
+
+ #: An :class:`~threading.Event` to signal that the application
+ #: is stopping, and that all threads should shutdown.
+ self.stop = threading.Event()
+
+ #: An :class:`~threading.Event` to signal receiving a closing
+ #: stream tag from the server.
+ self.stream_end_event = threading.Event()
+ self.stream_end_event.set()
+
+ #: An :class:`~threading.Event` to signal the start of a stream
+ #: session. Until this event fires, the send queue is not used
+ #: and data is sent immediately over the wire.
+ self.session_started_event = threading.Event()
+
+ #: The default time in seconds to wait for a session to start
+ #: after connecting before reconnecting and trying again.
+ self.session_timeout = 45
+
+ #: A queue of stream, custom, and scheduled events to be processed.
+ self.event_queue = queue.Queue()
+
+ #: A queue of string data to be sent over the stream.
+ self.send_queue = queue.Queue()
+
+ #: A :class:`~sleekxmpp.xmlstream.scheduler.Scheduler` instance for
+ #: executing callbacks in the future based on time delays.
+ self.scheduler = Scheduler(self.stop)
+ self.__failed_send_stanza = None
+
+ #: A mapping of XML namespaces to well-known prefixes.
+ self.namespace_map = {StanzaBase.xml_ns: 'xml'}
+
+ self.__thread = {}
+ self.__root_stanza = []
+ self.__handlers = []
+ self.__event_handlers = {}
+ self.__event_handlers_lock = threading.Lock()
+ self.__filters = {'in': [], 'out': []}
+
+ self._id = 0
+ self._id_lock = threading.Lock()
+
+ #: The :attr:`auto_reconnnect` setting controls whether or not
+ #: the stream will be restarted in the event of an error.
+ self.auto_reconnect = True
+
+ #: The :attr:`disconnect_wait` setting is the default value
+ #: for controlling if the system waits for the send queue to
+ #: empty before ending the stream. This may be overridden by
+ #: passing ``wait=True`` or ``wait=False`` to :meth:`disconnect`.
+ #: The default :attr:`disconnect_wait` value is ``False``.
+ self.disconnect_wait = False
+
+ #: A list of DNS results that have not yet been tried.
+ self.dns_answers = []
+
+ self.add_event_handler('connected', self._handle_connected)
+ self.add_event_handler('session_start', self._start_keepalive)
+ self.add_event_handler('session_end', self._end_keepalive)
+
+ def use_signals(self, signals=None):
+ """Register signal handlers for ``SIGHUP`` and ``SIGTERM``.
+
+ By using signals, a ``'killed'`` event will be raised when the
+ application is terminated.
+
+ If a signal handler already existed, it will be executed first,
+ before the ``'killed'`` event is raised.
+
+ :param list signals: A list of signal names to be monitored.
+ Defaults to ``['SIGHUP', 'SIGTERM']``.
+ """
+ if signals is None:
+ signals = ['SIGHUP', 'SIGTERM']
+
+ existing_handlers = {}
+ for sig_name in signals:
+ if hasattr(signal, sig_name):
+ sig = getattr(signal, sig_name)
+ handler = signal.getsignal(sig)
+ if handler:
+ existing_handlers[sig] = handler
+
+ def handle_kill(signum, frame):
+ """
+ Capture kill event and disconnect cleanly after first
+ spawning the ``'killed'`` event.
+ """
+
+ if signum in existing_handlers and \
+ existing_handlers[signum] != handle_kill:
+ existing_handlers[signum](signum, frame)
+
+ self.event("killed", direct=True)
+ self.disconnect()
+
+ try:
+ for sig_name in signals:
+ if hasattr(signal, sig_name):
+ sig = getattr(signal, sig_name)
+ signal.signal(sig, handle_kill)
+ self.__signals_installed = True
+ except:
+ log.debug("Can not set interrupt signal handlers. " + \
+ "SleekXMPP is not running from a main thread.")
+
+ def new_id(self):
+ """Generate and return a new stream ID in hexadecimal form.
+
+ Many stanzas, handlers, or matchers may require unique
+ ID values. Using this method ensures that all new ID values
+ are unique in this stream.
+ """
+ with self._id_lock:
+ self._id += 1
+ return self.get_id()
+
+ def get_id(self):
+ """Return the current unique stream ID in hexadecimal form."""
+ return "%X" % self._id
+
+ def connect(self, host='', port=0, use_ssl=False,
+ use_tls=True, reattempt=True):
+ """Create a new socket and connect to the server.
+
+ Setting ``reattempt`` to ``True`` will cause connection attempts to
+ be made every second until a successful connection is established.
+
+ :param host: The name of the desired server for the connection.
+ :param port: Port to connect to on the server.
+ :param use_ssl: Flag indicating if SSL should be used by connecting
+ directly to a port using SSL.
+ :param use_tls: Flag indicating if TLS should be used, allowing for
+ connecting to a port without using SSL immediately and
+ later upgrading the connection.
+ :param reattempt: Flag indicating if the socket should reconnect
+ after disconnections.
+ """
+ if host and port:
+ self.address = (host, int(port))
+ try:
+ Socket.inet_aton(self.address[0])
+ except Socket.error:
+ self.default_domain = self.address[0]
+
+ # Respect previous SSL and TLS usage directives.
+ if use_ssl is not None:
+ self.use_ssl = use_ssl
+ if use_tls is not None:
+ self.use_tls = use_tls
+
+ # Repeatedly attempt to connect until a successful connection
+ # is established.
+ connected = self.state.transition('disconnected', 'connected',
+ func=self._connect)
+ while reattempt and not connected and not self.stop.is_set():
+ connected = self.state.transition('disconnected', 'connected',
+ func=self._connect)
+ return connected
+
+ def _connect(self):
+ self.scheduler.remove('Session timeout check')
+ self.stop.clear()
+ if self.default_domain:
+ self.address = self.pick_dns_answer(self.default_domain,
+ self.address[1])
+ self.socket = self.socket_class(Socket.AF_INET, Socket.SOCK_STREAM)
+ self.configure_socket()
+
+ if self.reconnect_delay is None:
+ delay = 1.0
+ else:
+ delay = min(self.reconnect_delay * 2, self.reconnect_max_delay)
+ delay = random.normalvariate(delay, delay * 0.1)
+ log.debug('Waiting %s seconds before connecting.', delay)
+ elapsed = 0
+ try:
+ while elapsed < delay and not self.stop.is_set():
+ time.sleep(0.1)
+ elapsed += 0.1
+ except KeyboardInterrupt:
+ self.stop.set()
+ return False
+ except SystemExit:
+ self.stop.set()
+ return False
+
+ 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:
+ cert_policy = ssl.CERT_NONE
+ else:
+ cert_policy = ssl.CERT_REQUIRED
+
+ ssl_socket = ssl.wrap_socket(self.socket,
+ ca_certs=self.ca_certs,
+ cert_reqs=cert_policy)
+
+ if hasattr(self.socket, 'socket'):
+ # We are using a testing socket, so preserve the top
+ # layer of wrapping.
+ self.socket.socket = ssl_socket
+ else:
+ self.socket = ssl_socket
+
+ try:
+ 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)
+ self.reconnect_delay = 1.0
+ 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)
+ self.reconnect_delay = delay
+ return 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 and not self.stop.is_set():
+ 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 _handle_connected(self, event=None):
+ """
+ Add check to ensure that a session is established within
+ a reasonable amount of time.
+ """
+
+ def _handle_session_timeout():
+ if not self.session_started_event.is_set():
+ log.debug("Session start has taken more " + \
+ "than %d seconds", self.session_timeout)
+ self.disconnect(reconnect=self.auto_reconnect)
+
+ self.schedule("Session timeout check",
+ self.session_timeout,
+ _handle_session_timeout)
+
+ def disconnect(self, reconnect=False, wait=None):
+ """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``.
+
+ .. warning::
+
+ 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.
+
+ :param reconnect: Flag indicating if the connection
+ and processing should be restarted.
+ Defaults to ``False``.
+ :param wait: Flag indicating if the send queue should
+ be emptied before disconnecting, overriding
+ :attr:`disconnect_wait`.
+ """
+ self.state.transition('connected', 'disconnected',
+ func=self._disconnect, args=(reconnect, wait))
+
+ def _disconnect(self, reconnect=False, wait=None):
+ self.event('session_end', direct=True)
+
+ # Wait for the send queue to empty.
+ if wait is not None:
+ if wait:
+ self.send_queue.join()
+ elif self.disconnect_wait:
+ self.send_queue.join()
+
+ # Send the end of stream marker.
+ self.send_raw(self.stream_footer, now=True)
+ self.session_started_event.clear()
+ # Wait for confirmation that the stream was
+ # closed in the other direction.
+ self.auto_reconnect = reconnect
+ log.debug('Waiting for %s from server', self.stream_footer)
+ self.stream_end_event.wait(4)
+ if not self.auto_reconnect:
+ self.stop.set()
+ try:
+ self.socket.shutdown(Socket.SHUT_RDWR)
+ self.socket.close()
+ self.filesocket.close()
+ except Socket.error as serr:
+ self.event('socket_error', serr)
+ finally:
+ #clear your application state
+ self.event("disconnected", direct=True)
+ return True
+
+ def reconnect(self, reattempt=True):
+ """Reset the stream's state and reconnect to the server."""
+ log.debug("reconnecting...")
+ if self.state.ensure('connected'):
+ self.state.transition('connected', 'disconnected', wait=2.0,
+ func=self._disconnect, args=(True,))
+
+ log.debug("connecting...")
+ connected = self.state.transition('disconnected', 'connected',
+ wait=2.0, func=self._connect)
+ while reattempt and not connected and not self.stop.is_set():
+ connected = self.state.transition('disconnected', 'connected',
+ wait=2.0, func=self._connect)
+ connected = connected or self.state.ensure('connected')
+ return connected
+
+ def set_socket(self, socket, ignore=False):
+ """Set the socket to use for the stream.
+
+ The filesocket will be recreated as well.
+
+ :param socket: The new socket object to use.
+ :param bool ignore: If ``True``, don't set the connection
+ state to ``'connected'``.
+ """
+ self.socket = socket
+ if socket is not None:
+ # ElementTree.iterparse requires a file.
+ # 0 buffer files have to be binary.
+
+ # Use the correct fileobject type based on the Python
+ # version to work around a broken implementation in
+ # Python 2.x.
+ if sys.version_info < (3, 0):
+ self.filesocket = FileSocket(self.socket)
+ else:
+ self.filesocket = self.socket.makefile('rb', 0)
+ if not ignore:
+ self.state._set_state('connected')
+
+ def configure_socket(self):
+ """Set timeout and other options for self.socket.
+
+ Meant to be overridden.
+ """
+ self.socket.settimeout(None)
+
+ def configure_dns(self, resolver, domain=None, port=None):
+ """
+ Configure and set options for a :class:`~dns.resolver.Resolver`
+ instance, and other DNS related tasks. For example, you
+ can also check :meth:`~socket.socket.getaddrinfo` to see
+ if you need to call out to ``libresolv.so.2`` to
+ run ``res_init()``.
+
+ Meant to be overridden.
+
+ :param resolver: A :class:`~dns.resolver.Resolver` instance
+ or ``None`` if ``dnspython`` is not installed.
+ :param domain: The initial domain under consideration.
+ :param port: The initial port under consideration.
+ """
+ pass
+
+ def start_tls(self):
+ """Perform handshakes for TLS.
+
+ If the handshake is successful, the XML stream will need
+ to be restarted.
+ """
+ if self.ssl_support:
+ log.info("Negotiating TLS")
+ log.info("Using SSL version: %s", str(self.ssl_version))
+ if self.ca_certs is None:
+ cert_policy = ssl.CERT_NONE
+ else:
+ cert_policy = ssl.CERT_REQUIRED
+
+ ssl_socket = ssl.wrap_socket(self.socket,
+ ssl_version=self.ssl_version,
+ do_handshake_on_connect=False,
+ ca_certs=self.ca_certs,
+ cert_reqs=cert_policy)
+
+ if hasattr(self.socket, 'socket'):
+ # We are using a testing socket, so preserve the top
+ # layer of wrapping.
+ self.socket.socket = ssl_socket
+ else:
+ self.socket = ssl_socket
+ self.socket.do_handshake()
+ self.set_socket(self.socket)
+ return True
+ else:
+ log.warning("Tried to enable TLS, but ssl module not found.")
+ return False
+
+ def _start_keepalive(self, event):
+ """Begin sending whitespace periodically to keep the connection alive.
+
+ May be disabled by setting::
+
+ self.whitespace_keepalive = False
+
+ The keepalive interval can be set using::
+
+ self.whitespace_keepalive_interval = 300
+ """
+
+ def send_keepalive():
+ if self.send_queue.empty():
+ self.send_raw(' ')
+
+ self.schedule('Whitespace Keepalive',
+ self.whitespace_keepalive_interval,
+ send_keepalive,
+ repeat=True)
+
+ def _end_keepalive(self, event):
+ """Stop sending whitespace keepalives"""
+ self.scheduler.remove('Whitespace Keepalive')
+
+ def start_stream_handler(self, xml):
+ """Perform any initialization actions, such as handshakes,
+ once the stream header has been sent.
+
+ Meant to be overridden.
+ """
+ pass
+
+ def register_stanza(self, stanza_class):
+ """Add a stanza object class as a known root stanza.
+
+ A root stanza is one that appears as a direct child of the stream's
+ root element.
+
+ Stanzas that appear as substanzas of a root stanza do not need to
+ be registered here. That is done using register_stanza_plugin() from
+ sleekxmpp.xmlstream.stanzabase.
+
+ Stanzas that are not registered will not be converted into
+ stanza objects, but may still be processed using handlers and
+ matchers.
+
+ :param stanza_class: The top-level stanza object's class.
+ """
+ self.__root_stanza.append(stanza_class)
+
+ def remove_stanza(self, stanza_class):
+ """Remove a stanza from being a known root stanza.
+
+ A root stanza is one that appears as a direct child of the stream's
+ root element.
+
+ Stanzas that are not registered will not be converted into
+ stanza objects, but may still be processed using handlers and
+ matchers.
+ """
+ del self.__root_stanza[stanza_class]
+
+ def add_filter(self, mode, handler, order=None):
+ """Add a filter for incoming or outgoing stanzas.
+
+ These filters are applied before incoming stanzas are
+ passed to any handlers, and before outgoing stanzas
+ are put in the send queue.
+
+ Each filter must accept a single stanza, and return
+ either a stanza or ``None``. If the filter returns
+ ``None``, then the stanza will be dropped from being
+ processed for events or from being sent.
+
+ :param mode: One of ``'in'`` or ``'out'``.
+ :param handler: The filter function.
+ :param int order: The position to insert the filter in
+ the list of active filters.
+ """
+ if order:
+ self.__filters[mode].insert(order, handler)
+ else:
+ self.__filters[mode].append(handler)
+
+ def add_handler(self, mask, pointer, name=None, disposable=False,
+ threaded=False, filter=False, instream=False):
+ """A shortcut method for registering a handler using XML masks.
+
+ The use of :meth:`register_handler()` is preferred.
+
+ :param mask: An XML snippet matching the structure of the
+ stanzas that will be passed to this handler.
+ :param pointer: The handler function itself.
+ :parm name: A unique name for the handler. A name will
+ be generated if one is not provided.
+ :param disposable: Indicates if the handler should be discarded
+ after one use.
+ :param threaded: **DEPRECATED**.
+ Remains for backwards compatibility.
+ :param filter: **DEPRECATED**.
+ Remains for backwards compatibility.
+ :param instream: Indicates if the handler should execute during
+ stream processing and not during normal event
+ processing.
+ """
+ # To prevent circular dependencies, we must load the matcher
+ # and handler classes here.
+
+ if name is None:
+ name = 'add_handler_%s' % self.getNewId()
+ self.registerHandler(XMLCallback(name, MatchXMLMask(mask), pointer,
+ once=disposable, instream=instream))
+
+ def register_handler(self, handler, before=None, after=None):
+ """Add a stream event handler that will be executed when a matching
+ stanza is received.
+
+ :param handler: The :class:`~sleekxmpp.xmlstream.handler.base.BaseHandler`
+ derived object to execute.
+ """
+ if handler.stream is None:
+ self.__handlers.append(handler)
+ handler.stream = weakref.ref(self)
+
+ def remove_handler(self, name):
+ """Remove any stream event handlers with the given name.
+
+ :param name: The name of the handler.
+ """
+ idx = 0
+ for handler in self.__handlers:
+ if handler.name == name:
+ self.__handlers.pop(idx)
+ return True
+ idx += 1
+ return False
+
+ def get_dns_records(self, domain, port=None):
+ """Get the DNS records for a domain.
+
+ :param domain: The domain in question.
+ :param port: If the results don't include a port, use this one.
+ """
+ if port is None:
+ port = self.default_port
+ if DNSPYTHON:
+ resolver = dns.resolver.get_default_resolver()
+ self.configure_dns(resolver, domain=domain, port=port)
+
+ try:
+ answers = resolver.query(domain, dns.rdatatype.A)
+ except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer):
+ log.warning("No A records for %s", domain)
+ return [((domain, port), 0, 0)]
+ except dns.exception.Timeout:
+ log.warning("DNS resolution timed out " + \
+ "for A record of %s", domain)
+ return [((domain, port), 0, 0)]
+ else:
+ return [((ans.address, port), 0, 0) for ans in answers]
+ else:
+ log.warning("dnspython is not installed -- " + \
+ "relying on OS A record resolution")
+ self.configure_dns(None, domain=domain, port=port)
+ return [((domain, port), 0, 0)]
+
+ def pick_dns_answer(self, domain, port=None):
+ """Pick a server and port from DNS answers.
+
+ Gets DNS answers if none available.
+ Removes used answer from available answers.
+
+ :param domain: The domain in question.
+ :param port: If the results don't include a port, use this one.
+ """
+ if not self.dns_answers:
+ self.dns_answers = self.get_dns_records(domain, port)
+ addresses = {}
+ intmax = 0
+ topprio = 65535
+ for answer in self.dns_answers:
+ topprio = min(topprio, answer[1])
+ for answer in self.dns_answers:
+ if answer[1] == topprio:
+ intmax += answer[2]
+ addresses[intmax] = answer[0]
+
+ #python3 returns a generator for dictionary keys
+ items = [x for x in addresses.keys()]
+ items.sort()
+
+ picked = random.randint(0, intmax)
+ for item in items:
+ if picked <= item:
+ address = addresses[item]
+ break
+ for idx, answer in enumerate(self.dns_answers):
+ if self.dns_answers[0] == address:
+ break
+ self.dns_answers.pop(idx)
+ log.debug("Trying to connect to %s:%s", *address)
+ return address
+
+ def add_event_handler(self, name, pointer,
+ threaded=False, disposable=False):
+ """Add a custom event handler that will be executed whenever
+ its event is manually triggered.
+
+ :param name: The name of the event that will trigger
+ this handler.
+ :param pointer: The function to execute.
+ :param threaded: If set to ``True``, the handler will execute
+ in its own thread. Defaults to ``False``.
+ :param disposable: If set to ``True``, the handler will be
+ discarded after one use. Defaults to ``False``.
+ """
+ if not name in self.__event_handlers:
+ self.__event_handlers[name] = []
+ self.__event_handlers[name].append((pointer, threaded, disposable))
+
+ def del_event_handler(self, name, pointer):
+ """Remove a function as a handler for an event.
+
+ :param name: The name of the event.
+ :param pointer: The function to remove as a handler.
+ """
+ if not name in self.__event_handlers:
+ return
+
+ # Need to keep handlers that do not use
+ # the given function pointer
+ def filter_pointers(handler):
+ return handler[0] != pointer
+
+ self.__event_handlers[name] = list(filter(
+ filter_pointers,
+ self.__event_handlers[name]))
+
+ def event_handled(self, name):
+ """Returns the number of registered handlers for an event.
+
+ :param name: The name of the event to check.
+ """
+ return len(self.__event_handlers.get(name, []))
+
+ def event(self, name, data={}, direct=False):
+ """Manually trigger a custom event.
+
+ :param name: The name of the event to trigger.
+ :param data: Data that will be passed to each event handler.
+ Defaults to an empty dictionary, but is usually
+ a stanza object.
+ :param direct: Runs the event directly if True, skipping the
+ event queue. All event handlers will run in the
+ same thread.
+ """
+ handlers = self.__event_handlers.get(name, [])
+ for handler in handlers:
+ #TODO: Data should not be copied, but should be read only,
+ # but this might break current code so it's left for future.
+
+ out_data = copy.copy(data) if len(handlers) > 1 else data
+ old_exception = getattr(data, 'exception', None)
+ if direct:
+ try:
+ handler[0](out_data)
+ except Exception as e:
+ error_msg = 'Error processing event handler: %s'
+ log.exception(error_msg, str(handler[0]))
+ if old_exception:
+ old_exception(e)
+ else:
+ self.exception(e)
+ else:
+ self.event_queue.put(('event', handler, out_data))
+ if handler[2]:
+ # If the handler is disposable, we will go ahead and
+ # remove it now instead of waiting for it to be
+ # processed in the queue.
+ with self.__event_handlers_lock:
+ try:
+ h_index = self.__event_handlers[name].index(handler)
+ self.__event_handlers[name].pop(h_index)
+ except:
+ pass
+
+ def schedule(self, name, seconds, callback, args=None,
+ kwargs=None, repeat=False):
+ """Schedule a callback function to execute after a given delay.
+
+ :param name: A unique name for the scheduled callback.
+ :param seconds: The time in seconds to wait before executing.
+ :param callback: A pointer to the function to execute.
+ :param args: A tuple of arguments to pass to the function.
+ :param kwargs: A dictionary of keyword arguments to pass to
+ the function.
+ :param repeat: Flag indicating if the scheduled event should
+ be reset and repeat after executing.
+ """
+ self.scheduler.add(name, seconds, callback, args, kwargs,
+ repeat, qpointer=self.event_queue)
+
+ def incoming_filter(self, xml):
+ """Filter incoming XML objects before they are processed.
+
+ Possible uses include remapping namespaces, or correcting elements
+ from sources with incorrect behavior.
+
+ Meant to be overridden.
+ """
+ return xml
+
+ def send(self, data, mask=None, timeout=None, now=False):
+ """A wrapper for :meth:`send_raw()` for sending stanza objects.
+
+ May optionally block until an expected response is received.
+
+ :param data: The :class:`~sleekxmpp.xmlstream.stanzabase.ElementBase`
+ stanza to send on the stream.
+ :param mask: **DEPRECATED**
+ An XML string snippet matching the structure
+ of the expected response. Execution will block
+ in this thread until the response is received
+ or a timeout occurs.
+ :param int timeout: Time in seconds to wait for a response before
+ continuing. Defaults to :attr:`response_timeout`.
+ :param bool now: Indicates if the send queue should be skipped,
+ sending the stanza immediately. Useful mainly
+ for stream initialization stanzas.
+ Defaults to ``False``.
+ """
+ if timeout is None:
+ timeout = self.response_timeout
+ if hasattr(mask, 'xml'):
+ mask = mask.xml
+
+ if isinstance(data, ElementBase):
+ for filter in self.__filters['out']:
+ if data is not None:
+ data = filter(data)
+ if data is None:
+ return
+
+ data = str(data)
+ if mask is not None:
+ log.warning("Use of send mask waiters is deprecated.")
+ wait_for = Waiter("SendWait_%s" % self.new_id(),
+ MatchXMLMask(mask))
+ self.register_handler(wait_for)
+ self.send_raw(data, now)
+ if mask is not None:
+ return wait_for.wait(timeout)
+
+ def send_xml(self, data, mask=None, timeout=None, now=False):
+ """Send an XML object on the stream, and optionally wait
+ for a response.
+
+ :param data: The :class:`~xml.etree.ElementTree.Element` XML object
+ to send on the stream.
+ :param mask: **DEPRECATED**
+ An XML string snippet matching the structure
+ of the expected response. Execution will block
+ in this thread until the response is received
+ or a timeout occurs.
+ :param int timeout: Time in seconds to wait for a response before
+ continuing. Defaults to :attr:`response_timeout`.
+ :param bool now: Indicates if the send queue should be skipped,
+ sending the stanza immediately. Useful mainly
+ for stream initialization stanzas.
+ Defaults to ``False``.
+ """
+ if timeout is None:
+ timeout = self.response_timeout
+ return self.send(tostring(data), mask, timeout, now)
+
+ def send_raw(self, data, now=False, reconnect=None):
+ """Send raw data across the stream.
+
+ :param string data: Any string value.
+ :param bool reconnect: Indicates if the stream should be
+ restarted if there is an error sending
+ the stanza. Used mainly for testing.
+ Defaults to :attr:`auto_reconnect`.
+ """
+ if now:
+ log.debug("SEND (IMMED): %s", data)
+ try:
+ data = data.encode('utf-8')
+ total = len(data)
+ sent = 0
+ count = 0
+ tries = 0
+ while sent < total and not self.stop.is_set():
+ try:
+ sent += self.socket.send(data[sent:])
+ count += 1
+ except ssl.SSLError as serr:
+ if tries >= self.ssl_retry_max:
+ log.debug('SSL error - max retries reached')
+ self.exception(serr)
+ log.warning("Failed to send %s", data)
+ if reconnect is None:
+ reconnect = self.auto_reconnect
+ self.disconnect(reconnect)
+ log.warning('SSL write error - reattempting')
+ time.sleep(self.ssl_retry_delay)
+ tries += 1
+ if count > 1:
+ log.debug('SENT: %d chunks', count)
+ except Socket.error as serr:
+ self.event('socket_error', serr)
+ log.warning("Failed to send %s", data)
+ if reconnect is None:
+ reconnect = self.auto_reconnect
+ self.disconnect(reconnect)
+ else:
+ self.send_queue.put(data)
+ return True
+
+ def process(self, **kwargs):
+ """Initialize the XML streams and begin processing events.
+
+ The number of threads used for processing stream events is determined
+ by :data:`HANDLER_THREADS`.
+
+ :param bool block: If ``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``.
+ :param bool threaded: **DEPRECATED**
+ If ``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``. This does **not** mean that no
+ threads are used at all if ``threaded=False``.
+
+ Regardless of these threading options, these threads will
+ always exist:
+
+ - The event queue processor
+ - The send queue processor
+ - The scheduler
+ """
+ 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):
+ self.__thread[name] = threading.Thread(name=name, target=target)
+ self.__thread[name].start()
+
+ for t in range(0, HANDLER_THREADS):
+ log.debug("Starting HANDLER THREAD")
+ start_thread('stream_event_handler_%s' % t, self._event_runner)
+
+ start_thread('send_thread', self._send_thread)
+
+ if threaded:
+ # Run the XML stream in the background for another application.
+ start_thread('process', self._process)
+ else:
+ self._process()
+
+ def _process(self):
+ """Start processing the XML streams.
+
+ Processing will continue after any recoverable errors
+ if reconnections are allowed.
+ """
+
+ # The body of this loop will only execute once per connection.
+ # Additional passes will be made only if an error occurs and
+ # reconnecting is permitted.
+ while True:
+ shutdown = False
+ try:
+ # The call to self.__read_xml will block and prevent
+ # the body of the loop from running until a disconnect
+ # occurs. After any reconnection, the stream header will
+ # be resent and processing will resume.
+ while not self.stop.is_set():
+ # Only process the stream while connected to the server
+ if not self.state.ensure('connected', wait=0.1,
+ block_on_transition=True):
+ continue
+ # Ensure the stream header is sent for any
+ # new connections.
+ if not self.session_started_event.is_set():
+ self.send_raw(self.stream_header, now=True)
+ if not self.__read_xml():
+ # If the server terminated the stream, end processing
+ break
+ except KeyboardInterrupt:
+ log.debug("Keyboard Escape Detected in _process")
+ self.event('killed', direct=True)
+ shutdown = True
+ except SystemExit:
+ log.debug("SystemExit in _process")
+ shutdown = True
+ except SyntaxError as e:
+ log.error("Error reading from XML stream.")
+ shutdown = True
+ self.exception(e)
+ except Socket.error as serr:
+ self.event('socket_error', serr)
+ log.exception('Socket Error')
+ except Exception as e:
+ if not self.stop.is_set():
+ log.exception('Connection error.')
+ self.exception(e)
+
+ if not shutdown and not self.stop.is_set() \
+ and self.auto_reconnect:
+ self.reconnect()
+ else:
+ self.disconnect()
+ break
+
+ def __read_xml(self):
+ """Parse the incoming XML stream
+
+ Stream events are raised for each received stanza.
+ """
+ depth = 0
+ root = None
+ for event, xml in ET.iterparse(self.filesocket, (b'end', b'start')):
+ if event == b'start':
+ if depth == 0:
+ # We have received the start of the root element.
+ root = xml
+ # Perform any stream initialization actions, such
+ # as handshakes.
+ self.stream_end_event.clear()
+ self.start_stream_handler(root)
+ depth += 1
+ if event == b'end':
+ depth -= 1
+ if depth == 0:
+ # The stream's root element has closed,
+ # terminating the stream.
+ log.debug("End of stream recieved")
+ self.stream_end_event.set()
+ return False
+ elif depth == 1:
+ # We only raise events for stanzas that are direct
+ # children of the root element.
+ try:
+ self.__spawn_event(xml)
+ except RestartStream:
+ return True
+ if root is not None:
+ # Keep the root element empty of children to
+ # save on memory use.
+ root.clear()
+ log.debug("Ending read XML loop")
+
+ def _build_stanza(self, xml, default_ns=None):
+ """Create a stanza object from a given XML object.
+
+ If a specialized stanza type is not found for the XML, then
+ a generic :class:`~sleekxmpp.xmlstream.stanzabase.StanzaBase`
+ stanza will be returned.
+
+ :param xml: The :class:`~xml.etree.ElementTree.Element` XML object
+ to convert into a stanza object.
+ :param default_ns: Optional default namespace to use instead of the
+ stream's current default namespace.
+ """
+ if default_ns is None:
+ default_ns = self.default_ns
+ stanza_type = StanzaBase
+ for stanza_class in self.__root_stanza:
+ if xml.tag == "{%s}%s" % (default_ns, stanza_class.name) or \
+ xml.tag == stanza_class.tag_name():
+ stanza_type = stanza_class
+ break
+ stanza = stanza_type(self, xml)
+ return stanza
+
+ def __spawn_event(self, xml):
+ """
+ Analyze incoming XML stanzas and convert them into stanza
+ objects if applicable and queue stream events to be processed
+ by matching handlers.
+
+ :param xml: The :class:`~sleekxmpp.xmlstream.stanzabase.ElementBase`
+ stanza to analyze.
+ """
+ # Apply any preprocessing filters.
+ xml = self.incoming_filter(xml)
+
+ # Convert the raw XML object into a stanza object. If no registered
+ # stanza type applies, a generic StanzaBase stanza will be used.
+ stanza = self._build_stanza(xml)
+
+ for filter in self.__filters['in']:
+ if stanza is not None:
+ stanza = filter(stanza)
+ if stanza is None:
+ return
+
+ log.debug("RECV: %s", stanza)
+
+ # Match the stanza against registered handlers. Handlers marked
+ # to run "in stream" will be executed immediately; the rest will
+ # be queued.
+ unhandled = True
+ matched_handlers = [h for h in self.__handlers if h.match(stanza)]
+ for handler in matched_handlers:
+ if len(matched_handlers) > 1:
+ stanza_copy = copy.copy(stanza)
+ else:
+ stanza_copy = stanza
+ handler.prerun(stanza_copy)
+ self.event_queue.put(('stanza', handler, stanza_copy))
+ try:
+ if handler.check_delete():
+ self.__handlers.remove(handler)
+ except:
+ pass # not thread safe
+ unhandled = False
+
+ # Some stanzas require responses, such as Iq queries. A default
+ # handler will be executed immediately for this case.
+ if unhandled:
+ stanza.unhandled()
+
+ def _threaded_event_wrapper(self, func, args):
+ """Capture exceptions for event handlers that run
+ in individual threads.
+
+ :param func: The event handler to execute.
+ :param args: Arguments to the event handler.
+ """
+ # this is always already copied before this is invoked
+ orig = args[0]
+ try:
+ func(*args)
+ except Exception as e:
+ error_msg = 'Error processing event handler: %s'
+ log.exception(error_msg, str(func))
+ if hasattr(orig, 'exception'):
+ orig.exception(e)
+ else:
+ self.exception(e)
+
+ def _event_runner(self):
+ """Process the event queue and execute handlers.
+
+ The number of event runner threads is controlled by HANDLER_THREADS.
+
+ Stream event handlers will all execute in this thread. Custom event
+ handlers may be spawned in individual threads.
+ """
+ log.debug("Loading event runner")
+ try:
+ while not self.stop.is_set():
+ try:
+ wait = self.wait_timeout
+ event = self.event_queue.get(True, timeout=wait)
+ except queue.Empty:
+ event = None
+ if event is None:
+ continue
+
+ etype, handler = event[0:2]
+ args = event[2:]
+ orig = copy.copy(args[0])
+
+ if etype == 'stanza':
+ try:
+ handler.run(args[0])
+ except Exception as e:
+ error_msg = 'Error processing stream handler: %s'
+ log.exception(error_msg, handler.name)
+ orig.exception(e)
+ elif etype == 'schedule':
+ name = args[1]
+ try:
+ log.debug('Scheduled event: %s: %s', name, args[0])
+ handler(*args[0])
+ except Exception as e:
+ log.exception('Error processing scheduled task')
+ self.exception(e)
+ elif etype == 'event':
+ func, threaded, disposable = handler
+ try:
+ if threaded:
+ x = threading.Thread(
+ name="Event_%s" % str(func),
+ target=self._threaded_event_wrapper,
+ args=(func, args))
+ x.start()
+ else:
+ func(*args)
+ except Exception as e:
+ error_msg = 'Error processing event handler: %s'
+ log.exception(error_msg, str(func))
+ if hasattr(orig, 'exception'):
+ orig.exception(e)
+ else:
+ self.exception(e)
+ elif etype == 'quit':
+ log.debug("Quitting event runner thread")
+ return False
+ except KeyboardInterrupt:
+ log.debug("Keyboard Escape Detected in _event_runner")
+ self.event('killed', direct=True)
+ self.disconnect()
+ return
+ except SystemExit:
+ self.disconnect()
+ self.event_queue.put(('quit', None, None))
+ return
+
+ def _send_thread(self):
+ """Extract stanzas from the send queue and send them on the stream."""
+ try:
+ while not self.stop.is_set():
+ while not self.stop.is_set and \
+ not self.session_started_event.is_set():
+ self.session_started_event.wait(timeout=1)
+ if self.__failed_send_stanza is not None:
+ data = self.__failed_send_stanza
+ self.__failed_send_stanza = None
+ else:
+ try:
+ data = self.send_queue.get(True, 1)
+ except queue.Empty:
+ continue
+ log.debug("SEND: %s", data)
+ enc_data = data.encode('utf-8')
+ total = len(enc_data)
+ sent = 0
+ count = 0
+ tries = 0
+ try:
+ while sent < total and not self.stop.is_set():
+ try:
+ sent += self.socket.send(enc_data[sent:])
+ count += 1
+ except ssl.SSLError as serr:
+ if tries >= self.ssl_retry_max:
+ log.debug('SSL error - max retries reached')
+ self.exception(serr)
+ log.warning("Failed to send %s", data)
+ if reconnect is None:
+ reconnect = self.auto_reconnect
+ self.disconnect(reconnect)
+ log.warning('SSL write error - reattempting')
+ time.sleep(self.ssl_retry_delay)
+ tries += 1
+ if count > 1:
+ log.debug('SENT: %d chunks', count)
+ self.send_queue.task_done()
+ except Socket.error as serr:
+ self.event('socket_error', serr)
+ log.warning("Failed to send %s", data)
+ self.__failed_send_stanza = data
+ self.disconnect(self.auto_reconnect)
+ except Exception as ex:
+ log.exception('Unexpected error in send thread: %s', ex)
+ self.exception(ex)
+ if not self.stop.is_set():
+ self.disconnect(self.auto_reconnect)
+
+ def exception(self, exception):
+ """Process an unknown exception.
+
+ Meant to be overridden.
+
+ :param exception: An unhandled exception object.
+ """
+ pass
+
+
+# To comply with PEP8, method names now use underscores.
+# Deprecated method names are re-mapped for backwards compatibility.
+XMLStream.startTLS = XMLStream.start_tls
+XMLStream.registerStanza = XMLStream.register_stanza
+XMLStream.removeStanza = XMLStream.remove_stanza
+XMLStream.registerHandler = XMLStream.register_handler
+XMLStream.removeHandler = XMLStream.remove_handler
+XMLStream.setSocket = XMLStream.set_socket
+XMLStream.sendRaw = XMLStream.send_raw
+XMLStream.getId = XMLStream.get_id
+XMLStream.getNewId = XMLStream.new_id
+XMLStream.sendXML = XMLStream.send_xml
diff --git a/testall.py b/testall.py
new file mode 100755
index 00000000..c9ad5448
--- /dev/null
+++ b/testall.py
@@ -0,0 +1,63 @@
+#!/usr/bin/env python
+
+import os
+import sys
+import logging
+import unittest
+import distutils.core
+
+from glob import glob
+from os.path import splitext, basename, join as pjoin
+
+
+def run_tests():
+ """
+ Find and run all tests in the tests/ directory.
+
+ Excludes live tests (tests/live_*).
+ """
+ testfiles = ['tests.test_overall']
+ exclude = ['__init__.py', 'test_overall.py']
+ for t in glob(pjoin('tests', '*.py')):
+ if True not in [t.endswith(ex) for ex in exclude]:
+ if basename(t).startswith('test_'):
+ testfiles.append('tests.%s' % splitext(basename(t))[0])
+
+ suites = []
+ for file in testfiles:
+ __import__(file)
+ suites.append(sys.modules[file].suite)
+
+ tests = unittest.TestSuite(suites)
+ runner = unittest.TextTestRunner(verbosity=2)
+
+ # Disable logging output
+ logging.basicConfig(level=100)
+ logging.disable(100)
+
+ result = runner.run(tests)
+ return result
+
+
+# Add a 'test' command for setup.py
+
+class TestCommand(distutils.core.Command):
+
+ user_options = [ ]
+
+ def initialize_options(self):
+ self._dir = os.getcwd()
+
+ def finalize_options(self):
+ pass
+
+ def run(self):
+ run_tests()
+
+
+if __name__ == '__main__':
+ result = run_tests()
+ print("<tests %s ran='%s' errors='%s' fails='%s' success='%s' />" % (
+ "xmlns='http//andyet.net/protocol/tests'",
+ result.testsRun, len(result.errors),
+ len(result.failures), result.wasSuccessful()))
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/tests/__init__.py
diff --git a/tests/live_multiple_streams.py b/tests/live_multiple_streams.py
new file mode 100644
index 00000000..69ee74c4
--- /dev/null
+++ b/tests/live_multiple_streams.py
@@ -0,0 +1,57 @@
+import logging
+
+from sleekxmpp.test import *
+
+
+class TestMultipleStreams(SleekTest):
+ """
+ Test that we can test a live stanza stream.
+ """
+
+ def setUp(self):
+ self.client1 = SleekTest()
+ self.client2 = SleekTest()
+
+ def tearDown(self):
+ self.client1.stream_close()
+ self.client2.stream_close()
+
+ def testMultipleStreams(self):
+ """Test that we can interact with multiple live ClientXMPP instance."""
+
+ client1 = self.client1
+ client2 = self.client2
+
+ client1.stream_start(mode='client',
+ socket='live',
+ skip=True,
+ jid='user@localhost/test1',
+ password='user')
+ client2.stream_start(mode='client',
+ socket='live',
+ skip=True,
+ jid='user@localhost/test2',
+ password='user')
+
+ client1.xmpp.send_message(mto='user@localhost/test2',
+ mbody='test')
+
+ client1.send('message@body=test', method='stanzapath')
+ client2.recv('message@body=test', method='stanzapath')
+
+
+suite = unittest.TestLoader().loadTestsFromTestCase(TestMultipleStreams)
+
+if __name__ == '__main__':
+ logging.basicConfig(level=logging.DEBUG,
+ format='%(levelname)-8s %(message)s')
+
+ tests = unittest.TestSuite([suite])
+ result = unittest.TextTestRunner(verbosity=2).run(tests)
+ test_ns = 'http://andyet.net/protocol/tests'
+ print("<tests xmlns='%s' %s %s %s %s />" % (
+ test_ns,
+ 'ran="%s"' % result.testsRun,
+ 'errors="%s"' % len(result.errors),
+ 'fails="%s"' % len(result.failures),
+ 'success="%s"' % result.wasSuccessful()))
diff --git a/tests/live_test.py b/tests/live_test.py
new file mode 100644
index 00000000..b71930af
--- /dev/null
+++ b/tests/live_test.py
@@ -0,0 +1,100 @@
+import logging
+
+from sleekxmpp.test import *
+
+
+class TestLiveStream(SleekTest):
+ """
+ Test that we can test a live stanza stream.
+ """
+
+ def tearDown(self):
+ self.stream_close()
+
+ def testClientConnection(self):
+ """Test that we can interact with a live ClientXMPP instance."""
+ self.stream_start(mode='client',
+ socket='live',
+ skip=False,
+ jid='user@localhost/test',
+ password='user')
+
+ # Use sid=None to ignore any id sent by the server since
+ # we can't know it in advance.
+ self.recv_header(sfrom='localhost', sid=None)
+ self.send_header(sto='localhost')
+ self.recv_feature("""
+ <stream:features>
+ <starttls xmlns="urn:ietf:params:xml:ns:xmpp-tls" />
+ <mechanisms xmlns="urn:ietf:params:xml:ns:xmpp-sasl">
+ <mechanism>DIGEST-MD5</mechanism>
+ <mechanism>PLAIN</mechanism>
+ </mechanisms>
+ </stream:features>
+ """)
+ self.send_feature("""
+ <starttls xmlns="urn:ietf:params:xml:ns:xmpp-tls" />
+ """)
+ self.recv_feature("""
+ <proceed xmlns="urn:ietf:params:xml:ns:xmpp-tls" />
+ """)
+ self.send_header(sto='localhost')
+ self.recv_header(sfrom='localhost', sid=None)
+ self.recv_feature("""
+ <stream:features>
+ <mechanisms xmlns="urn:ietf:params:xml:ns:xmpp-sasl">
+ <mechanism>DIGEST-MD5</mechanism>
+ <mechanism>PLAIN</mechanism>
+ </mechanisms>
+ </stream:features>
+ """)
+ self.send_feature("""
+ <auth xmlns="urn:ietf:params:xml:ns:xmpp-sasl"
+ mechanism="PLAIN">AHVzZXIAdXNlcg==</auth>
+ """)
+ self.recv_feature("""
+ <success xmlns="urn:ietf:params:xml:ns:xmpp-sasl" />
+ """)
+ self.send_header(sto='localhost')
+ self.recv_header(sfrom='localhost', sid=None)
+ self.recv_feature("""
+ <stream:features>
+ <bind xmlns="urn:ietf:params:xml:ns:xmpp-bind" />
+ <session xmlns="urn:ietf:params:xml:ns:xmpp-session" />
+ </stream:features>
+ """)
+
+ # Should really use send, but our Iq stanza objects
+ # can't handle bind element payloads yet.
+ self.send_feature("""
+ <iq type="set" id="1">
+ <bind xmlns="urn:ietf:params:xml:ns:xmpp-bind">
+ <resource>test</resource>
+ </bind>
+ </iq>
+ """)
+ self.recv_feature("""
+ <iq type="result" id="1">
+ <bind xmlns="urn:ietf:params:xml:ns:xmpp-bind">
+ <jid>user@localhost/test</jid>
+ </bind>
+ </iq>
+ """)
+ self.stream_close()
+
+
+suite = unittest.TestLoader().loadTestsFromTestCase(TestLiveStream)
+
+if __name__ == '__main__':
+ logging.basicConfig(level=logging.DEBUG,
+ format='%(levelname)-8s %(message)s')
+
+ tests = unittest.TestSuite([suite])
+ result = unittest.TextTestRunner(verbosity=2).run(tests)
+ test_ns = 'http://andyet.net/protocol/tests'
+ print("<tests xmlns='%s' %s %s %s %s />" % (
+ test_ns,
+ 'ran="%s"' % result.testsRun,
+ 'errors="%s"' % len(result.errors),
+ 'fails="%s"' % len(result.failures),
+ 'success="%s"' % result.wasSuccessful()))
diff --git a/tests/test_events.py b/tests/test_events.py
new file mode 100644
index 00000000..fb34be30
--- /dev/null
+++ b/tests/test_events.py
@@ -0,0 +1,95 @@
+import time
+from sleekxmpp.test import *
+
+
+class TestEvents(SleekTest):
+
+ def setUp(self):
+ self.stream_start()
+
+ def tearDown(self):
+ self.stream_close()
+
+ def testEventHappening(self):
+ """Test handler working"""
+ happened = []
+
+ def handletestevent(event):
+ happened.append(True)
+
+ self.xmpp.add_event_handler("test_event", handletestevent)
+ self.xmpp.event("test_event")
+ self.xmpp.event("test_event")
+
+ # Give the event queue time to process.
+ time.sleep(0.1)
+
+ msg = "Event was not triggered the correct number of times: %s"
+ self.failUnless(happened == [True, True], msg)
+
+ def testDelEvent(self):
+ """Test handler working, then deleted and not triggered"""
+ happened = []
+
+ def handletestevent(event):
+ happened.append(True)
+
+ self.xmpp.add_event_handler("test_event", handletestevent)
+ self.xmpp.event("test_event", {})
+
+ self.xmpp.del_event_handler("test_event", handletestevent)
+
+ # Should not trigger because it was deleted
+ self.xmpp.event("test_event", {})
+
+ # Give the event queue time to process.
+ time.sleep(0.1)
+
+ msg = "Event was not triggered the correct number of times: %s"
+ self.failUnless(happened == [True], msg % happened)
+
+ def testAddDelAddEvent(self):
+ """Test adding, then removing, then adding an event handler."""
+ happened = []
+
+ def handletestevent(event):
+ happened.append(True)
+
+ self.xmpp.add_event_handler("test_event", handletestevent)
+ self.xmpp.event("test_event", {})
+
+ self.xmpp.del_event_handler("test_event", handletestevent)
+ # Should not trigger because it was deleted
+ self.xmpp.event("test_event", {})
+
+ self.xmpp.add_event_handler("test_event", handletestevent)
+ self.xmpp.event("test_event", {})
+
+ # Give the event queue time to process.
+ time.sleep(0.1)
+
+ msg = "Event was not triggered the correct number of times: %s"
+ self.failUnless(happened == [True, True], msg % happened)
+
+ def testDisposableEvent(self):
+ """Test disposable handler working, then not being triggered again."""
+ happened = []
+
+ def handletestevent(event):
+ happened.append(True)
+
+ self.xmpp.add_event_handler("test_event", handletestevent,
+ disposable=True)
+ self.xmpp.event("test_event", {})
+
+ # Should not trigger because it was deleted
+ self.xmpp.event("test_event", {})
+
+ # Give the event queue time to process.
+ time.sleep(0.1)
+
+ msg = "Event was not triggered the correct number of times: %s"
+ self.failUnless(happened == [True], msg % happened)
+
+
+suite = unittest.TestLoader().loadTestsFromTestCase(TestEvents)
diff --git a/tests/test_jid.py b/tests/test_jid.py
new file mode 100644
index 00000000..ef1145d3
--- /dev/null
+++ b/tests/test_jid.py
@@ -0,0 +1,141 @@
+from sleekxmpp.test import *
+from sleekxmpp.xmlstream.jid import JID
+
+
+class TestJIDClass(SleekTest):
+
+ """Verify that the JID class can parse and manipulate JIDs."""
+
+ def testJIDFromFull(self):
+ """Test using JID of the form 'user@server/resource/with/slashes'."""
+ self.check_jid(JID('user@someserver/some/resource'),
+ 'user',
+ 'someserver',
+ 'some/resource',
+ 'user@someserver',
+ 'user@someserver/some/resource',
+ 'user@someserver/some/resource')
+
+ def testJIDchange(self):
+ """Test changing JID of the form 'user@server/resource/with/slashes'"""
+ j = JID('user1@someserver1/some1/resource1')
+ j.user = 'user'
+ j.domain = 'someserver'
+ j.resource = 'some/resource'
+ self.check_jid(j,
+ 'user',
+ 'someserver',
+ 'some/resource',
+ 'user@someserver',
+ 'user@someserver/some/resource',
+ 'user@someserver/some/resource')
+
+ def testJIDaliases(self):
+ """Test changing JID using aliases for domain."""
+ j = JID('user@someserver/resource')
+ j.server = 'anotherserver'
+ self.check_jid(j, domain='anotherserver')
+ j.host = 'yetanother'
+ self.check_jid(j, domain='yetanother')
+
+ def testJIDSetFullWithUser(self):
+ """Test setting the full JID with a user portion."""
+ j = JID('user@domain/resource')
+ j.full = 'otheruser@otherdomain/otherresource'
+ self.check_jid(j,
+ 'otheruser',
+ 'otherdomain',
+ 'otherresource',
+ 'otheruser@otherdomain',
+ 'otheruser@otherdomain/otherresource',
+ 'otheruser@otherdomain/otherresource')
+
+ def testJIDFullNoUserWithResource(self):
+ """
+ Test setting the full JID without a user
+ portion and with a resource.
+ """
+ j = JID('user@domain/resource')
+ j.full = 'otherdomain/otherresource'
+ self.check_jid(j,
+ '',
+ 'otherdomain',
+ 'otherresource',
+ 'otherdomain',
+ 'otherdomain/otherresource',
+ 'otherdomain/otherresource')
+
+ def testJIDFullNoUserNoResource(self):
+ """
+ Test setting the full JID without a user
+ portion and without a resource.
+ """
+ j = JID('user@domain/resource')
+ j.full = 'otherdomain'
+ self.check_jid(j,
+ '',
+ 'otherdomain',
+ '',
+ 'otherdomain',
+ 'otherdomain',
+ 'otherdomain')
+
+ def testJIDBareUser(self):
+ """Test setting the bare JID with a user."""
+ j = JID('user@domain/resource')
+ j.bare = 'otheruser@otherdomain'
+ self.check_jid(j,
+ 'otheruser',
+ 'otherdomain',
+ 'resource',
+ 'otheruser@otherdomain',
+ 'otheruser@otherdomain/resource',
+ 'otheruser@otherdomain/resource')
+
+ def testJIDBareNoUser(self):
+ """Test setting the bare JID without a user."""
+ j = JID('user@domain/resource')
+ j.bare = 'otherdomain'
+ self.check_jid(j,
+ '',
+ 'otherdomain',
+ 'resource',
+ 'otherdomain',
+ 'otherdomain/resource',
+ 'otherdomain/resource')
+
+ def testJIDNoResource(self):
+ """Test using JID of the form 'user@domain'."""
+ self.check_jid(JID('user@someserver'),
+ 'user',
+ 'someserver',
+ '',
+ 'user@someserver',
+ 'user@someserver',
+ 'user@someserver')
+
+ def testJIDNoUser(self):
+ """Test JID of the form 'component.domain.tld'."""
+ self.check_jid(JID('component.someserver'),
+ '',
+ 'component.someserver',
+ '',
+ 'component.someserver',
+ 'component.someserver',
+ 'component.someserver')
+
+ def testJIDEquality(self):
+ """Test that JIDs with the same content are equal."""
+ jid1 = JID('user@domain/resource')
+ jid2 = JID('user@domain/resource')
+ self.assertTrue(jid1 == jid2, "Same JIDs are not considered equal")
+ self.assertFalse(jid1 != jid2, "Same JIDs are considered not equal")
+
+ def testJIDInequality(self):
+ jid1 = JID('user@domain/resource')
+ jid2 = JID('otheruser@domain/resource')
+ self.assertFalse(jid1 == jid2, "Same JIDs are not considered equal")
+ self.assertTrue(jid1 != jid2, "Same JIDs are considered not equal")
+
+
+suite = unittest.TestLoader().loadTestsFromTestCase(TestJIDClass)
diff --git a/tests/test_overall.py b/tests/test_overall.py
new file mode 100644
index 00000000..05fdc6d8
--- /dev/null
+++ b/tests/test_overall.py
@@ -0,0 +1,29 @@
+import os
+import re
+import sys
+import unittest
+import tabnanny
+import compileall
+
+class TestOverall(unittest.TestCase):
+
+ """
+ Test overall package health by compiling and checking
+ code style.
+ """
+
+ def testModules(self):
+ """Testing all modules by compiling them"""
+ src = '.%ssleekxmpp' % os.sep
+ if sys.version_info < (3, 0):
+ rx = re.compile('/[.]svn')
+ else:
+ rx = re.compile('/[.]svn|.*26.*')
+ self.failUnless(compileall.compile_dir(src, rx=rx, quiet=True))
+
+ def testTabNanny(self):
+ """Testing that indentation is consistent"""
+ self.failIf(tabnanny.check('..%ssleekxmpp' % os.sep))
+
+
+suite = unittest.TestLoader().loadTestsFromTestCase(TestOverall)
diff --git a/tests/test_stanza_base.py b/tests/test_stanza_base.py
new file mode 100644
index 00000000..9bd326b6
--- /dev/null
+++ b/tests/test_stanza_base.py
@@ -0,0 +1,79 @@
+from sleekxmpp.test import *
+from sleekxmpp.xmlstream.stanzabase import ET, StanzaBase
+
+
+class TestStanzaBase(SleekTest):
+
+ def testTo(self):
+ """Test the 'to' interface of StanzaBase."""
+ stanza = StanzaBase()
+ stanza['to'] = 'user@example.com'
+ self.failUnless(str(stanza['to']) == 'user@example.com',
+ "Setting and retrieving stanza 'to' attribute did not work.")
+
+ def testFrom(self):
+ """Test the 'from' interface of StanzaBase."""
+ stanza = StanzaBase()
+ stanza['from'] = 'user@example.com'
+ self.failUnless(str(stanza['from']) == 'user@example.com',
+ "Setting and retrieving stanza 'from' attribute did not work.")
+
+ def testPayload(self):
+ """Test the 'payload' interface of StanzaBase."""
+ stanza = StanzaBase()
+ self.failUnless(stanza['payload'] == [],
+ "Empty stanza does not have an empty payload.")
+
+ stanza['payload'] = ET.Element("{foo}foo")
+ self.failUnless(len(stanza['payload']) == 1,
+ "Stanza contents and payload do not match.")
+
+ stanza['payload'] = ET.Element('{bar}bar')
+ self.failUnless(len(stanza['payload']) == 2,
+ "Stanza payload was not appended.")
+
+ del stanza['payload']
+ self.failUnless(stanza['payload'] == [],
+ "Stanza payload not cleared after deletion.")
+
+ stanza['payload'] = [ET.Element('{foo}foo'),
+ ET.Element('{bar}bar')]
+ self.failUnless(len(stanza['payload']) == 2,
+ "Adding multiple elements to stanza's payload did not work.")
+
+ def testClear(self):
+ """Test clearing a stanza."""
+ stanza = StanzaBase()
+ stanza['to'] = 'user@example.com'
+ stanza['payload'] = ET.Element("{foo}foo")
+ stanza.clear()
+
+ self.failUnless(stanza['payload'] == [],
+ "Stanza payload was not cleared after calling .clear()")
+ self.failUnless(str(stanza['to']) == "user@example.com",
+ "Stanza attributes were not preserved after calling .clear()")
+
+ def testReply(self):
+ """Test creating a reply stanza."""
+ stanza = StanzaBase()
+ stanza['to'] = "recipient@example.com"
+ stanza['from'] = "sender@example.com"
+ stanza['payload'] = ET.Element("{foo}foo")
+
+ stanza.reply()
+
+ self.failUnless(str(stanza['to'] == "sender@example.com"),
+ "Stanza reply did not change 'to' attribute.")
+ self.failUnless(stanza['payload'] == [],
+ "Stanza reply did not empty stanza payload.")
+
+ def testError(self):
+ """Test marking a stanza as an error."""
+ stanza = StanzaBase()
+ stanza['type'] = 'get'
+ stanza.error()
+ self.failUnless(stanza['type'] == 'error',
+ "Stanza type is not 'error' after calling error()")
+
+
+suite = unittest.TestLoader().loadTestsFromTestCase(TestStanzaBase)
diff --git a/tests/test_stanza_element.py b/tests/test_stanza_element.py
new file mode 100644
index 00000000..f7ec59c0
--- /dev/null
+++ b/tests/test_stanza_element.py
@@ -0,0 +1,746 @@
+from sleekxmpp.test import *
+from sleekxmpp.xmlstream.stanzabase import ElementBase
+
+
+class TestElementBase(SleekTest):
+
+ def testFixNs(self):
+ """Test fixing namespaces in an XPath expression."""
+
+ e = ElementBase()
+ ns = "http://jabber.org/protocol/disco#items"
+ result = e._fix_ns("{%s}foo/bar/{abc}baz/{%s}more" % (ns, ns))
+
+ expected = "/".join(["{%s}foo" % ns,
+ "{%s}bar" % ns,
+ "{abc}baz",
+ "{%s}more" % ns])
+ self.failUnless(expected == result,
+ "Incorrect namespace fixing result: %s" % str(result))
+
+
+ def testExtendedName(self):
+ """Test element names of the form tag1/tag2/tag3."""
+
+ class TestStanza(ElementBase):
+ name = "foo/bar/baz"
+ namespace = "test"
+
+ stanza = TestStanza()
+ self.check(stanza, """
+ <foo xmlns="test">
+ <bar>
+ <baz />
+ </bar>
+ </foo>
+ """)
+
+ def testGetStanzaValues(self):
+ """Test getStanzaValues using plugins and substanzas."""
+
+ class TestStanzaPlugin(ElementBase):
+ name = "foo2"
+ namespace = "foo"
+ interfaces = set(('bar', 'baz'))
+ plugin_attrib = "foo2"
+
+ class TestSubStanza(ElementBase):
+ name = "subfoo"
+ namespace = "foo"
+ interfaces = set(('bar', 'baz'))
+
+ class TestStanza(ElementBase):
+ name = "foo"
+ namespace = "foo"
+ interfaces = set(('bar', 'baz'))
+
+ register_stanza_plugin(TestStanza, TestStanzaPlugin, iterable=True)
+
+ stanza = TestStanza()
+ stanza['bar'] = 'a'
+ stanza['foo2']['baz'] = 'b'
+ substanza = TestSubStanza()
+ substanza['bar'] = 'c'
+ stanza.append(substanza)
+
+ values = stanza.getStanzaValues()
+ expected = {'bar': 'a',
+ 'baz': '',
+ 'foo2': {'bar': '',
+ 'baz': 'b'},
+ 'substanzas': [{'__childtag__': '{foo}foo2',
+ 'bar': '',
+ 'baz': 'b'},
+ {'__childtag__': '{foo}subfoo',
+ 'bar': 'c',
+ 'baz': ''}]}
+ self.failUnless(values == expected,
+ "Unexpected stanza values:\n%s\n%s" % (str(expected), str(values)))
+
+
+ def testSetStanzaValues(self):
+ """Test using setStanzaValues with substanzas and plugins."""
+
+ class TestStanzaPlugin(ElementBase):
+ name = "pluginfoo"
+ namespace = "foo"
+ interfaces = set(('bar', 'baz'))
+ plugin_attrib = "plugin_foo"
+
+ class TestStanzaPlugin2(ElementBase):
+ name = "pluginfoo2"
+ namespace = "foo"
+ interfaces = set(('bar', 'baz'))
+ plugin_attrib = "plugin_foo2"
+
+ class TestSubStanza(ElementBase):
+ name = "subfoo"
+ namespace = "foo"
+ interfaces = set(('bar', 'baz'))
+
+ class TestStanza(ElementBase):
+ name = "foo"
+ namespace = "foo"
+ interfaces = set(('bar', 'baz'))
+
+ register_stanza_plugin(TestStanza, TestSubStanza, iterable=True)
+ register_stanza_plugin(TestStanza, TestStanzaPlugin)
+ register_stanza_plugin(TestStanza, TestStanzaPlugin2)
+
+ stanza = TestStanza()
+ values = {'bar': 'a',
+ 'baz': '',
+ 'plugin_foo': {'bar': '',
+ 'baz': 'b'},
+ 'plugin_foo2': {'bar': 'd',
+ 'baz': 'e'},
+ 'substanzas': [{'__childtag__': '{foo}subfoo',
+ 'bar': 'c',
+ 'baz': ''}]}
+ stanza.values = values
+
+ self.check(stanza, """
+ <foo xmlns="foo" bar="a">
+ <pluginfoo baz="b" />
+ <pluginfoo2 bar="d" baz="e" />
+ <subfoo bar="c" />
+ </foo>
+ """)
+
+ def testGetItem(self):
+ """Test accessing stanza interfaces."""
+
+ class TestStanza(ElementBase):
+ name = "foo"
+ namespace = "foo"
+ interfaces = set(('bar', 'baz', 'qux'))
+ sub_interfaces = set(('baz',))
+
+ def getQux(self):
+ return 'qux'
+
+ class TestStanzaPlugin(ElementBase):
+ name = "foobar"
+ namespace = "foo"
+ plugin_attrib = "foobar"
+ interfaces = set(('fizz',))
+
+ register_stanza_plugin(TestStanza, TestStanza, iterable=True)
+ register_stanza_plugin(TestStanza, TestStanzaPlugin)
+
+ stanza = TestStanza()
+ substanza = TestStanza()
+ stanza.append(substanza)
+ stanza.setStanzaValues({'bar': 'a',
+ 'baz': 'b',
+ 'qux': 42,
+ 'foobar': {'fizz': 'c'}})
+
+ # Test non-plugin interfaces
+ expected = {'substanzas': [substanza],
+ 'bar': 'a',
+ 'baz': 'b',
+ 'qux': 'qux',
+ 'meh': ''}
+ for interface, value in expected.items():
+ result = stanza[interface]
+ self.failUnless(result == value,
+ "Incorrect stanza interface access result: %s" % result)
+
+ # Test plugin interfaces
+ self.failUnless(isinstance(stanza['foobar'], TestStanzaPlugin),
+ "Incorrect plugin object result.")
+ self.failUnless(stanza['foobar']['fizz'] == 'c',
+ "Incorrect plugin subvalue result.")
+
+ def testSetItem(self):
+ """Test assigning to stanza interfaces."""
+
+ class TestStanza(ElementBase):
+ name = "foo"
+ namespace = "foo"
+ interfaces = set(('bar', 'baz', 'qux'))
+ sub_interfaces = set(('baz',))
+
+ def setQux(self, value):
+ pass
+
+ class TestStanzaPlugin(ElementBase):
+ name = "foobar"
+ namespace = "foo"
+ plugin_attrib = "foobar"
+ interfaces = set(('foobar',))
+
+ register_stanza_plugin(TestStanza, TestStanzaPlugin)
+
+ stanza = TestStanza()
+
+ stanza['bar'] = 'attribute!'
+ stanza['baz'] = 'element!'
+ stanza['qux'] = 'overridden'
+ stanza['foobar'] = 'plugin'
+
+ self.check(stanza, """
+ <foo xmlns="foo" bar="attribute!">
+ <baz>element!</baz>
+ <foobar foobar="plugin" />
+ </foo>
+ """)
+
+ def testDelItem(self):
+ """Test deleting stanza interface values."""
+
+ class TestStanza(ElementBase):
+ name = "foo"
+ namespace = "foo"
+ interfaces = set(('bar', 'baz', 'qux'))
+ sub_interfaces = set(('bar',))
+
+ def delQux(self):
+ pass
+
+ class TestStanzaPlugin(ElementBase):
+ name = "foobar"
+ namespace = "foo"
+ plugin_attrib = "foobar"
+ interfaces = set(('foobar',))
+
+ register_stanza_plugin(TestStanza, TestStanzaPlugin)
+
+ stanza = TestStanza()
+ stanza['bar'] = 'a'
+ stanza['baz'] = 'b'
+ stanza['qux'] = 'c'
+ stanza['foobar']['foobar'] = 'd'
+
+ self.check(stanza, """
+ <foo xmlns="foo" baz="b" qux="c">
+ <bar>a</bar>
+ <foobar foobar="d" />
+ </foo>
+ """)
+
+ del stanza['bar']
+ del stanza['baz']
+ del stanza['qux']
+ del stanza['foobar']
+
+ self.check(stanza, """
+ <foo xmlns="foo" qux="c" />
+ """)
+
+ def testModifyingAttributes(self):
+ """Test modifying top level attributes of a stanza's XML object."""
+
+ class TestStanza(ElementBase):
+ name = "foo"
+ namespace = "foo"
+ interfaces = set(('bar', 'baz'))
+
+ stanza = TestStanza()
+
+ self.check(stanza, """
+ <foo xmlns="foo" />
+ """)
+
+ self.failUnless(stanza._get_attr('bar') == '',
+ "Incorrect value returned for an unset XML attribute.")
+
+ stanza._set_attr('bar', 'a')
+ stanza._set_attr('baz', 'b')
+
+ self.check(stanza, """
+ <foo xmlns="foo" bar="a" baz="b" />
+ """)
+
+ self.failUnless(stanza._get_attr('bar') == 'a',
+ "Retrieved XML attribute value is incorrect.")
+
+ stanza._set_attr('bar', None)
+ stanza._del_attr('baz')
+
+ self.check(stanza, """
+ <foo xmlns="foo" />
+ """)
+
+ self.failUnless(stanza._get_attr('bar', 'c') == 'c',
+ "Incorrect default value returned for an unset XML attribute.")
+
+ def testGetSubText(self):
+ """Test retrieving the contents of a sub element."""
+
+ class TestStanza(ElementBase):
+ name = "foo"
+ namespace = "foo"
+ interfaces = set(('bar',))
+
+ def setBar(self, value):
+ wrapper = ET.Element("{foo}wrapper")
+ bar = ET.Element("{foo}bar")
+ bar.text = value
+ wrapper.append(bar)
+ self.xml.append(wrapper)
+
+ def getBar(self):
+ return self._get_sub_text("wrapper/bar", default="not found")
+
+ stanza = TestStanza()
+ self.failUnless(stanza['bar'] == 'not found',
+ "Default _get_sub_text value incorrect.")
+
+ stanza['bar'] = 'found'
+ self.check(stanza, """
+ <foo xmlns="foo">
+ <wrapper>
+ <bar>found</bar>
+ </wrapper>
+ </foo>
+ """)
+ self.failUnless(stanza['bar'] == 'found',
+ "_get_sub_text value incorrect: %s." % stanza['bar'])
+
+ def testSubElement(self):
+ """Test setting the contents of a sub element."""
+
+ class TestStanza(ElementBase):
+ name = "foo"
+ namespace = "foo"
+ interfaces = set(('bar', 'baz'))
+
+ def setBaz(self, value):
+ self._set_sub_text("wrapper/baz", text=value)
+
+ def getBaz(self):
+ return self._get_sub_text("wrapper/baz")
+
+ def setBar(self, value):
+ self._set_sub_text("wrapper/bar", text=value)
+
+ def getBar(self):
+ return self._get_sub_text("wrapper/bar")
+
+ stanza = TestStanza()
+ stanza['bar'] = 'a'
+ stanza['baz'] = 'b'
+ self.check(stanza, """
+ <foo xmlns="foo">
+ <wrapper>
+ <bar>a</bar>
+ <baz>b</baz>
+ </wrapper>
+ </foo>
+ """)
+ stanza._set_sub_text('wrapper/bar', text='', keep=True)
+ self.check(stanza, """
+ <foo xmlns="foo">
+ <wrapper>
+ <bar />
+ <baz>b</baz>
+ </wrapper>
+ </foo>
+ """, use_values=False)
+
+ stanza['bar'] = 'a'
+ stanza._set_sub_text('wrapper/bar', text='')
+ self.check(stanza, """
+ <foo xmlns="foo">
+ <wrapper>
+ <baz>b</baz>
+ </wrapper>
+ </foo>
+ """)
+
+ def testDelSub(self):
+ """Test removing sub elements."""
+
+ class TestStanza(ElementBase):
+ name = "foo"
+ namespace = "foo"
+ interfaces = set(('bar', 'baz'))
+
+ def setBar(self, value):
+ self._set_sub_text("path/to/only/bar", value);
+
+ def getBar(self):
+ return self._get_sub_text("path/to/only/bar")
+
+ def delBar(self):
+ self._del_sub("path/to/only/bar")
+
+ def setBaz(self, value):
+ self._set_sub_text("path/to/just/baz", value);
+
+ def getBaz(self):
+ return self._get_sub_text("path/to/just/baz")
+
+ def delBaz(self):
+ self._del_sub("path/to/just/baz")
+
+ stanza = TestStanza()
+ stanza['bar'] = 'a'
+ stanza['baz'] = 'b'
+
+ self.check(stanza, """
+ <foo xmlns="foo">
+ <path>
+ <to>
+ <only>
+ <bar>a</bar>
+ </only>
+ <just>
+ <baz>b</baz>
+ </just>
+ </to>
+ </path>
+ </foo>
+ """)
+
+ del stanza['bar']
+ del stanza['baz']
+
+ self.check(stanza, """
+ <foo xmlns="foo">
+ <path>
+ <to>
+ <only />
+ <just />
+ </to>
+ </path>
+ </foo>
+ """, use_values=False)
+
+ stanza['bar'] = 'a'
+ stanza['baz'] = 'b'
+
+ stanza._del_sub('path/to/only/bar', all=True)
+
+ self.check(stanza, """
+ <foo xmlns="foo">
+ <path>
+ <to>
+ <just>
+ <baz>b</baz>
+ </just>
+ </to>
+ </path>
+ </foo>
+ """)
+
+ def testMatch(self):
+ """Test matching a stanza against an XPath expression."""
+
+ class TestSubStanza(ElementBase):
+ name = "sub"
+ namespace = "baz"
+ interfaces = set(('attrib',))
+
+ class TestStanza(ElementBase):
+ name = "foo"
+ namespace = "foo"
+ interfaces = set(('bar','baz', 'qux'))
+ sub_interfaces = set(('qux',))
+
+ def setQux(self, value):
+ self._set_sub_text('qux', text=value)
+
+ def getQux(self):
+ return self._get_sub_text('qux')
+
+ class TestStanzaPlugin(ElementBase):
+ name = "plugin"
+ namespace = "http://test/slash/bar"
+ interfaces = set(('attrib',))
+
+ register_stanza_plugin(TestStanza, TestSubStanza, iterable=True)
+ register_stanza_plugin(TestStanza, TestStanzaPlugin)
+
+ stanza = TestStanza()
+ self.failUnless(stanza.match("foo"),
+ "Stanza did not match its own tag name.")
+
+ self.failUnless(stanza.match("{foo}foo"),
+ "Stanza did not match its own namespaced name.")
+
+ stanza['bar'] = 'a'
+ self.failUnless(stanza.match("foo@bar=a"),
+ "Stanza did not match its own name with attribute value check.")
+
+ stanza['baz'] = 'b'
+ self.failUnless(stanza.match("foo@bar=a@baz=b"),
+ "Stanza did not match its own name with multiple attributes.")
+
+ stanza['qux'] = 'c'
+ self.failUnless(stanza.match("foo/qux"),
+ "Stanza did not match with subelements.")
+
+ stanza['qux'] = ''
+ self.failUnless(stanza.match("foo/qux") == False,
+ "Stanza matched missing subinterface element.")
+
+ self.failUnless(stanza.match("foo/bar") == False,
+ "Stanza matched nonexistent element.")
+
+ stanza['plugin']['attrib'] = 'c'
+ self.failUnless(stanza.match("foo/plugin@attrib=c"),
+ "Stanza did not match with plugin and attribute.")
+
+ self.failUnless(stanza.match("foo/{http://test/slash/bar}plugin"),
+ "Stanza did not match with namespaced plugin.")
+
+ substanza = TestSubStanza()
+ substanza['attrib'] = 'd'
+ stanza.append(substanza)
+ self.failUnless(stanza.match("foo/sub@attrib=d"),
+ "Stanza did not match with substanzas and attribute.")
+
+ self.failUnless(stanza.match("foo/{baz}sub"),
+ "Stanza did not match with namespaced substanza.")
+
+ def testComparisons(self):
+ """Test comparing ElementBase objects."""
+
+ class TestStanza(ElementBase):
+ name = "foo"
+ namespace = "foo"
+ interfaces = set(('bar', 'baz'))
+
+ stanza1 = TestStanza()
+ stanza1['bar'] = 'a'
+
+ self.failUnless(stanza1,
+ "Stanza object does not evaluate to True")
+
+ stanza2 = TestStanza()
+ stanza2['baz'] = 'b'
+
+ self.failUnless(stanza1 != stanza2,
+ "Different stanza objects incorrectly compared equal.")
+
+ stanza1['baz'] = 'b'
+ stanza2['bar'] = 'a'
+
+ self.failUnless(stanza1 == stanza2,
+ "Equal stanzas incorrectly compared inequal.")
+
+ def testKeys(self):
+ """Test extracting interface names from a stanza object."""
+
+ class TestStanza(ElementBase):
+ name = "foo"
+ namespace = "foo"
+ interfaces = set(('bar', 'baz'))
+ plugin_attrib = 'qux'
+
+ register_stanza_plugin(TestStanza, TestStanza)
+
+ stanza = TestStanza()
+
+ self.failUnless(set(stanza.keys()) == set(('bar', 'baz')),
+ "Returned set of interface keys does not match expected.")
+
+ stanza.enable('qux')
+
+ self.failUnless(set(stanza.keys()) == set(('bar', 'baz', 'qux')),
+ "Incorrect set of interface and plugin keys.")
+
+ def testGet(self):
+ """Test accessing stanza interfaces using get()."""
+
+ class TestStanza(ElementBase):
+ name = "foo"
+ namespace = "foo"
+ interfaces = set(('bar', 'baz'))
+
+ stanza = TestStanza()
+ stanza['bar'] = 'a'
+
+ self.failUnless(stanza.get('bar') == 'a',
+ "Incorrect value returned by stanza.get")
+
+ self.failUnless(stanza.get('baz', 'b') == 'b',
+ "Incorrect default value returned by stanza.get")
+
+ def testSubStanzas(self):
+ """Test manipulating substanzas of a stanza object."""
+
+ class TestSubStanza(ElementBase):
+ name = "foobar"
+ namespace = "foo"
+ interfaces = set(('qux',))
+
+ class TestStanza(ElementBase):
+ name = "foo"
+ namespace = "foo"
+ interfaces = set(('bar', 'baz'))
+
+ register_stanza_plugin(TestStanza, TestSubStanza, iterable=True)
+
+ stanza = TestStanza()
+ substanza1 = TestSubStanza()
+ substanza2 = TestSubStanza()
+ substanza1['qux'] = 'a'
+ substanza2['qux'] = 'b'
+
+ # Test appending substanzas
+ self.failUnless(len(stanza) == 0,
+ "Incorrect empty stanza size.")
+
+ stanza.append(substanza1)
+ self.check(stanza, """
+ <foo xmlns="foo">
+ <foobar qux="a" />
+ </foo>
+ """, use_values=False)
+ self.failUnless(len(stanza) == 1,
+ "Incorrect stanza size with 1 substanza.")
+
+ stanza.append(substanza2)
+ self.check(stanza, """
+ <foo xmlns="foo">
+ <foobar qux="a" />
+ <foobar qux="b" />
+ </foo>
+ """, use_values=False)
+ self.failUnless(len(stanza) == 2,
+ "Incorrect stanza size with 2 substanzas.")
+
+ # Test popping substanzas
+ stanza.pop(0)
+ self.check(stanza, """
+ <foo xmlns="foo">
+ <foobar qux="b" />
+ </foo>
+ """, use_values=False)
+
+ # Test iterating over substanzas
+ stanza.append(substanza1)
+ results = []
+ for substanza in stanza:
+ results.append(substanza['qux'])
+ self.failUnless(results == ['b', 'a'],
+ "Iteration over substanzas failed: %s." % str(results))
+
+ def testCopy(self):
+ """Test copying stanza objects."""
+
+ class TestStanza(ElementBase):
+ name = "foo"
+ namespace = "foo"
+ interfaces = set(('bar', 'baz'))
+
+ stanza1 = TestStanza()
+ stanza1['bar'] = 'a'
+
+ stanza2 = stanza1.__copy__()
+
+ self.failUnless(stanza1 == stanza2,
+ "Copied stanzas are not equal to each other.")
+
+ stanza1['baz'] = 'b'
+ self.failUnless(stanza1 != stanza2,
+ "Divergent stanza copies incorrectly compared equal.")
+
+ def testExtension(self):
+ """Testing using is_extension."""
+
+ class TestStanza(ElementBase):
+ name = "foo"
+ namespace = "foo"
+ interfaces = set(('bar', 'baz'))
+
+ class TestExtension(ElementBase):
+ name = 'extended'
+ namespace = 'foo'
+ plugin_attrib = name
+ interfaces = set((name,))
+ is_extension = True
+
+ def set_extended(self, value):
+ self.xml.text = value
+
+ def get_extended(self):
+ return self.xml.text
+
+ def del_extended(self):
+ self.parent().xml.remove(self.xml)
+
+ register_stanza_plugin(TestStanza, TestExtension)
+
+ stanza = TestStanza()
+ stanza['extended'] = 'testing'
+
+ self.check(stanza, """
+ <foo xmlns="foo">
+ <extended>testing</extended>
+ </foo>
+ """)
+
+ self.failUnless(stanza['extended'] == 'testing',
+ "Could not retrieve stanza extension value.")
+
+ del stanza['extended']
+ self.check(stanza, """
+ <foo xmlns="foo" />
+ """)
+
+ def testOverrides(self):
+ """Test using interface overrides."""
+
+ class TestStanza(ElementBase):
+ name = "foo"
+ namespace = "foo"
+ interfaces = set(('bar', 'baz'))
+
+ class TestOverride(ElementBase):
+ name = 'overrider'
+ namespace = 'foo'
+ plugin_attrib = name
+ interfaces = set(('bar',))
+ overrides = ['set_bar']
+
+ def setup(self, xml):
+ # Don't create XML for the plugin
+ self.xml = ET.Element('')
+
+ def set_bar(self, value):
+ if not value.startswith('override-'):
+ self.parent()._set_attr('bar', 'override-%s' % value)
+ else:
+ self.parent()._set_attr('bar', value)
+
+ stanza = TestStanza()
+ stanza['bar'] = 'foo'
+ self.check(stanza, """
+ <foo xmlns="foo" bar="foo" />
+ """)
+
+ register_stanza_plugin(TestStanza, TestOverride, overrides=True)
+
+ stanza = TestStanza()
+ stanza['bar'] = 'foo'
+ self.check(stanza, """
+ <foo xmlns="foo" bar="override-foo" />
+ """)
+
+
+suite = unittest.TestLoader().loadTestsFromTestCase(TestElementBase)
diff --git a/tests/test_stanza_error.py b/tests/test_stanza_error.py
new file mode 100644
index 00000000..a41bf4bf
--- /dev/null
+++ b/tests/test_stanza_error.py
@@ -0,0 +1,81 @@
+from sleekxmpp.test import *
+
+
+class TestErrorStanzas(SleekTest):
+
+ def setUp(self):
+ # Ensure that the XEP-0086 plugin has been loaded.
+ self.stream_start()
+ self.stream_close()
+
+ def testSetup(self):
+ """Test setting initial values in error stanza."""
+ msg = self.Message()
+ msg.enable('error')
+ self.check(msg, """
+ <message type="error">
+ <error type="cancel" code="501">
+ <feature-not-implemented xmlns="urn:ietf:params:xml:ns:xmpp-stanzas" />
+ </error>
+ </message>
+ """)
+
+ def testCondition(self):
+ """Test modifying the error condition."""
+ msg = self.Message()
+ msg['error']['condition'] = 'item-not-found'
+
+ self.check(msg, """
+ <message type="error">
+ <error type="cancel" code="404">
+ <item-not-found xmlns="urn:ietf:params:xml:ns:xmpp-stanzas" />
+ </error>
+ </message>
+ """)
+
+ self.failUnless(msg['error']['condition'] == 'item-not-found', "Error condition doesn't match.")
+
+ msg['error']['condition'] = 'resource-constraint'
+
+ self.check(msg, """
+ <message type="error">
+ <error type="wait" code="500">
+ <resource-constraint xmlns="urn:ietf:params:xml:ns:xmpp-stanzas" />
+ </error>
+ </message>
+ """)
+
+ def testDelCondition(self):
+ """Test that deleting error conditions doesn't remove extra elements."""
+ msg = self.Message()
+ msg['error']['text'] = 'Error!'
+ msg['error']['condition'] = 'internal-server-error'
+
+ del msg['error']['condition']
+
+ self.check(msg, """
+ <message type="error">
+ <error type="wait" code="500">
+ <text xmlns="urn:ietf:params:xml:ns:xmpp-stanzas">Error!</text>
+ </error>
+ </message>
+ """, use_values=False)
+
+ def testDelText(self):
+ """Test deleting the text of an error."""
+ msg = self.Message()
+ msg['error']['test'] = 'Error!'
+ msg['error']['condition'] = 'internal-server-error'
+
+ del msg['error']['text']
+
+ self.check(msg, """
+ <message type="error">
+ <error type="wait" code="500">
+ <internal-server-error xmlns="urn:ietf:params:xml:ns:xmpp-stanzas" />
+ </error>
+ </message>
+ """)
+
+
+suite = unittest.TestLoader().loadTestsFromTestCase(TestErrorStanzas)
diff --git a/tests/test_stanza_gmail.py b/tests/test_stanza_gmail.py
new file mode 100644
index 00000000..6190c608
--- /dev/null
+++ b/tests/test_stanza_gmail.py
@@ -0,0 +1,88 @@
+from sleekxmpp.test import *
+import sleekxmpp.plugins.gmail_notify as gmail
+
+
+class TestGmail(SleekTest):
+
+ def setUp(self):
+ register_stanza_plugin(Iq, gmail.GmailQuery)
+ register_stanza_plugin(Iq, gmail.MailBox)
+ register_stanza_plugin(Iq, gmail.NewMail)
+
+ def testCreateQuery(self):
+ """Testing querying Gmail for emails."""
+
+ iq = self.Iq()
+ iq['type'] = 'get'
+ iq['gmail']['search'] = 'is:starred'
+ iq['gmail']['newer-than-time'] = '1140638252542'
+ iq['gmail']['newer-than-tid'] = '11134623426430234'
+
+ self.check(iq, """
+ <iq type="get">
+ <query xmlns="google:mail:notify"
+ newer-than-time="1140638252542"
+ newer-than-tid="11134623426430234"
+ q="is:starred" />
+ </iq>
+ """, use_values=False)
+
+ def testMailBox(self):
+ """Testing reading from Gmail mailbox result"""
+
+ # Use the example from Google's documentation at
+ # http://code.google.com/apis/talk/jep_extensions/gmail.html#notifications
+ xml = ET.fromstring("""
+ <iq type="result">
+ <mailbox xmlns="google:mail:notify"
+ result-time='1118012394209'
+ url='http://mail.google.com/mail'
+ total-matched='95'
+ total-estimate='0'>
+ <mail-thread-info tid='1172320964060972012'
+ participation='1'
+ messages='28'
+ date='1118012394209'
+ url='http://mail.google.com/mail?view=cv'>
+ <senders>
+ <sender name='Me' address='romeo@gmail.com' originator='1' />
+ <sender name='Benvolio' address='benvolio@gmail.com' />
+ <sender name='Mercutio' address='mercutio@gmail.com' unread='1'/>
+ </senders>
+ <labels>act1scene3</labels>
+ <subject>Put thy rapier up.</subject>
+ <snippet>Ay, ay, a scratch, a scratch; marry, 'tis enough.</snippet>
+ </mail-thread-info>
+ </mailbox>
+ </iq>
+ """)
+
+ iq = self.Iq(xml=xml)
+ mailbox = iq['mailbox']
+ self.failUnless(mailbox['result-time'] == '1118012394209', "result-time doesn't match")
+ self.failUnless(mailbox['url'] == 'http://mail.google.com/mail', "url doesn't match")
+ self.failUnless(mailbox['matched'] == '95', "total-matched incorrect")
+ self.failUnless(mailbox['estimate'] == False, "total-estimate incorrect")
+ self.failUnless(len(mailbox['threads']) == 1, "could not extract message threads")
+
+ thread = mailbox['threads'][0]
+ self.failUnless(thread['tid'] == '1172320964060972012', "thread tid doesn't match")
+ self.failUnless(thread['participation'] == '1', "thread participation incorrect")
+ self.failUnless(thread['messages'] == '28', "thread message count incorrect")
+ self.failUnless(thread['date'] == '1118012394209', "thread date doesn't match")
+ self.failUnless(thread['url'] == 'http://mail.google.com/mail?view=cv', "thread url doesn't match")
+ self.failUnless(thread['labels'] == 'act1scene3', "thread labels incorrect")
+ self.failUnless(thread['subject'] == 'Put thy rapier up.', "thread subject doesn't match")
+ self.failUnless(thread['snippet'] == "Ay, ay, a scratch, a scratch; marry, 'tis enough.", "snippet doesn't match")
+ self.failUnless(len(thread['senders']) == 3, "could not extract senders")
+
+ sender1 = thread['senders'][0]
+ self.failUnless(sender1['name'] == 'Me', "sender name doesn't match")
+ self.failUnless(sender1['address'] == 'romeo@gmail.com', "sender address doesn't match")
+ self.failUnless(sender1['originator'] == True, "sender originator incorrect")
+ self.failUnless(sender1['unread'] == False, "sender unread incorrectly True")
+
+ sender2 = thread['senders'][2]
+ self.failUnless(sender2['unread'] == True, "sender unread incorrectly False")
+
+suite = unittest.TestLoader().loadTestsFromTestCase(TestGmail)
diff --git a/tests/test_stanza_iq.py b/tests/test_stanza_iq.py
new file mode 100644
index 00000000..42e4dcde
--- /dev/null
+++ b/tests/test_stanza_iq.py
@@ -0,0 +1,90 @@
+from sleekxmpp.test import *
+from sleekxmpp.xmlstream.stanzabase import ET
+
+
+class TestIqStanzas(SleekTest):
+
+ def tearDown(self):
+ """Shutdown the XML stream after testing."""
+ self.stream_close()
+
+ def testSetup(self):
+ """Test initializing default Iq values."""
+ iq = self.Iq()
+ self.check(iq, """
+ <iq id="0" />
+ """)
+
+ def testPayload(self):
+ """Test setting Iq stanza payload."""
+ iq = self.Iq()
+ iq.setPayload(ET.Element('{test}tester'))
+ self.check(iq, """
+ <iq id="0">
+ <tester xmlns="test" />
+ </iq>
+ """, use_values=False)
+
+
+ def testUnhandled(self):
+ """Test behavior for Iq.unhandled."""
+ self.stream_start()
+ self.recv("""
+ <iq id="test" type="get">
+ <query xmlns="test" />
+ </iq>
+ """)
+
+ iq = self.Iq()
+ iq['id'] = 'test'
+ iq['error']['condition'] = 'feature-not-implemented'
+ iq['error']['text'] = 'No handlers registered for this request.'
+
+ self.send(iq, """
+ <iq id="test" type="error">
+ <error type="cancel">
+ <feature-not-implemented xmlns="urn:ietf:params:xml:ns:xmpp-stanzas" />
+ <text xmlns="urn:ietf:params:xml:ns:xmpp-stanzas">
+ No handlers registered for this request.
+ </text>
+ </error>
+ </iq>
+ """)
+
+ def testQuery(self):
+ """Test modifying query element of Iq stanzas."""
+ iq = self.Iq()
+
+ iq['query'] = 'query_ns'
+ self.check(iq, """
+ <iq id="0">
+ <query xmlns="query_ns" />
+ </iq>
+ """)
+
+ iq['query'] = 'query_ns2'
+ self.check(iq, """
+ <iq id="0">
+ <query xmlns="query_ns2" />
+ </iq>
+ """)
+
+ self.failUnless(iq['query'] == 'query_ns2', "Query namespace doesn't match")
+
+ del iq['query']
+ self.check(iq, """
+ <iq id="0" />
+ """)
+
+ def testReply(self):
+ """Test setting proper result type in Iq replies."""
+ iq = self.Iq()
+ iq['to'] = 'user@localhost'
+ iq['type'] = 'get'
+ iq.reply()
+
+ self.check(iq, """
+ <iq id="0" type="result" />
+ """)
+
+suite = unittest.TestLoader().loadTestsFromTestCase(TestIqStanzas)
diff --git a/tests/test_stanza_message.py b/tests/test_stanza_message.py
new file mode 100644
index 00000000..e55971df
--- /dev/null
+++ b/tests/test_stanza_message.py
@@ -0,0 +1,57 @@
+from sleekxmpp.test import *
+from sleekxmpp.stanza.message import Message
+from sleekxmpp.stanza.htmlim import HTMLIM
+
+
+class TestMessageStanzas(SleekTest):
+
+ def setUp(self):
+ register_stanza_plugin(Message, HTMLIM)
+
+ def testGroupchatReplyRegression(self):
+ "Regression groupchat reply should be to barejid"
+ msg = self.Message()
+ msg['to'] = 'me@myserver.tld'
+ msg['from'] = 'room@someservice.someserver.tld/somenick'
+ msg['type'] = 'groupchat'
+ msg['body'] = "this is a message"
+ msg.reply()
+ self.failUnless(str(msg['to']) == 'room@someservice.someserver.tld')
+
+ def testAttribProperty(self):
+ "Test attrib property returning self"
+ msg = self.Message()
+ msg.attrib.attrib.attrib['to'] = 'usr@server.tld'
+ self.failUnless(str(msg['to']) == 'usr@server.tld')
+
+ def testHTMLPlugin(self):
+ "Test message/html/body stanza"
+ msg = self.Message()
+ msg['to'] = "fritzy@netflint.net/sleekxmpp"
+ msg['body'] = "this is the plaintext message"
+ msg['type'] = 'chat'
+ p = ET.Element('{http://www.w3.org/1999/xhtml}p')
+ p.text = "This is the htmlim message"
+ msg['html']['body'] = p
+ self.check(msg, """
+ <message to="fritzy@netflint.net/sleekxmpp" type="chat">
+ <body>this is the plaintext message</body>
+ <html xmlns="http://jabber.org/protocol/xhtml-im">
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <p>This is the htmlim message</p>
+ </body>
+ </html>
+ </message>""")
+
+ def testNickPlugin(self):
+ "Test message/nick/nick stanza."
+ msg = self.Message()
+ msg['nick']['nick'] = 'A nickname!'
+ self.check(msg, """
+ <message>
+ <nick xmlns="http://jabber.org/protocol/nick">A nickname!</nick>
+ </message>
+ """)
+
+
+suite = unittest.TestLoader().loadTestsFromTestCase(TestMessageStanzas)
diff --git a/tests/test_stanza_presence.py b/tests/test_stanza_presence.py
new file mode 100644
index 00000000..2ec43b65
--- /dev/null
+++ b/tests/test_stanza_presence.py
@@ -0,0 +1,67 @@
+from sleekxmpp.test import *
+from sleekxmpp.stanza.presence import Presence
+
+
+class TestPresenceStanzas(SleekTest):
+
+ def testPresenceShowRegression(self):
+ """Regression check presence['type'] = 'dnd' show value working"""
+ p = self.Presence()
+ p['type'] = 'dnd'
+ self.check(p, "<presence><show>dnd</show></presence>")
+
+ def testPresenceType(self):
+ """Test manipulating presence['type']"""
+ p = self.Presence()
+ p['type'] = 'available'
+ self.check(p, "<presence />")
+ self.failUnless(p['type'] == 'available',
+ "Incorrect presence['type'] for type 'available': %s" % p['type'])
+
+ for showtype in ['away', 'chat', 'dnd', 'xa']:
+ p['type'] = showtype
+ self.check(p, """
+ <presence><show>%s</show></presence>
+ """ % showtype)
+ self.failUnless(p['type'] == showtype,
+ "Incorrect presence['type'] for type '%s'" % showtype)
+
+ p['type'] = None
+ self.check(p, "<presence />")
+
+ def testPresenceUnsolicitedOffline(self):
+ """
+ Unsolicted offline presence does not spawn changed_status
+ or update the roster.
+ """
+ p = self.Presence()
+ p['type'] = 'unavailable'
+ p['from'] = 'bill@chadmore.com/gmail15af'
+
+ c = sleekxmpp.ClientXMPP('crap@wherever', 'password')
+ happened = []
+
+ def handlechangedpresence(event):
+ happened.append(True)
+
+ c.add_event_handler("changed_status", handlechangedpresence)
+ c._handle_presence(p)
+
+ self.failUnless(happened == [],
+ "changed_status event triggered for extra unavailable presence")
+ roster = c.roster['crap@wherever']
+ self.failUnless(roster['bill@chadmore.com'].resources == {},
+ "Roster updated for superfulous unavailable presence")
+
+ def testNickPlugin(self):
+ """Test presence/nick/nick stanza."""
+ p = self.Presence()
+ p['nick']['nick'] = 'A nickname!'
+ self.check(p, """
+ <presence>
+ <nick xmlns="http://jabber.org/protocol/nick">A nickname!</nick>
+ </presence>
+ """)
+
+
+suite = unittest.TestLoader().loadTestsFromTestCase(TestPresenceStanzas)
diff --git a/tests/test_stanza_roster.py b/tests/test_stanza_roster.py
new file mode 100644
index 00000000..8ec2d32b
--- /dev/null
+++ b/tests/test_stanza_roster.py
@@ -0,0 +1,88 @@
+from sleekxmpp.test import *
+from sleekxmpp.stanza.roster import Roster
+
+
+class TestRosterStanzas(SleekTest):
+
+ def testAddItems(self):
+ """Test adding items to a roster stanza."""
+ iq = self.Iq()
+ iq['roster'].setItems({
+ 'user@example.com': {
+ 'name': 'User',
+ 'subscription': 'both',
+ 'groups': ['Friends', 'Coworkers']},
+ 'otheruser@example.com': {
+ 'name': 'Other User',
+ 'subscription': 'both',
+ 'groups': []}})
+ self.check(iq, """
+ <iq>
+ <query xmlns="jabber:iq:roster">
+ <item jid="user@example.com" name="User" subscription="both">
+ <group>Friends</group>
+ <group>Coworkers</group>
+ </item>
+ <item jid="otheruser@example.com" name="Other User"
+ subscription="both" />
+ </query>
+ </iq>
+ """)
+
+ def testGetItems(self):
+ """Test retrieving items from a roster stanza."""
+ xml_string = """
+ <iq>
+ <query xmlns="jabber:iq:roster">
+ <item jid="user@example.com" name="User" subscription="both">
+ <group>Friends</group>
+ <group>Coworkers</group>
+ </item>
+ <item jid="otheruser@example.com" name="Other User"
+ subscription="both" />
+ </query>
+ </iq>
+ """
+ iq = self.Iq(ET.fromstring(xml_string))
+ expected = {
+ 'user@example.com': {
+ 'name': 'User',
+ 'subscription': 'both',
+ 'ask': '',
+ 'approved': '',
+ 'groups': ['Friends', 'Coworkers']},
+ 'otheruser@example.com': {
+ 'name': 'Other User',
+ 'subscription': 'both',
+ 'ask': '',
+ 'approved': '',
+ 'groups': []}}
+ debug = "Roster items don't match after retrieval."
+ debug += "\nReturned: %s" % str(iq['roster']['items'])
+ debug += "\nExpected: %s" % str(expected)
+ self.failUnless(iq['roster']['items'] == expected, debug)
+
+ def testDelItems(self):
+ """Test clearing items from a roster stanza."""
+ xml_string = """
+ <iq>
+ <query xmlns="jabber:iq:roster">
+ <item jid="user@example.com" name="User" subscription="both">
+ <group>Friends</group>
+ <group>Coworkers</group>
+ </item>
+ <item jid="otheruser@example.com" name="Other User"
+ subscription="both" />
+ </query>
+ </iq>
+ """
+ iq = self.Iq(ET.fromstring(xml_string))
+ del iq['roster']['items']
+ self.check(iq, """
+ <iq>
+ <query xmlns="jabber:iq:roster" />
+ </iq>
+ """)
+
+
+suite = unittest.TestLoader().loadTestsFromTestCase(TestRosterStanzas)
diff --git a/tests/test_stanza_xep_0004.py b/tests/test_stanza_xep_0004.py
new file mode 100644
index 00000000..e183e5e9
--- /dev/null
+++ b/tests/test_stanza_xep_0004.py
@@ -0,0 +1,198 @@
+from sleekxmpp.test import *
+from sleekxmpp.thirdparty import OrderedDict
+
+import sleekxmpp.plugins.xep_0004 as xep_0004
+
+
+class TestDataForms(SleekTest):
+
+ def setUp(self):
+ register_stanza_plugin(Message, xep_0004.Form)
+ register_stanza_plugin(xep_0004.Form, xep_0004.FormField)
+ register_stanza_plugin(xep_0004.FormField, xep_0004.FieldOption)
+
+ def testMultipleInstructions(self):
+ """Testing using multiple instructions elements in a data form."""
+ msg = self.Message()
+ msg['form']['instructions'] = "Instructions\nSecond batch"
+
+ self.check(msg, """
+ <message>
+ <x xmlns="jabber:x:data" type="form">
+ <instructions>Instructions</instructions>
+ <instructions>Second batch</instructions>
+ </x>
+ </message>
+ """)
+
+ def testAddField(self):
+ """Testing adding fields to a data form."""
+
+ msg = self.Message()
+ form = msg['form']
+ form.addField(var='f1',
+ ftype='text-single',
+ label='Text',
+ desc='A text field',
+ required=True,
+ value='Some text!')
+
+ self.check(msg, """
+ <message>
+ <x xmlns="jabber:x:data" type="form">
+ <field var="f1" type="text-single" label="Text">
+ <desc>A text field</desc>
+ <required />
+ <value>Some text!</value>
+ </field>
+ </x>
+ </message>
+ """)
+
+ fields = OrderedDict()
+ fields['f1'] = {'type': 'text-single',
+ 'label': 'Username',
+ 'required': True}
+ fields['f2'] = {'type': 'text-private',
+ 'label': 'Password',
+ 'required': True}
+ fields['f3'] = {'type': 'text-multi',
+ 'label': 'Message',
+ 'value': 'Enter message.\nA long one even.'}
+ fields['f4'] = {'type': 'list-single',
+ 'label': 'Message Type',
+ 'options': [{'label': 'Cool!',
+ 'value': 'cool'},
+ {'label': 'Urgh!',
+ 'value': 'urgh'}]}
+ form['fields'] = fields
+
+
+ self.check(msg, """
+ <message>
+ <x xmlns="jabber:x:data" type="form">
+ <field var="f1" type="text-single" label="Username">
+ <required />
+ </field>
+ <field var="f2" type="text-private" label="Password">
+ <required />
+ </field>
+ <field var="f3" type="text-multi" label="Message">
+ <value>Enter message.</value>
+ <value>A long one even.</value>
+ </field>
+ <field var="f4" type="list-single" label="Message Type">
+ <option label="Cool!">
+ <value>cool</value>
+ </option>
+ <option label="Urgh!">
+ <value>urgh</value>
+ </option>
+ </field>
+ </x>
+ </message>
+ """)
+
+ def testSetValues(self):
+ """Testing setting form values"""
+
+ msg = self.Message()
+ form = msg['form']
+ form.add_field(var='foo', ftype='text-single')
+ form.add_field(var='bar', ftype='list-multi')
+
+ form.setValues({'foo': 'Foo!',
+ 'bar': ['a', 'b']})
+
+ self.check(msg, """
+ <message>
+ <x xmlns="jabber:x:data" type="form">
+ <field var="foo" type="text-single">
+ <value>Foo!</value>
+ </field>
+ <field var="bar" type="list-multi">
+ <value>a</value>
+ <value>b</value>
+ </field>
+ </x>
+ </message>""")
+
+ def testSubmitType(self):
+ """Test that setting type to 'submit' clears extra details"""
+ msg = self.Message()
+ form = msg['form']
+
+ fields = OrderedDict()
+ fields['f1'] = {'type': 'text-single',
+ 'label': 'Username',
+ 'required': True}
+ fields['f2'] = {'type': 'text-private',
+ 'label': 'Password',
+ 'required': True}
+ fields['f3'] = {'type': 'text-multi',
+ 'label': 'Message',
+ 'value': 'Enter message.\nA long one even.'}
+ fields['f4'] = {'type': 'list-single',
+ 'label': 'Message Type',
+ 'options': [{'label': 'Cool!',
+ 'value': 'cool'},
+ {'label': 'Urgh!',
+ 'value': 'urgh'}]}
+ form['fields'] = fields
+
+ form['type'] = 'submit'
+ form['values'] = {'f1': 'username',
+ 'f2': 'hunter2',
+ 'f3': 'A long\nmultiline\nmessage',
+ 'f4': 'cool'}
+
+ self.check(form, """
+ <x xmlns="jabber:x:data" type="submit">
+ <field var="f1">
+ <value>username</value>
+ </field>
+ <field var="f2">
+ <value>hunter2</value>
+ </field>
+ <field var="f3">
+ <value>A long</value>
+ <value>multiline</value>
+ <value>message</value>
+ </field>
+ <field var="f4">
+ <value>cool</value>
+ </field>
+ </x>
+ """, use_values=False)
+
+ def testCancelType(self):
+ """Test that setting type to 'cancel' clears all fields"""
+ msg = self.Message()
+ form = msg['form']
+
+ fields = OrderedDict()
+ fields['f1'] = {'type': 'text-single',
+ 'label': 'Username',
+ 'required': True}
+ fields['f2'] = {'type': 'text-private',
+ 'label': 'Password',
+ 'required': True}
+ fields['f3'] = {'type': 'text-multi',
+ 'label': 'Message',
+ 'value': 'Enter message.\nA long one even.'}
+ fields['f4'] = {'type': 'list-single',
+ 'label': 'Message Type',
+ 'options': [{'label': 'Cool!',
+ 'value': 'cool'},
+ {'label': 'Urgh!',
+ 'value': 'urgh'}]}
+ form['fields'] = fields
+
+ form['type'] = 'cancel'
+
+ self.check(form, """
+ <x xmlns="jabber:x:data" type="cancel" />
+ """)
+
+
+suite = unittest.TestLoader().loadTestsFromTestCase(TestDataForms)
diff --git a/tests/test_stanza_xep_0009.py b/tests/test_stanza_xep_0009.py
new file mode 100644
index 00000000..36800335
--- /dev/null
+++ b/tests/test_stanza_xep_0009.py
@@ -0,0 +1,288 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2011 Nathanael C. Fritz, Dann Martens (TOMOTON).
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+import base64
+
+from sleekxmpp.plugins.xep_0009.stanza.RPC import RPCQuery, MethodCall, \
+ MethodResponse
+from sleekxmpp.plugins.xep_0009.binding import py2xml, xml2py, rpcbase64, \
+ rpctime
+from sleekxmpp.stanza.iq import Iq
+from sleekxmpp.test.sleektest import SleekTest
+from sleekxmpp.xmlstream.stanzabase import register_stanza_plugin
+from sleekxmpp.xmlstream.tostring import tostring
+import unittest
+
+
+
+class TestJabberRPC(SleekTest):
+
+ def setUp(self):
+ register_stanza_plugin(Iq, RPCQuery)
+ register_stanza_plugin(RPCQuery, MethodCall)
+ register_stanza_plugin(RPCQuery, MethodResponse)
+
+ def testMethodCall(self):
+ iq = self.Iq()
+ iq['rpc_query']['method_call']['method_name'] = 'system.exit'
+ iq['rpc_query']['method_call']['params'] = py2xml(*())
+ self.check(iq, """
+ <iq>
+ <query xmlns="jabber:iq:rpc">
+ <methodCall>
+ <methodName>system.exit</methodName>
+ <params />
+ </methodCall>
+ </query>
+ </iq>
+ """, use_values=False)
+
+ def testMethodResponse(self):
+ iq = self.Iq()
+ iq['rpc_query']['method_response']['params'] = py2xml(*())
+ self.check(iq, """
+ <iq>
+ <query xmlns="jabber:iq:rpc">
+ <methodResponse>
+ <params />
+ </methodResponse>
+ </query>
+ </iq>
+ """, use_values=False)
+
+ def testConvertNil(self):
+ params = [None]
+ params_xml = py2xml(*params)
+ expected_xml = self.parse_xml("""
+ <params xmlns="jabber:iq:rpc">
+ <param>
+ <value>
+ <nil />
+ </value>
+ </param>
+ </params>
+ """)
+ self.assertTrue(self.compare(expected_xml, params_xml),
+ "Nil to XML conversion\nExpected: %s\nGot: %s" % (
+ tostring(expected_xml), tostring(params_xml)))
+ self.assertEqual(params, xml2py(expected_xml),
+ "XML to nil conversion")
+
+ def testConvertBoolean(self):
+ params = [True, False]
+ params_xml = py2xml(*params)
+ expected_xml = self.parse_xml("""
+ <params xmlns="jabber:iq:rpc">
+ <param>
+ <value>
+ <boolean>1</boolean>
+ </value>
+ </param>
+ <param>
+ <value>
+ <boolean>0</boolean>
+ </value>
+ </param>
+ </params>
+ """)
+ self.assertTrue(self.compare(expected_xml, params_xml),
+ "Boolean to XML conversion\nExpected: %s\nGot: %s" % (
+ tostring(expected_xml), tostring(params_xml)))
+ self.assertEqual(params, xml2py(expected_xml),
+ "XML to boolean conversion")
+
+ def testConvertString(self):
+ params = ["'This' & \"That\""]
+ params_xml = py2xml(*params)
+ expected_xml = self.parse_xml("""
+ <params xmlns="jabber:iq:rpc">
+ <param>
+ <value>
+ <string>&apos;This&apos; &amp; &quot;That&quot;</string>
+ </value>
+ </param>
+ </params>
+ """)
+ self.assertTrue(self.compare(expected_xml, params_xml),
+ "String to XML conversion\nExpected: %s\nGot: %s" % (
+ tostring(expected_xml), tostring(params_xml)))
+ self.assertEqual(params, xml2py(expected_xml),
+ "XML to string conversion")
+
+ def testConvertInteger(self):
+ params = [32767, -32768]
+ params_xml = py2xml(*params)
+ expected_xml = self.parse_xml("""
+ <params xmlns="jabber:iq:rpc">
+ <param>
+ <value>
+ <i4>32767</i4>
+ </value>
+ </param>
+ <param>
+ <value>
+ <i4>-32768</i4>
+ </value>
+ </param>
+ </params>
+ """)
+ alternate_xml = self.parse_xml("""
+ <params xmlns="jabber:iq:rpc">
+ <param>
+ <value>
+ <int>32767</int>
+ </value>
+ </param>
+ <param>
+ <value>
+ <int>-32768</int>
+ </value>
+ </param>
+ </params>
+ """)
+ self.assertTrue(self.compare(expected_xml, params_xml),
+ "Integer to XML conversion\nExpected: %s\nGot: %s" % (
+ tostring(expected_xml), tostring(params_xml)))
+ self.assertEqual(params, xml2py(expected_xml),
+ "XML to boolean conversion")
+ self.assertEqual(params, xml2py(alternate_xml),
+ "Alternate XML to boolean conversion")
+
+
+ def testConvertDouble(self):
+ params = [3.14159265]
+ params_xml = py2xml(*params)
+ expected_xml = self.parse_xml("""
+ <params xmlns="jabber:iq:rpc">
+ <param>
+ <value>
+ <double>3.14159265</double>
+ </value>
+ </param>
+ </params>
+ """)
+ self.assertTrue(self.compare(expected_xml, params_xml),
+ "Double to XML conversion\nExpected: %s\nGot: %s" % (
+ tostring(expected_xml), tostring(params_xml)))
+ self.assertEqual(params, xml2py(expected_xml),
+ "XML to double conversion")
+
+ def testConvertBase64(self):
+ params = [rpcbase64(base64.b64encode(b"Hello, world!"))]
+ params_xml = py2xml(*params)
+ expected_xml = self.parse_xml("""
+ <params xmlns="jabber:iq:rpc">
+ <param>
+ <value>
+ <base64>SGVsbG8sIHdvcmxkIQ==</base64>
+ </value>
+ </param>
+ </params>
+ """)
+ alternate_xml = self.parse_xml("""
+ <params xmlns="jabber:iq:rpc">
+ <param>
+ <value>
+ <Base64>SGVsbG8sIHdvcmxkIQ==</Base64>
+ </value>
+ </param>
+ </params>
+ """)
+ self.assertTrue(self.compare(expected_xml, params_xml),
+ "Base64 to XML conversion\nExpected: %s\nGot: %s" % (
+ tostring(expected_xml), tostring(params_xml)))
+ self.assertEqual(list(map(lambda x: x.decode(), params)),
+ list(map(lambda x: x.decode(), xml2py(expected_xml))),
+ "XML to base64 conversion")
+ self.assertEqual(list(map(lambda x: x.decode(), params)),
+ list(map(lambda x: x.decode(), xml2py(alternate_xml))),
+ "Alternate XML to base64 conversion")
+
+ def testConvertDateTime(self):
+ params = [rpctime("20111220T01:50:00")]
+ params_xml = py2xml(*params)
+ expected_xml = self.parse_xml("""
+ <params xmlns="jabber:iq:rpc">
+ <param>
+ <value>
+ <dateTime.iso8601>20111220T01:50:00</dateTime.iso8601>
+ </value>
+ </param>
+ </params>
+ """)
+ self.assertTrue(self.compare(expected_xml, params_xml),
+ "DateTime to XML conversion\nExpected: %s\nGot: %s" % (
+ tostring(expected_xml), tostring(params_xml)))
+ self.assertEqual(list(map(lambda x: x.iso8601(), params)),
+ list(map(lambda x: x.iso8601(), xml2py(expected_xml))),
+ None)
+
+ def testConvertArray(self):
+ params = [[1,2,3], ('a', 'b', 'c')]
+ params_xml = py2xml(*params)
+ expected_xml = self.parse_xml("""
+ <params xmlns="jabber:iq:rpc">
+ <param>
+ <value>
+ <array>
+ <data>
+ <value><i4>1</i4></value>
+ <value><i4>2</i4></value>
+ <value><i4>3</i4></value>
+ </data>
+ </array>
+ </value>
+ </param>
+ <param>
+ <value>
+ <array>
+ <data>
+ <value><string>a</string></value>
+ <value><string>b</string></value>
+ <value><string>c</string></value>
+ </data>
+ </array>
+ </value>
+ </param>
+ </params>
+ """)
+ self.assertTrue(self.compare(expected_xml, params_xml),
+ "Array to XML conversion\nExpected: %s\nGot: %s" % (
+ tostring(expected_xml), tostring(params_xml)))
+ self.assertEqual(list(map(list, params)), xml2py(expected_xml),
+ "XML to array conversion")
+
+ def testConvertStruct(self):
+ params = [{"foo": "bar", "baz": False}]
+ params_xml = py2xml(*params)
+ expected_xml = self.parse_xml("""
+ <params xmlns="jabber:iq:rpc">
+ <param>
+ <value>
+ <struct>
+ <member>
+ <name>foo</name>
+ <value><string>bar</string></value>
+ </member>
+ <member>
+ <name>baz</name>
+ <value><boolean>0</boolean></value>
+ </member>
+ </struct>
+ </value>
+ </param>
+ </params>
+ """)
+ self.assertTrue(self.compare(expected_xml, params_xml),
+ "Struct to XML conversion\nExpected: %s\nGot: %s" % (
+ tostring(expected_xml), tostring(params_xml)))
+ self.assertEqual(params, xml2py(expected_xml),
+ "XML to struct conversion")
+
+suite = unittest.TestLoader().loadTestsFromTestCase(TestJabberRPC)
+
diff --git a/tests/test_stanza_xep_0030.py b/tests/test_stanza_xep_0030.py
new file mode 100644
index 00000000..2d64988d
--- /dev/null
+++ b/tests/test_stanza_xep_0030.py
@@ -0,0 +1,516 @@
+from sleekxmpp.test import *
+import sleekxmpp.plugins.xep_0030 as xep_0030
+
+
+class TestDisco(SleekTest):
+
+ """
+ Test creating and manipulating the disco#info and
+ disco#items stanzas from the XEP-0030 plugin.
+ """
+
+ def setUp(self):
+ register_stanza_plugin(Iq, xep_0030.DiscoInfo)
+ register_stanza_plugin(Iq, xep_0030.DiscoItems)
+
+ def testCreateInfoQueryNoNode(self):
+ """Testing disco#info query with no node."""
+ iq = self.Iq()
+ iq['disco_info']['node'] = ''
+
+ self.check(iq, """
+ <iq>
+ <query xmlns="http://jabber.org/protocol/disco#info" />
+ </iq>
+ """)
+
+ def testCreateInfoQueryWithNode(self):
+ """Testing disco#info query with a node."""
+ iq = self.Iq()
+ iq['disco_info']['node'] = 'foo'
+
+ self.check(iq, """
+ <iq>
+ <query xmlns="http://jabber.org/protocol/disco#info"
+ node="foo" />
+ </iq>
+ """)
+
+ def testCreateItemsQueryNoNode(self):
+ """Testing disco#items query with no node."""
+ iq = self.Iq()
+ iq['disco_items']['node'] = ''
+
+ self.check(iq, """
+ <iq>
+ <query xmlns="http://jabber.org/protocol/disco#items" />
+ </iq>
+ """)
+
+ def testCreateItemsQueryWithNode(self):
+ """Testing disco#items query with a node."""
+ iq = self.Iq()
+ iq['disco_items']['node'] = 'foo'
+
+ self.check(iq, """
+ <iq>
+ <query xmlns="http://jabber.org/protocol/disco#items"
+ node="foo" />
+ </iq>
+ """)
+
+ def testIdentities(self):
+ """Testing adding identities to disco#info."""
+ iq = self.Iq()
+ iq['disco_info'].add_identity('conference', 'text',
+ name='Chatroom',
+ lang='en')
+
+ self.check(iq, """
+ <iq>
+ <query xmlns="http://jabber.org/protocol/disco#info">
+ <identity category="conference"
+ type="text"
+ name="Chatroom"
+ xml:lang="en" />
+ </query>
+ </iq>
+ """)
+
+ def testDuplicateIdentities(self):
+ """
+ Test adding multiple copies of the same category
+ and type combination. Only the first identity should
+ be kept.
+ """
+ iq = self.Iq()
+ iq['disco_info'].add_identity('conference', 'text',
+ name='Chatroom')
+ iq['disco_info'].add_identity('conference', 'text',
+ name='MUC')
+ self.check(iq, """
+ <iq>
+ <query xmlns="http://jabber.org/protocol/disco#info">
+ <identity category="conference"
+ type="text"
+ name="Chatroom" />
+ </query>
+ </iq>
+ """)
+
+ def testDuplicateIdentitiesWithLangs(self):
+ """
+ Test adding multiple copies of the same category,
+ type, and language combination. Only the first identity
+ should be kept.
+ """
+ iq = self.Iq()
+ iq['disco_info'].add_identity('conference', 'text',
+ name='Chatroom',
+ lang='en')
+ iq['disco_info'].add_identity('conference', 'text',
+ name='MUC',
+ lang='en')
+ self.check(iq, """
+ <iq>
+ <query xmlns="http://jabber.org/protocol/disco#info">
+ <identity category="conference"
+ type="text"
+ name="Chatroom"
+ xml:lang="en" />
+ </query>
+ </iq>
+ """)
+
+ def testRemoveIdentitiesNoLang(self):
+ """Test removing identities from a disco#info stanza."""
+ iq = self.Iq()
+ iq['disco_info'].add_identity('client', 'pc')
+ iq['disco_info'].add_identity('client', 'bot')
+
+ iq['disco_info'].del_identity('client', 'pc')
+
+ self.check(iq, """
+ <iq>
+ <query xmlns="http://jabber.org/protocol/disco#info">
+ <identity category="client" type="bot" />
+ </query>
+ </iq>
+ """)
+
+ def testRemoveIdentitiesWithLang(self):
+ """Test removing identities from a disco#info stanza."""
+ iq = self.Iq()
+ iq['disco_info'].add_identity('client', 'pc')
+ iq['disco_info'].add_identity('client', 'bot')
+ iq['disco_info'].add_identity('client', 'pc', lang='no')
+
+ iq['disco_info'].del_identity('client', 'pc')
+
+ self.check(iq, """
+ <iq>
+ <query xmlns="http://jabber.org/protocol/disco#info">
+ <identity category="client" type="bot" />
+ <identity category="client"
+ type="pc"
+ xml:lang="no" />
+ </query>
+ </iq>
+ """)
+
+ def testRemoveAllIdentitiesNoLang(self):
+ """Test removing all identities from a disco#info stanza."""
+ iq = self.Iq()
+ iq['disco_info'].add_identity('client', 'bot', name='Bot')
+ iq['disco_info'].add_identity('client', 'bot', lang='no')
+ iq['disco_info'].add_identity('client', 'console')
+
+ del iq['disco_info']['identities']
+
+ self.check(iq, """
+ <iq>
+ <query xmlns="http://jabber.org/protocol/disco#info" />
+ </iq>
+ """)
+
+ def testRemoveAllIdentitiesWithLang(self):
+ """Test removing all identities from a disco#info stanza."""
+ iq = self.Iq()
+ iq['disco_info'].add_identity('client', 'bot', name='Bot')
+ iq['disco_info'].add_identity('client', 'bot', lang='no')
+ iq['disco_info'].add_identity('client', 'console')
+
+ iq['disco_info'].del_identities(lang='no')
+
+ self.check(iq, """
+ <iq>
+ <query xmlns="http://jabber.org/protocol/disco#info">
+ <identity category="client" type="bot" name="Bot" />
+ <identity category="client" type="console" />
+ </query>
+ </iq>
+ """)
+
+ def testAddBatchIdentitiesNoLang(self):
+ """Test adding multiple identities at once to a disco#info stanza."""
+ iq = self.Iq()
+ identities = [('client', 'pc', 'no', 'PC Client'),
+ ('client', 'bot', None, 'Bot'),
+ ('client', 'console', None, None)]
+
+ iq['disco_info']['identities'] = identities
+
+ self.check(iq, """
+ <iq>
+ <query xmlns="http://jabber.org/protocol/disco#info">
+ <identity category="client"
+ type="pc"
+ xml:lang="no"
+ name="PC Client" />
+ <identity category="client" type="bot" name="Bot" />
+ <identity category="client" type="console" />
+ </query>
+ </iq>
+ """)
+
+
+ def testAddBatchIdentitiesWithLang(self):
+ """Test selectively replacing identities based on language."""
+ iq = self.Iq()
+ iq['disco_info'].add_identity('client', 'pc', lang='no')
+ iq['disco_info'].add_identity('client', 'pc', lang='en')
+ iq['disco_info'].add_identity('client', 'pc', lang='fr')
+
+ identities = [('client', 'bot', 'fr', 'Bot'),
+ ('client', 'bot', 'en', 'Bot')]
+
+ iq['disco_info'].set_identities(identities, lang='fr')
+
+ self.check(iq, """
+ <iq>
+ <query xmlns="http://jabber.org/protocol/disco#info">
+ <identity category="client" type="pc" xml:lang="no" />
+ <identity category="client" type="pc" xml:lang="en" />
+ <identity category="client"
+ type="bot"
+ xml:lang="fr"
+ name="Bot" />
+ <identity category="client"
+ type="bot"
+ xml:lang="en"
+ name="Bot" />
+ </query>
+ </iq>
+ """)
+
+ def testGetIdentitiesNoLang(self):
+ """Test getting all identities from a disco#info stanza."""
+ iq = self.Iq()
+ iq['disco_info'].add_identity('client', 'pc')
+ iq['disco_info'].add_identity('client', 'pc', lang='no')
+ iq['disco_info'].add_identity('client', 'pc', lang='en')
+ iq['disco_info'].add_identity('client', 'pc', lang='fr')
+
+ expected = set([('client', 'pc', None, None),
+ ('client', 'pc', 'no', None),
+ ('client', 'pc', 'en', None),
+ ('client', 'pc', 'fr', None)])
+ self.failUnless(iq['disco_info']['identities'] == expected,
+ "Identities do not match:\n%s\n%s" % (
+ expected,
+ iq['disco_info']['identities']))
+
+ def testGetIdentitiesWithLang(self):
+ """
+ Test getting all identities of a given
+ lang from a disco#info stanza.
+ """
+ iq = self.Iq()
+ iq['disco_info'].add_identity('client', 'pc')
+ iq['disco_info'].add_identity('client', 'pc', lang='no')
+ iq['disco_info'].add_identity('client', 'pc', lang='en')
+ iq['disco_info'].add_identity('client', 'pc', lang='fr')
+
+ expected = set([('client', 'pc', 'no', None)])
+ result = iq['disco_info'].get_identities(lang='no')
+ self.failUnless(result == expected,
+ "Identities do not match:\n%s\n%s" % (
+ expected, result))
+
+ def testFeatures(self):
+ """Testing adding features to disco#info."""
+ iq = self.Iq()
+ iq['disco_info'].add_feature('foo')
+ iq['disco_info'].add_feature('bar')
+
+ self.check(iq, """
+ <iq>
+ <query xmlns="http://jabber.org/protocol/disco#info">
+ <feature var="foo" />
+ <feature var="bar" />
+ </query>
+ </iq>
+ """)
+
+ def testFeaturesDuplicate(self):
+ """Test adding duplicate features to disco#info."""
+ iq = self.Iq()
+ iq['disco_info'].add_feature('foo')
+ iq['disco_info'].add_feature('bar')
+ iq['disco_info'].add_feature('foo')
+
+ self.check(iq, """
+ <iq>
+ <query xmlns="http://jabber.org/protocol/disco#info">
+ <feature var="foo" />
+ <feature var="bar" />
+ </query>
+ </iq>
+ """)
+
+ def testRemoveFeature(self):
+ """Test removing a feature from disco#info."""
+ iq = self.Iq()
+ iq['disco_info'].add_feature('foo')
+ iq['disco_info'].add_feature('bar')
+ iq['disco_info'].add_feature('baz')
+
+ iq['disco_info'].del_feature('foo')
+
+ self.check(iq, """
+ <iq>
+ <query xmlns="http://jabber.org/protocol/disco#info">
+ <feature var="bar" />
+ <feature var="baz" />
+ </query>
+ </iq>
+ """)
+
+ def testGetFeatures(self):
+ """Test getting all features from a disco#info stanza."""
+ iq = self.Iq()
+ iq['disco_info'].add_feature('foo')
+ iq['disco_info'].add_feature('bar')
+ iq['disco_info'].add_feature('baz')
+
+ expected = set(['foo', 'bar', 'baz'])
+ self.failUnless(iq['disco_info']['features'] == expected,
+ "Features do not match:\n%s\n%s" % (
+ expected,
+ iq['disco_info']['features']))
+
+ def testRemoveAllFeatures(self):
+ """Test removing all features from a disco#info stanza."""
+ iq = self.Iq()
+ iq['disco_info'].add_feature('foo')
+ iq['disco_info'].add_feature('bar')
+ iq['disco_info'].add_feature('baz')
+
+ del iq['disco_info']['features']
+
+ self.check(iq, """
+ <iq>
+ <query xmlns="http://jabber.org/protocol/disco#info" />
+ </iq>
+ """)
+
+ def testAddBatchFeatures(self):
+ """Test adding multiple features at once to a disco#info stanza."""
+ iq = self.Iq()
+ features = ['foo', 'bar', 'baz']
+
+ iq['disco_info']['features'] = features
+
+ self.check(iq, """
+ <iq>
+ <query xmlns="http://jabber.org/protocol/disco#info">
+ <feature var="foo" />
+ <feature var="bar" />
+ <feature var="baz" />
+ </query>
+ </iq>
+ """)
+
+ def testItems(self):
+ """Testing adding features to disco#info."""
+ iq = self.Iq()
+ iq['disco_items'].add_item('user@localhost')
+ iq['disco_items'].add_item('user@localhost', 'foo')
+ iq['disco_items'].add_item('user@localhost', 'bar', name='Testing')
+
+ self.check(iq, """
+ <iq>
+ <query xmlns="http://jabber.org/protocol/disco#items">
+ <item jid="user@localhost" />
+ <item jid="user@localhost"
+ node="foo" />
+ <item jid="user@localhost"
+ node="bar"
+ name="Testing" />
+ </query>
+ </iq>
+ """)
+
+ def testDuplicateItems(self):
+ """Test adding items with the same JID without any nodes."""
+ iq = self.Iq()
+ iq['disco_items'].add_item('user@localhost', name='First')
+ iq['disco_items'].add_item('user@localhost', name='Second')
+
+ self.check(iq, """
+ <iq>
+ <query xmlns="http://jabber.org/protocol/disco#items">
+ <item jid="user@localhost" name="First" />
+ </query>
+ </iq>
+ """)
+
+
+ def testDuplicateItemsWithNodes(self):
+ """Test adding items with the same JID/node combination."""
+ iq = self.Iq()
+ iq['disco_items'].add_item('user@localhost',
+ node='foo',
+ name='First')
+ iq['disco_items'].add_item('user@localhost',
+ node='foo',
+ name='Second')
+
+ self.check(iq, """
+ <iq>
+ <query xmlns="http://jabber.org/protocol/disco#items">
+ <item jid="user@localhost" node="foo" name="First" />
+ </query>
+ </iq>
+ """)
+
+ def testRemoveItemsNoNode(self):
+ """Test removing items without nodes from a disco#items stanza."""
+ iq = self.Iq()
+ iq['disco_items'].add_item('user@localhost')
+ iq['disco_items'].add_item('user@localhost', node='foo')
+ iq['disco_items'].add_item('test@localhost')
+
+ iq['disco_items'].del_item('user@localhost')
+
+ self.check(iq, """
+ <iq>
+ <query xmlns="http://jabber.org/protocol/disco#items">
+ <item jid="user@localhost" node="foo" />
+ <item jid="test@localhost" />
+ </query>
+ </iq>
+ """)
+
+ def testRemoveItemsWithNode(self):
+ """Test removing items with nodes from a disco#items stanza."""
+ iq = self.Iq()
+ iq['disco_items'].add_item('user@localhost')
+ iq['disco_items'].add_item('user@localhost', node='foo')
+ iq['disco_items'].add_item('test@localhost')
+
+ iq['disco_items'].del_item('user@localhost', node='foo')
+
+ self.check(iq, """
+ <iq>
+ <query xmlns="http://jabber.org/protocol/disco#items">
+ <item jid="user@localhost" />
+ <item jid="test@localhost" />
+ </query>
+ </iq>
+ """)
+
+ def testGetItems(self):
+ """Test retrieving items from disco#items stanza."""
+ iq = self.Iq()
+ iq['disco_items'].add_item('user@localhost')
+ iq['disco_items'].add_item('user@localhost', node='foo')
+ iq['disco_items'].add_item('test@localhost',
+ node='bar',
+ name='Tester')
+
+ expected = set([('user@localhost', None, None),
+ ('user@localhost', 'foo', None),
+ ('test@localhost', 'bar', 'Tester')])
+ self.failUnless(iq['disco_items']['items'] == expected,
+ "Items do not match:\n%s\n%s" % (
+ expected,
+ iq['disco_items']['items']))
+
+ def testRemoveAllItems(self):
+ """Test removing all items from a disco#items stanza."""
+ iq = self.Iq()
+ iq['disco_items'].add_item('user@localhost')
+ iq['disco_items'].add_item('user@localhost', node='foo')
+ iq['disco_items'].add_item('test@localhost',
+ node='bar',
+ name='Tester')
+
+ del iq['disco_items']['items']
+
+ self.check(iq, """
+ <iq>
+ <query xmlns="http://jabber.org/protocol/disco#items" />
+ </iq>
+ """)
+
+ def testAddBatchItems(self):
+ """Test adding multiple items to a disco#items stanza."""
+ iq = self.Iq()
+ items = [('user@localhost', 'foo', 'Test'),
+ ('test@localhost', None, None),
+ ('other@localhost', None, 'Other')]
+
+ iq['disco_items']['items'] = items
+
+ self.check(iq, """
+ <iq>
+ <query xmlns="http://jabber.org/protocol/disco#items">
+ <item jid="user@localhost" node="foo" name="Test" />
+ <item jid="test@localhost" />
+ <item jid="other@localhost" name="Other" />
+ </query>
+ </iq>
+ """)
+
+suite = unittest.TestLoader().loadTestsFromTestCase(TestDisco)
diff --git a/tests/test_stanza_xep_0033.py b/tests/test_stanza_xep_0033.py
new file mode 100644
index 00000000..ec9a5309
--- /dev/null
+++ b/tests/test_stanza_xep_0033.py
@@ -0,0 +1,111 @@
+from sleekxmpp.test import *
+import sleekxmpp.plugins.xep_0033 as xep_0033
+
+
+class TestAddresses(SleekTest):
+
+ def setUp(self):
+ register_stanza_plugin(Message, xep_0033.Addresses)
+
+ def testAddAddress(self):
+ """Testing adding extended stanza address."""
+ msg = self.Message()
+ msg['addresses'].addAddress(atype='to', jid='to@header1.org')
+ self.check(msg, """
+ <message>
+ <addresses xmlns="http://jabber.org/protocol/address">
+ <address jid="to@header1.org" type="to" />
+ </addresses>
+ </message>
+ """)
+
+ msg = self.Message()
+ msg['addresses'].addAddress(atype='replyto',
+ jid='replyto@header1.org',
+ desc='Reply address')
+ self.check(msg, """
+ <message>
+ <addresses xmlns="http://jabber.org/protocol/address">
+ <address jid="replyto@header1.org" type="replyto" desc="Reply address" />
+ </addresses>
+ </message>
+ """)
+
+ def testAddAddresses(self):
+ """Testing adding multiple extended stanza addresses."""
+
+ xmlstring = """
+ <message>
+ <addresses xmlns="http://jabber.org/protocol/address">
+ <address jid="replyto@header1.org" type="replyto" desc="Reply address" />
+ <address jid="cc@header2.org" type="cc" />
+ <address jid="bcc@header2.org" type="bcc" />
+ </addresses>
+ </message>
+ """
+
+ msg = self.Message()
+ msg['addresses'].setAddresses([
+ {'type':'replyto',
+ 'jid':'replyto@header1.org',
+ 'desc':'Reply address'},
+ {'type':'cc',
+ 'jid':'cc@header2.org'},
+ {'type':'bcc',
+ 'jid':'bcc@header2.org'}])
+ self.check(msg, xmlstring)
+
+ msg = self.Message()
+ msg['addresses']['replyto'] = [{'jid':'replyto@header1.org',
+ 'desc':'Reply address'}]
+ msg['addresses']['cc'] = [{'jid':'cc@header2.org'}]
+ msg['addresses']['bcc'] = [{'jid':'bcc@header2.org'}]
+ self.check(msg, xmlstring)
+
+ def testAddURI(self):
+ """Testing adding URI attribute to extended stanza address."""
+
+ msg = self.Message()
+ addr = msg['addresses'].addAddress(atype='to',
+ jid='to@header1.org',
+ node='foo')
+ self.check(msg, """
+ <message>
+ <addresses xmlns="http://jabber.org/protocol/address">
+ <address node="foo" jid="to@header1.org" type="to" />
+ </addresses>
+ </message>
+ """)
+
+ addr['uri'] = 'mailto:to@header2.org'
+ self.check(msg, """
+ <message>
+ <addresses xmlns="http://jabber.org/protocol/address">
+ <address type="to" uri="mailto:to@header2.org" />
+ </addresses>
+ </message>
+ """)
+
+ def testDelivered(self):
+ """Testing delivered attribute of extended stanza addresses."""
+
+ xmlstring = """
+ <message>
+ <addresses xmlns="http://jabber.org/protocol/address">
+ <address %s jid="to@header1.org" type="to" />
+ </addresses>
+ </message>
+ """
+
+ msg = self.Message()
+ addr = msg['addresses'].addAddress(jid='to@header1.org', atype='to')
+ self.check(msg, xmlstring % '')
+
+ addr['delivered'] = True
+ self.check(msg, xmlstring % 'delivered="true"')
+
+ addr['delivered'] = False
+ self.check(msg, xmlstring % '')
+
+
+suite = unittest.TestLoader().loadTestsFromTestCase(TestAddresses)
diff --git a/tests/test_stanza_xep_0050.py b/tests/test_stanza_xep_0050.py
new file mode 100644
index 00000000..ae584de4
--- /dev/null
+++ b/tests/test_stanza_xep_0050.py
@@ -0,0 +1,114 @@
+from sleekxmpp import Iq
+from sleekxmpp.test import *
+from sleekxmpp.plugins.xep_0050 import Command
+
+
+class TestAdHocCommandStanzas(SleekTest):
+
+ def setUp(self):
+ register_stanza_plugin(Iq, Command)
+
+ def testAction(self):
+ """Test using the action attribute."""
+ iq = self.Iq()
+ iq['type'] = 'set'
+ iq['command']['node'] = 'foo'
+
+ iq['command']['action'] = 'execute'
+ self.failUnless(iq['command']['action'] == 'execute')
+
+ iq['command']['action'] = 'complete'
+ self.failUnless(iq['command']['action'] == 'complete')
+
+ iq['command']['action'] = 'cancel'
+ self.failUnless(iq['command']['action'] == 'cancel')
+
+ def testSetActions(self):
+ """Test setting next actions in a command stanza."""
+ iq = self.Iq()
+ iq['type'] = 'result'
+ iq['command']['node'] = 'foo'
+ iq['command']['actions'] = ['prev', 'next']
+
+ self.check(iq, """
+ <iq id="0" type="result">
+ <command xmlns="http://jabber.org/protocol/commands"
+ node="foo">
+ <actions>
+ <prev />
+ <next />
+ </actions>
+ </command>
+ </iq>
+ """)
+
+ def testGetActions(self):
+ """Test retrieving next actions from a command stanza."""
+ iq = self.Iq()
+ iq['command']['node'] = 'foo'
+ iq['command']['actions'] = ['prev', 'next']
+
+ results = iq['command']['actions']
+ expected = ['prev', 'next']
+ self.assertEqual(results, expected,
+ "Incorrect next actions: %s" % results)
+
+ def testDelActions(self):
+ """Test removing next actions from a command stanza."""
+ iq = self.Iq()
+ iq['type'] = 'result'
+ iq['command']['node'] = 'foo'
+ iq['command']['actions'] = ['prev', 'next']
+
+ del iq['command']['actions']
+
+ self.check(iq, """
+ <iq id="0" type="result">
+ <command xmlns="http://jabber.org/protocol/commands"
+ node="foo" />
+ </iq>
+ """)
+
+ def testAddNote(self):
+ """Test adding a command note."""
+ iq = self.Iq()
+ iq['type'] = 'result'
+ iq['command']['node'] = 'foo'
+ iq['command'].add_note('Danger!', ntype='warning')
+
+ self.check(iq, """
+ <iq id="0" type="result">
+ <command xmlns="http://jabber.org/protocol/commands"
+ node="foo">
+ <note type="warning">Danger!</note>
+ </command>
+ </iq>
+ """)
+
+ def testNotes(self):
+ """Test using command notes."""
+ iq = self.Iq()
+ iq['type'] = 'result'
+ iq['command']['node'] = 'foo'
+
+ notes = [('info', 'Interesting...'),
+ ('warning', 'Danger!'),
+ ('error', "I can't let you do that")]
+ iq['command']['notes'] = notes
+
+ self.failUnless(iq['command']['notes'] == notes,
+ "Notes don't match: %s %s" % (notes, iq['command']['notes']))
+
+ self.check(iq, """
+ <iq id="0" type="result">
+ <command xmlns="http://jabber.org/protocol/commands"
+ node="foo">
+ <note type="info">Interesting...</note>
+ <note type="warning">Danger!</note>
+ <note type="error">I can't let you do that</note>
+ </command>
+ </iq>
+ """)
+
+
+suite = unittest.TestLoader().loadTestsFromTestCase(TestAdHocCommandStanzas)
diff --git a/tests/test_stanza_xep_0059.py b/tests/test_stanza_xep_0059.py
new file mode 100644
index 00000000..913436a6
--- /dev/null
+++ b/tests/test_stanza_xep_0059.py
@@ -0,0 +1,106 @@
+from sleekxmpp.test import *
+from sleekxmpp.plugins.xep_0059 import Set
+
+
+class TestSetStanzas(SleekTest):
+
+ def testSetFirstIndex(self):
+ s = Set()
+ s['first'] = 'id'
+ s.set_first_index('10')
+ self.check(s, """
+ <set xmlns="http://jabber.org/protocol/rsm">
+ <first index="10">id</first>
+ </set>
+ """)
+
+ def testGetFirstIndex(self):
+ xml_string = """
+ <set xmlns="http://jabber.org/protocol/rsm">
+ <first index="10">id</first>
+ </set>
+ """
+ s = Set(ET.fromstring(xml_string))
+ expected = '10'
+ self.failUnless(s['first_index'] == expected)
+
+ def testDelFirstIndex(self):
+ xml_string = """
+ <set xmlns="http://jabber.org/protocol/rsm">
+ <first index="10">id</first>
+ </set>
+ """
+ s = Set(ET.fromstring(xml_string))
+ del s['first_index']
+ self.check(s, """
+ <set xmlns="http://jabber.org/protocol/rsm">
+ <first>id</first>
+ </set>
+ """)
+
+ def testSetBefore(self):
+ s = Set()
+ s['before'] = True
+ self.check(s, """
+ <set xmlns="http://jabber.org/protocol/rsm">
+ <before />
+ </set>
+ """)
+
+ def testGetBefore(self):
+ xml_string = """
+ <set xmlns="http://jabber.org/protocol/rsm">
+ <before />
+ </set>
+ """
+ s = Set(ET.fromstring(xml_string))
+ expected = True
+ self.failUnless(s['before'] == expected)
+
+ def testGetBefore(self):
+ xml_string = """
+ <set xmlns="http://jabber.org/protocol/rsm">
+ <before />
+ </set>
+ """
+ s = Set(ET.fromstring(xml_string))
+ del s['before']
+ self.check(s, """
+ <set xmlns="http://jabber.org/protocol/rsm">
+ </set>
+ """)
+
+ def testSetBeforeVal(self):
+ s = Set()
+ s['before'] = 'id'
+ self.check(s, """
+ <set xmlns="http://jabber.org/protocol/rsm">
+ <before>id</before>
+ </set>
+ """)
+
+ def testGetBeforeVal(self):
+ xml_string = """
+ <set xmlns="http://jabber.org/protocol/rsm">
+ <before>id</before>
+ </set>
+ """
+ s = Set(ET.fromstring(xml_string))
+ expected = 'id'
+ self.failUnless(s['before'] == expected)
+
+ def testGetBeforeVal(self):
+ xml_string = """
+ <set xmlns="http://jabber.org/protocol/rsm">
+ <before>id</before>
+ </set>
+ """
+ s = Set(ET.fromstring(xml_string))
+ del s['before']
+ self.check(s, """
+ <set xmlns="http://jabber.org/protocol/rsm">
+ </set>
+ """)
+
+
+suite = unittest.TestLoader().loadTestsFromTestCase(TestSetStanzas)
diff --git a/tests/test_stanza_xep_0060.py b/tests/test_stanza_xep_0060.py
new file mode 100644
index 00000000..16a7cb37
--- /dev/null
+++ b/tests/test_stanza_xep_0060.py
@@ -0,0 +1,575 @@
+from sleekxmpp.test import *
+import sleekxmpp.plugins.xep_0004 as xep_0004
+import sleekxmpp.plugins.xep_0060.stanza as pubsub
+
+
+class TestPubsubStanzas(SleekTest):
+
+ def testAffiliations(self):
+ "Testing iq/pubsub/affiliations/affiliation stanzas"
+ iq = self.Iq()
+ aff1 = pubsub.Affiliation()
+ aff1['node'] = 'testnode'
+ aff1['affiliation'] = 'owner'
+ aff2 = pubsub.Affiliation()
+ aff2['node'] = 'testnode2'
+ aff2['affiliation'] = 'publisher'
+ iq['pubsub']['affiliations'].append(aff1)
+ iq['pubsub']['affiliations'].append(aff2)
+ self.check(iq, """
+ <iq id="0">
+ <pubsub xmlns="http://jabber.org/protocol/pubsub">
+ <affiliations>
+ <affiliation node="testnode" affiliation="owner" />
+ <affiliation node="testnode2" affiliation="publisher" />
+ </affiliations>
+ </pubsub>
+ </iq>""")
+
+ def testSubscriptions(self):
+ "Testing iq/pubsub/subscriptions/subscription stanzas"
+ iq = self.Iq()
+ sub1 = pubsub.Subscription()
+ sub1['node'] = 'testnode'
+ sub1['jid'] = 'steve@myserver.tld/someresource'
+ sub2 = pubsub.Subscription()
+ sub2['node'] = 'testnode2'
+ sub2['jid'] = 'boogers@bork.top/bill'
+ sub2['subscription'] = 'subscribed'
+ iq['pubsub']['subscriptions'].append(sub1)
+ iq['pubsub']['subscriptions'].append(sub2)
+ self.check(iq, """
+ <iq id="0">
+ <pubsub xmlns="http://jabber.org/protocol/pubsub">
+ <subscriptions>
+ <subscription node="testnode" jid="steve@myserver.tld/someresource" />
+ <subscription node="testnode2" jid="boogers@bork.top/bill" subscription="subscribed" />
+ </subscriptions>
+ </pubsub>
+ </iq>""")
+
+ def testOptionalSettings(self):
+ "Testing iq/pubsub/subscription/subscribe-options stanzas"
+ iq = self.Iq()
+ iq['pubsub']['subscription']['suboptions']['required'] = True
+ iq['pubsub']['subscription']['node'] = 'testnode alsdkjfas'
+ iq['pubsub']['subscription']['jid'] = "fritzy@netflint.net/sleekxmpp"
+ iq['pubsub']['subscription']['subscription'] = 'unconfigured'
+ self.check(iq, """
+ <iq id="0">
+ <pubsub xmlns="http://jabber.org/protocol/pubsub">
+ <subscription node="testnode alsdkjfas" jid="fritzy@netflint.net/sleekxmpp" subscription="unconfigured">
+ <subscribe-options>
+ <required />
+ </subscribe-options>
+ </subscription>
+ </pubsub>
+ </iq>""")
+
+ def testItems(self):
+ "Testing iq/pubsub/items stanzas"
+ iq = self.Iq()
+ iq['pubsub']['items']['node'] = 'crap'
+ payload = ET.fromstring("""
+ <thinger xmlns="http://andyet.net/protocol/thinger" x="1" y='2'>
+ <child1 />
+ <child2 normandy='cheese' foo='bar' />
+ </thinger>""")
+ payload2 = ET.fromstring("""
+ <thinger2 xmlns="http://andyet.net/protocol/thinger2" x="12" y='22'>
+ <child12 />
+ <child22 normandy='cheese2' foo='bar2' />
+ </thinger2>""")
+ item = pubsub.Item()
+ item['id'] = 'asdf'
+ item['payload'] = payload
+ item2 = pubsub.Item()
+ item2['id'] = 'asdf2'
+ item2['payload'] = payload2
+ iq['pubsub']['items'].append(item)
+ iq['pubsub']['items'].append(item2)
+ self.check(iq, """
+ <iq id="0">
+ <pubsub xmlns="http://jabber.org/protocol/pubsub">
+ <items node="crap">
+ <item id="asdf">
+ <thinger xmlns="http://andyet.net/protocol/thinger" y="2" x="1">
+ <child1 />
+ <child2 foo="bar" normandy="cheese" />
+ </thinger>
+ </item>
+ <item id="asdf2">
+ <thinger2 xmlns="http://andyet.net/protocol/thinger2" y="22" x="12">
+ <child12 />
+ <child22 foo="bar2" normandy="cheese2" />
+ </thinger2>
+ </item>
+ </items>
+ </pubsub>
+ </iq>""")
+
+ def testCreate(self):
+ "Testing iq/pubsub/create&configure stanzas"
+ iq = self.Iq()
+ iq['pubsub']['create']['node'] = 'mynode'
+ iq['pubsub']['configure']['form'].addField('pubsub#title',
+ ftype='text-single',
+ value='This thing is awesome')
+ self.check(iq, """
+ <iq id="0">
+ <pubsub xmlns="http://jabber.org/protocol/pubsub">
+ <create node="mynode" />
+ <configure>
+ <x xmlns="jabber:x:data" type="form">
+ <field var="pubsub#title" type="text-single">
+ <value>This thing is awesome</value>
+ </field>
+ </x>
+ </configure>
+ </pubsub>
+ </iq>""")
+
+ def testState(self):
+ "Testing iq/psstate stanzas"
+ iq = self.Iq()
+ iq['psstate']['node']= 'mynode'
+ iq['psstate']['item']= 'myitem'
+ pl = ET.Element('{http://andyet.net/protocol/pubsubqueue}claimed')
+ iq['psstate']['payload'] = pl
+ self.check(iq, """
+ <iq id="0">
+ <state xmlns="http://jabber.org/protocol/psstate" node="mynode" item="myitem">
+ <claimed xmlns="http://andyet.net/protocol/pubsubqueue" />
+ </state>
+ </iq>""")
+
+ def testDefault(self):
+ "Testing iq/pubsub_owner/default stanzas"
+ iq = self.Iq()
+ iq['pubsub_owner']['default']
+ iq['pubsub_owner']['default']['node'] = 'mynode'
+ iq['pubsub_owner']['default']['form'].addField('pubsub#title',
+ ftype='text-single',
+ value='This thing is awesome')
+ self.check(iq, """
+ <iq id="0">
+ <pubsub xmlns="http://jabber.org/protocol/pubsub#owner">
+ <default node="mynode">
+ <x xmlns="jabber:x:data" type="form">
+ <field var="pubsub#title" type="text-single">
+ <value>This thing is awesome</value>
+ </field>
+ </x>
+ </default>
+ </pubsub>
+ </iq>""", use_values=False)
+
+ def testSubscribe(self):
+ "testing iq/pubsub/subscribe stanzas"
+ iq = self.Iq()
+ iq['pubsub']['subscribe']['options']
+ iq['pubsub']['subscribe']['node'] = 'cheese'
+ iq['pubsub']['subscribe']['jid'] = 'fritzy@netflint.net/sleekxmpp'
+ iq['pubsub']['subscribe']['options']['node'] = 'cheese'
+ iq['pubsub']['subscribe']['options']['jid'] = 'fritzy@netflint.net/sleekxmpp'
+ form = xep_0004.Form()
+ form.addField('pubsub#title', ftype='text-single', value='this thing is awesome')
+ iq['pubsub']['subscribe']['options']['options'] = form
+ self.check(iq, """
+ <iq id="0">
+ <pubsub xmlns="http://jabber.org/protocol/pubsub">
+ <subscribe node="cheese" jid="fritzy@netflint.net/sleekxmpp">
+ <options node="cheese" jid="fritzy@netflint.net/sleekxmpp">
+ <x xmlns="jabber:x:data" type="submit">
+ <field var="pubsub#title">
+ <value>this thing is awesome</value>
+ </field>
+ </x>
+ </options>
+ </subscribe>
+ </pubsub>
+ </iq>""", use_values=False)
+
+ def testPublish(self):
+ "Testing iq/pubsub/publish stanzas"
+ iq = self.Iq()
+ iq['pubsub']['publish']['node'] = 'thingers'
+ payload = ET.fromstring("""
+ <thinger xmlns="http://andyet.net/protocol/thinger" x="1" y='2'>
+ <child1 />
+ <child2 normandy='cheese' foo='bar' />
+ </thinger>""")
+ payload2 = ET.fromstring("""
+ <thinger2 xmlns="http://andyet.net/protocol/thinger2" x="12" y='22'>
+ <child12 />
+ <child22 normandy='cheese2' foo='bar2' />
+ </thinger2>""")
+ item = pubsub.Item()
+ item['id'] = 'asdf'
+ item['payload'] = payload
+ item2 = pubsub.Item()
+ item2['id'] = 'asdf2'
+ item2['payload'] = payload2
+ iq['pubsub']['publish'].append(item)
+ iq['pubsub']['publish'].append(item2)
+ form = xep_0004.Form()
+ form.addField('pubsub#description', ftype='text-single', value='this thing is awesome')
+ iq['pubsub']['publish_options'] = form
+
+ self.check(iq, """
+ <iq id="0">
+ <pubsub xmlns="http://jabber.org/protocol/pubsub">
+ <publish node="thingers">
+ <item id="asdf">
+ <thinger xmlns="http://andyet.net/protocol/thinger" y="2" x="1">
+ <child1 />
+ <child2 foo="bar" normandy="cheese" />
+ </thinger>
+ </item>
+ <item id="asdf2">
+ <thinger2 xmlns="http://andyet.net/protocol/thinger2" y="22" x="12">
+ <child12 />
+ <child22 foo="bar2" normandy="cheese2" />
+ </thinger2>
+ </item>
+ </publish>
+ <publish-options>
+ <x xmlns="jabber:x:data" type="submit">
+ <field var="pubsub#description">
+ <value>this thing is awesome</value>
+ </field>
+ </x>
+ </publish-options>
+ </pubsub>
+ </iq>""")
+
+ def testDelete(self):
+ "Testing iq/pubsub_owner/delete stanzas"
+ iq = self.Iq()
+ iq['pubsub_owner']['delete']['node'] = 'thingers'
+ self.check(iq, """
+ <iq id="0">
+ <pubsub xmlns="http://jabber.org/protocol/pubsub#owner">
+ <delete node="thingers" />
+ </pubsub>
+ </iq>""")
+
+ def testCreateConfigGet(self):
+ """Testing getting config from full create"""
+ iq = self.Iq()
+ iq['to'] = 'pubsub.asdf'
+ iq['from'] = 'fritzy@asdf/87292ede-524d-4117-9076-d934ed3db8e7'
+ iq['type'] = 'set'
+ iq['id'] = 'E'
+
+ pub = iq['pubsub']
+ pub['create']['node'] = 'testnode2'
+ pub['configure']['form']['type'] = 'submit'
+ pub['configure']['form'].setFields([
+ ('FORM_TYPE', {'type': 'hidden',
+ 'value': 'http://jabber.org/protocol/pubsub#node_config'}),
+ ('pubsub#node_type', {'type': 'list-single',
+ 'label': 'Select the node type',
+ 'value': 'leaf'}),
+ ('pubsub#title', {'type': 'text-single',
+ 'label': 'A friendly name for the node'}),
+ ('pubsub#deliver_notifications', {'type': 'boolean',
+ 'label': 'Deliver event notifications',
+ 'value': True}),
+ ('pubsub#deliver_payloads', {'type': 'boolean',
+ 'label': 'Deliver payloads with event notifications',
+ 'value': True}),
+ ('pubsub#notify_config', {'type': 'boolean',
+ 'label': 'Notify subscribers when the node configuration changes'}),
+ ('pubsub#notify_delete', {'type': 'boolean',
+ 'label': 'Notify subscribers when the node is deleted'}),
+ ('pubsub#notify_retract', {'type': 'boolean',
+ 'label': 'Notify subscribers when items are removed from the node',
+ 'value': True}),
+ ('pubsub#notify_sub', {'type': 'boolean',
+ 'label': 'Notify owners about new subscribers and unsubscribes'}),
+ ('pubsub#persist_items', {'type': 'boolean',
+ 'label': 'Persist items in storage'}),
+ ('pubsub#max_items', {'type': 'text-single',
+ 'label': 'Max # of items to persist',
+ 'value': '10'}),
+ ('pubsub#subscribe', {'type': 'boolean',
+ 'label': 'Whether to allow subscriptions',
+ 'value': True}),
+ ('pubsub#access_model', {'type': 'list-single',
+ 'label': 'Specify the subscriber model',
+ 'value': 'open'}),
+ ('pubsub#publish_model', {'type': 'list-single',
+ 'label': 'Specify the publisher model',
+ 'value': 'publishers'}),
+ ('pubsub#send_last_published_item', {'type': 'list-single',
+ 'label': 'Send last published item',
+ 'value': 'never'}),
+ ('pubsub#presence_based_delivery', {'type': 'boolean',
+ 'label': 'Deliver notification only to available users'}),
+ ])
+
+ self.check(iq, """
+ <iq to="pubsub.asdf" type="set" id="E" from="fritzy@asdf/87292ede-524d-4117-9076-d934ed3db8e7">
+ <pubsub xmlns="http://jabber.org/protocol/pubsub">
+ <create node="testnode2" />
+ <configure>
+ <x xmlns="jabber:x:data" type="submit">
+ <field var="FORM_TYPE">
+ <value>http://jabber.org/protocol/pubsub#node_config</value>
+ </field>
+ <field var="pubsub#node_type">
+ <value>leaf</value>
+ </field>
+ <field var="pubsub#title" />
+ <field var="pubsub#deliver_notifications">
+ <value>1</value>
+ </field>
+ <field var="pubsub#deliver_payloads">
+ <value>1</value>
+ </field>
+ <field var="pubsub#notify_config" />
+ <field var="pubsub#notify_delete" />
+ <field var="pubsub#notify_retract">
+ <value>1</value>
+ </field>
+ <field var="pubsub#notify_sub" />
+ <field var="pubsub#persist_items" />
+ <field var="pubsub#max_items">
+ <value>10</value>
+ </field>
+ <field var="pubsub#subscribe">
+ <value>1</value>
+ </field>
+ <field var="pubsub#access_model">
+ <value>open</value>
+ </field>
+ <field var="pubsub#publish_model">
+ <value>publishers</value>
+ </field>
+ <field var="pubsub#send_last_published_item">
+ <value>never</value>
+ </field>
+ <field var="pubsub#presence_based_delivery" />
+ </x>
+ </configure>
+ </pubsub>
+ </iq>""")
+
+ def testItemEvent(self):
+ """Testing message/pubsub_event/items/item"""
+ msg = self.Message()
+ item = pubsub.EventItem()
+ pl = ET.Element('{http://netflint.net/protocol/test}test', {'failed':'3', 'passed':'24'})
+ item['payload'] = pl
+ item['id'] = 'abc123'
+ msg['pubsub_event']['items'].append(item)
+ msg['pubsub_event']['items']['node'] = 'cheese'
+ msg['type'] = 'normal'
+ self.check(msg, """
+ <message type="normal">
+ <event xmlns="http://jabber.org/protocol/pubsub#event">
+ <items node="cheese">
+ <item id="abc123">
+ <test xmlns="http://netflint.net/protocol/test" failed="3" passed="24" />
+ </item>
+ </items>
+ </event>
+ </message>""")
+
+ def testItemsEvent(self):
+ """Testing multiple message/pubsub_event/items/item"""
+ msg = self.Message()
+ item = pubsub.EventItem()
+ item2 = pubsub.EventItem()
+ pl = ET.Element('{http://netflint.net/protocol/test}test', {'failed':'3', 'passed':'24'})
+ pl2 = ET.Element('{http://netflint.net/protocol/test-other}test', {'total':'27', 'failed':'3'})
+ item2['payload'] = pl2
+ item['payload'] = pl
+ item['id'] = 'abc123'
+ item2['id'] = '123abc'
+ msg['pubsub_event']['items'].append(item)
+ msg['pubsub_event']['items'].append(item2)
+ msg['pubsub_event']['items']['node'] = 'cheese'
+ msg['type'] = 'normal'
+ self.check(msg, """
+ <message type="normal">
+ <event xmlns="http://jabber.org/protocol/pubsub#event">
+ <items node="cheese">
+ <item id="abc123">
+ <test xmlns="http://netflint.net/protocol/test" failed="3" passed="24" />
+ </item>
+ <item id="123abc">
+ <test xmlns="http://netflint.net/protocol/test-other" failed="3" total="27" />
+ </item>
+ </items>
+ </event>
+ </message>""")
+
+ def testItemsEvent(self):
+ """Testing message/pubsub_event/items/item & retract mix"""
+ msg = self.Message()
+ item = pubsub.EventItem()
+ item2 = pubsub.EventItem()
+ pl = ET.Element('{http://netflint.net/protocol/test}test', {'failed':'3', 'passed':'24'})
+ pl2 = ET.Element('{http://netflint.net/protocol/test-other}test', {'total':'27', 'failed':'3'})
+ item2['payload'] = pl2
+ retract = pubsub.EventRetract()
+ retract['id'] = 'aabbcc'
+ item['payload'] = pl
+ item['id'] = 'abc123'
+ item2['id'] = '123abc'
+ msg['pubsub_event']['items'].append(item)
+ msg['pubsub_event']['items'].append(retract)
+ msg['pubsub_event']['items'].append(item2)
+ msg['pubsub_event']['items']['node'] = 'cheese'
+ msg['type'] = 'normal'
+ self.check(msg, """
+ <message type="normal">
+ <event xmlns="http://jabber.org/protocol/pubsub#event">
+ <items node="cheese">
+ <item id="abc123">
+ <test xmlns="http://netflint.net/protocol/test" failed="3" passed="24" />
+ </item><retract id="aabbcc" />
+ <item id="123abc">
+ <test xmlns="http://netflint.net/protocol/test-other" failed="3" total="27" />
+ </item>
+ </items>
+ </event>
+ </message>""")
+
+ def testCollectionAssociate(self):
+ """Testing message/pubsub_event/collection/associate"""
+ msg = self.Message()
+ msg['pubsub_event']['collection']['associate']['node'] = 'cheese'
+ msg['pubsub_event']['collection']['node'] = 'cheeseburger'
+ msg['type'] = 'headline'
+ self.check(msg, """
+ <message type="headline">
+ <event xmlns="http://jabber.org/protocol/pubsub#event">
+ <collection node="cheeseburger">
+ <associate node="cheese" />
+ </collection>
+ </event>
+ </message>""")
+
+ def testCollectionDisassociate(self):
+ """Testing message/pubsub_event/collection/disassociate"""
+ msg = self.Message()
+ msg['pubsub_event']['collection']['disassociate']['node'] = 'cheese'
+ msg['pubsub_event']['collection']['node'] = 'cheeseburger'
+ msg['type'] = 'headline'
+ self.check(msg, """
+ <message type="headline">
+ <event xmlns="http://jabber.org/protocol/pubsub#event">
+ <collection node="cheeseburger">
+ <disassociate node="cheese" />
+ </collection>
+ </event>
+ </message>""")
+
+ def testEventConfiguration(self):
+ """Testing message/pubsub_event/configuration/config"""
+ msg = self.Message()
+ msg['pubsub_event']['configuration']['node'] = 'cheese'
+ msg['pubsub_event']['configuration']['form'].addField('pubsub#title',
+ ftype='text-single',
+ value='This thing is awesome')
+ msg['type'] = 'headline'
+ self.check(msg, """
+ <message type="headline">
+ <event xmlns="http://jabber.org/protocol/pubsub#event">
+ <configuration node="cheese">
+ <x xmlns="jabber:x:data" type="form">
+ <field var="pubsub#title" type="text-single">
+ <value>This thing is awesome</value>
+ </field>
+ </x>
+ </configuration>
+ </event>
+ </message>""")
+
+ def testEventPurge(self):
+ """Testing message/pubsub_event/purge"""
+ msg = self.Message()
+ msg['pubsub_event']['purge']['node'] = 'pickles'
+ msg['type'] = 'headline'
+ self.check(msg, """
+ <message type="headline">
+ <event xmlns="http://jabber.org/protocol/pubsub#event">
+ <purge node="pickles" />
+ </event>
+ </message>""")
+
+ def testEventSubscription(self):
+ """Testing message/pubsub_event/subscription"""
+ msg = self.Message()
+ msg['pubsub_event']['subscription']['node'] = 'pickles'
+ msg['pubsub_event']['subscription']['jid'] = 'fritzy@netflint.net/test'
+ msg['pubsub_event']['subscription']['subid'] = 'aabb1122'
+ msg['pubsub_event']['subscription']['subscription'] = 'subscribed'
+ msg['pubsub_event']['subscription']['expiry'] = 'presence'
+ msg['type'] = 'headline'
+ self.check(msg, """
+ <message type="headline">
+ <event xmlns="http://jabber.org/protocol/pubsub#event">
+ <subscription node="pickles" subid="aabb1122" jid="fritzy@netflint.net/test" subscription="subscribed" expiry="presence" />
+ </event>
+ </message>""")
+
+ def testPubsubError(self):
+ """Test getting a pubsub specific condition from an error stanza"""
+ iq = self.Iq()
+ iq['error']['type'] = 'cancel'
+ iq['error']['code'] = '501'
+ iq['error']['condition'] = 'feature-not-implemented'
+ iq['error']['pubsub']['condition'] = 'subid-required'
+ self.check(iq, """
+ <iq type="error">
+ <error type="cancel" code="501">
+ <feature-not-implemented xmlns="urn:ietf:params:xml:ns:xmpp-stanzas" />
+ <subid-required xmlns="http://jabber.org/protocol/pubsub#errors" />
+ </error>
+ </iq>
+ """, use_values=False)
+
+ del iq['error']['pubsub']['condition']
+ self.check(iq, """
+ <iq type="error">
+ <error type="cancel" code="501">
+ <feature-not-implemented xmlns="urn:ietf:params:xml:ns:xmpp-stanzas" />
+ </error>
+ </iq>
+ """, use_values=False)
+
+ def testPubsubUnsupportedError(self):
+ """Test getting the feature from an unsupported error"""
+ iq = self.Iq()
+ iq['error']['type'] = 'cancel'
+ iq['error']['code'] = '501'
+ iq['error']['condition'] = 'feature-not-implemented'
+ iq['error']['pubsub']['condition'] = 'unsupported'
+ iq['error']['pubsub']['unsupported'] = 'instant-node'
+ self.check(iq, """
+ <iq type="error">
+ <error type="cancel" code="501">
+ <feature-not-implemented xmlns="urn:ietf:params:xml:ns:xmpp-stanzas" />
+ <unsupported xmlns="http://jabber.org/protocol/pubsub#errors" feature="instant-node" />
+ </error>
+ </iq>
+ """, use_values=False)
+
+ self.assertEqual(iq['error']['pubsub']['condition'], 'unsupported')
+ self.assertEqual(iq['error']['pubsub']['unsupported'], 'instant-node')
+
+ del iq['error']['pubsub']['unsupported']
+ self.check(iq, """
+ <iq type="error">
+ <error type="cancel" code="501">
+ <feature-not-implemented xmlns="urn:ietf:params:xml:ns:xmpp-stanzas" />
+ </error>
+ </iq>
+ """, use_values=False)
+
+
+suite = unittest.TestLoader().loadTestsFromTestCase(TestPubsubStanzas)
diff --git a/tests/test_stanza_xep_0085.py b/tests/test_stanza_xep_0085.py
new file mode 100644
index 00000000..b08404e2
--- /dev/null
+++ b/tests/test_stanza_xep_0085.py
@@ -0,0 +1,41 @@
+from sleekxmpp.test import *
+import sleekxmpp.plugins.xep_0085 as xep_0085
+
+class TestChatStates(SleekTest):
+
+ def setUp(self):
+ register_stanza_plugin(Message, xep_0085.ChatState)
+
+ def testCreateChatState(self):
+ """Testing creating chat states."""
+
+ xmlstring = """
+ <message>
+ <%s xmlns="http://jabber.org/protocol/chatstates" />
+ </message>
+ """
+
+ msg = self.Message()
+
+ self.assertEqual(msg['chat_state'], '')
+ self.check(msg, "<message />", use_values=False)
+
+ msg['chat_state'] = 'active'
+ self.check(msg, xmlstring % 'active', use_values=False)
+
+ msg['chat_state'] = 'composing'
+ self.check(msg, xmlstring % 'composing', use_values=False)
+
+ msg['chat_state'] = 'gone'
+ self.check(msg, xmlstring % 'gone', use_values=False)
+
+ msg['chat_state'] = 'inactive'
+ self.check(msg, xmlstring % 'inactive', use_values=False)
+
+ msg['chat_state'] = 'paused'
+ self.check(msg, xmlstring % 'paused', use_values=False)
+
+ del msg['chat_state']
+ self.check(msg, "<message />")
+
+suite = unittest.TestLoader().loadTestsFromTestCase(TestChatStates)
diff --git a/tests/test_stream.py b/tests/test_stream.py
new file mode 100644
index 00000000..deac24a5
--- /dev/null
+++ b/tests/test_stream.py
@@ -0,0 +1,79 @@
+import time
+from sleekxmpp.test import *
+
+
+class TestStreamTester(SleekTest):
+ """
+ Test that we can simulate and test a stanza stream.
+ """
+
+ def tearDown(self):
+ self.stream_close()
+
+ def testClientEcho(self):
+ """Test that we can interact with a ClientXMPP instance."""
+ self.stream_start(mode='client')
+
+ def echo(msg):
+ msg.reply('Thanks for sending: %(body)s' % msg).send()
+
+ self.xmpp.add_event_handler('message', echo)
+
+ self.recv("""
+ <message to="tester@localhost" from="user@localhost">
+ <body>Hi!</body>
+ </message>
+ """)
+
+ self.send("""
+ <message to="user@localhost">
+ <body>Thanks for sending: Hi!</body>
+ </message>
+ """)
+
+ def testComponentEcho(self):
+ """Test that we can interact with a ComponentXMPP instance."""
+ self.stream_start(mode='component')
+
+ def echo(msg):
+ msg.reply('Thanks for sending: %(body)s' % msg).send()
+
+ self.xmpp.add_event_handler('message', echo)
+
+ self.recv("""
+ <message to="tester.localhost" from="user@localhost">
+ <body>Hi!</body>
+ </message>
+ """)
+
+ self.send("""
+ <message to="user@localhost" from="tester.localhost">
+ <body>Thanks for sending: Hi!</body>
+ </message>
+ """)
+
+ def testSendStreamHeader(self):
+ """Test that we can check a sent stream header."""
+ self.stream_start(mode='client', skip=False)
+ self.send_header(sto='localhost')
+
+ def testStreamDisconnect(self):
+ """Test that the test socket can simulate disconnections."""
+ self.stream_start()
+ events = set()
+
+ def stream_error(event):
+ events.add('socket_error')
+
+ self.xmpp.add_event_handler('socket_error', stream_error)
+
+ self.stream_disconnect()
+ self.xmpp.send_raw(' ')
+
+ time.sleep(.1)
+
+ self.failUnless('socket_error' in events,
+ "Stream error event not raised: %s" % events)
+
+
+suite = unittest.TestLoader().loadTestsFromTestCase(TestStreamTester)
diff --git a/tests/test_stream_exceptions.py b/tests/test_stream_exceptions.py
new file mode 100644
index 00000000..c41edbb2
--- /dev/null
+++ b/tests/test_stream_exceptions.py
@@ -0,0 +1,274 @@
+import sys
+import sleekxmpp
+from sleekxmpp.xmlstream.matcher import MatchXPath
+from sleekxmpp.xmlstream.handler import Callback
+from sleekxmpp.exceptions import XMPPError
+from sleekxmpp.test import *
+
+
+class TestStreamExceptions(SleekTest):
+ """
+ Test handling roster updates.
+ """
+
+ def tearDown(self):
+ 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."""
+
+ def message(msg):
+ raise XMPPError(condition='feature-not-implemented',
+ text="We don't do things that way here.",
+ etype='cancel',
+ extension='foo',
+ extension_ns='foo:error',
+ extension_args={'test': 'true'})
+
+ 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">
+ <error type="cancel" code="501">
+ <feature-not-implemented
+ xmlns="urn:ietf:params:xml:ns:xmpp-stanzas" />
+ <text xmlns="urn:ietf:params:xml:ns:xmpp-stanzas">
+ We don&apos;t do things that way here.
+ </text>
+ <foo xmlns="foo:error" test="true" />
+ </error>
+ </message>
+ """, use_values=False)
+
+ def testIqErrorException(self):
+ """Test using error exceptions with Iq stanzas."""
+
+ def handle_iq(iq):
+ raise XMPPError(condition='feature-not-implemented',
+ text="We don't do things that way here.",
+ etype='cancel',
+ clear=False)
+
+ self.stream_start()
+ self.xmpp.register_handler(
+ Callback(
+ 'Test Iq',
+ MatchXPath('{%s}iq/{test}query' % self.xmpp.default_ns),
+ handle_iq))
+
+ self.recv("""
+ <iq type="get" id="0">
+ <query xmlns="test" />
+ </iq>
+ """)
+
+ self.send("""
+ <iq type="error" id="0">
+ <query xmlns="test" />
+ <error type="cancel" code="501">
+ <feature-not-implemented
+ xmlns="urn:ietf:params:xml:ns:xmpp-stanzas" />
+ <text xmlns="urn:ietf:params:xml:ns:xmpp-stanzas">
+ We don&apos;t do things that way here.
+ </text>
+ </error>
+ </iq>
+ """, use_values=False)
+
+ def testThreadedXMPPErrorException(self):
+ """Test raising an XMPPError exception in a threaded handler."""
+
+ def message(msg):
+ raise XMPPError(condition='feature-not-implemented',
+ text="We don't do things that way here.",
+ etype='cancel')
+
+ self.stream_start()
+ self.xmpp.add_event_handler('message', message,
+ threaded=True)
+
+ self.recv("""
+ <message>
+ <body>This is going to cause an error.</body>
+ </message>
+ """)
+
+ self.send("""
+ <message type="error">
+ <error type="cancel" code="501">
+ <feature-not-implemented
+ xmlns="urn:ietf:params:xml:ns:xmpp-stanzas" />
+ <text xmlns="urn:ietf:params:xml:ns:xmpp-stanzas">
+ We don&apos;t do things that way here.
+ </text>
+ </error>
+ </message>
+ """)
+
+ def testUnknownException(self):
+ """Test raising an generic exception in a threaded handler."""
+
+ 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.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_filters.py b/tests/test_stream_filters.py
new file mode 100644
index 00000000..ef4d5dc8
--- /dev/null
+++ b/tests/test_stream_filters.py
@@ -0,0 +1,88 @@
+import time
+
+from sleekxmpp import Message
+from sleekxmpp.test import *
+from sleekxmpp.xmlstream.handler import *
+from sleekxmpp.xmlstream.matcher import *
+
+
+class TestFilters(SleekTest):
+
+ """
+ Test using incoming and outgoing filters.
+ """
+
+ def setUp(self):
+ self.stream_start()
+
+ def tearDown(self):
+ self.stream_close()
+
+ def testIncoming(self):
+
+ data = []
+
+ def in_filter(stanza):
+ if isinstance(stanza, Message):
+ if stanza['body'] == 'testing':
+ stanza['subject'] = stanza['body'] + ' filter'
+ print('>>> %s' % stanza['subject'])
+ return stanza
+
+ def on_message(msg):
+ print('<<< %s' % msg['subject'])
+ data.append(msg['subject'])
+
+ self.xmpp.add_filter('in', in_filter)
+ self.xmpp.add_event_handler('message', on_message)
+
+ self.recv("""
+ <message>
+ <body>no filter</body>
+ </message>
+ """)
+
+ self.recv("""
+ <message>
+ <body>testing</body>
+ </message>
+ """)
+
+ time.sleep(0.5)
+
+ self.assertEqual(data, ['', 'testing filter'],
+ 'Incoming filter did not apply %s' % data)
+
+ def testOutgoing(self):
+
+ def out_filter(stanza):
+ if isinstance(stanza, Message):
+ if stanza['body'] == 'testing':
+ stanza['body'] = 'changed!'
+ return stanza
+
+ self.xmpp.add_filter('out', out_filter)
+
+ m1 = self.Message()
+ m1['body'] = 'testing'
+ m1.send()
+
+ m2 = self.Message()
+ m2['body'] = 'blah'
+ m2.send()
+
+ self.send("""
+ <message>
+ <body>changed!</body>
+ </message>
+ """)
+
+ self.send("""
+ <message>
+ <body>blah</body>
+ </message>
+ """)
+
+
+
+suite = unittest.TestLoader().loadTestsFromTestCase(TestFilters)
diff --git a/tests/test_stream_handlers.py b/tests/test_stream_handlers.py
new file mode 100644
index 00000000..7fd4e648
--- /dev/null
+++ b/tests/test_stream_handlers.py
@@ -0,0 +1,201 @@
+import time
+
+from sleekxmpp import Message
+from sleekxmpp.test import *
+from sleekxmpp.xmlstream.handler import *
+from sleekxmpp.xmlstream.matcher import *
+
+
+class TestHandlers(SleekTest):
+ """
+ Test using handlers and waiters.
+ """
+
+ def setUp(self):
+ self.stream_start()
+
+ def tearDown(self):
+ self.stream_close()
+
+ def testCallback(self):
+ """Test using stream callback handlers."""
+
+ def callback_handler(stanza):
+ self.xmpp.sendRaw("""
+ <message>
+ <body>Success!</body>
+ </message>
+ """)
+
+ callback = Callback('Test Callback',
+ MatchXPath('{test}tester'),
+ callback_handler)
+
+ self.xmpp.registerHandler(callback)
+
+ self.recv("""<tester xmlns="test" />""")
+
+ msg = self.Message()
+ msg['body'] = 'Success!'
+ self.send(msg)
+
+ def testWaiter(self):
+ """Test using stream waiter handler."""
+
+ def waiter_handler(stanza):
+ iq = self.xmpp.Iq()
+ iq['id'] = 'test'
+ iq['type'] = 'set'
+ iq['query'] = 'test'
+ reply = iq.send(block=True)
+ if reply:
+ self.xmpp.sendRaw("""
+ <message>
+ <body>Successful: %s</body>
+ </message>
+ """ % reply['query'])
+
+ self.xmpp.add_event_handler('message', waiter_handler, threaded=True)
+
+ # Send message to trigger waiter_handler
+ self.recv("""
+ <message>
+ <body>Testing</body>
+ </message>
+ """)
+
+ # Check that Iq was sent by waiter_handler
+ iq = self.Iq()
+ iq['id'] = 'test'
+ iq['type'] = 'set'
+ iq['query'] = 'test'
+ self.send(iq)
+
+ # Send the reply Iq
+ self.recv("""
+ <iq id="test" type="result">
+ <query xmlns="test" />
+ </iq>
+ """)
+
+ # Check that waiter_handler received the reply
+ msg = self.Message()
+ msg['body'] = 'Successful: test'
+ self.send(msg)
+
+ def testWaiterTimeout(self):
+ """Test that waiter handler is removed after timeout."""
+
+ def waiter_handler(stanza):
+ iq = self.xmpp.Iq()
+ iq['id'] = 'test2'
+ iq['type'] = 'set'
+ iq['query'] = 'test2'
+ try:
+ reply = iq.send(block=True, timeout=0)
+ except IqTimeout:
+ pass
+
+ self.xmpp.add_event_handler('message', waiter_handler, threaded=True)
+
+ # Start test by triggerig waiter_handler
+ self.recv("""<message><body>Start Test</body></message>""")
+
+ # Check that Iq was sent to trigger start of timeout period
+ iq = self.Iq()
+ iq['id'] = 'test2'
+ iq['type'] = 'set'
+ iq['query'] = 'test2'
+ self.send(iq)
+
+ # Give the event queue time to process.
+ time.sleep(0.1)
+
+ # Check that the waiter is no longer registered
+ waiter_exists = self.xmpp.removeHandler('IqWait_test2')
+
+ self.failUnless(waiter_exists == False,
+ "Waiter handler was not removed.")
+
+ def testIqCallback(self):
+ """Test that iq.send(callback=handle_foo) works."""
+ events = []
+
+ def handle_foo(iq):
+ events.append('foo')
+
+ iq = self.Iq()
+ iq['type'] = 'get'
+ iq['id'] = 'test-foo'
+ iq['to'] = 'user@localhost'
+ iq['query'] = 'foo'
+ iq.send(callback=handle_foo)
+
+ self.send("""
+ <iq type="get" id="test-foo" to="user@localhost">
+ <query xmlns="foo" />
+ </iq>
+ """)
+
+ self.recv("""
+ <iq type="result" id="test-foo"
+ to="test@localhost"
+ from="user@localhost">
+ <query xmlns="foo">
+ <data />
+ </query>
+ </iq>
+ """)
+
+ # Give event queue time to process
+ time.sleep(0.1)
+
+ self.failUnless(events == ['foo'],
+ "Iq callback was not executed: %s" % events)
+
+ def testMultipleHandlersForStanza(self):
+ """
+ Test that multiple handlers for a single stanza work
+ without clobbering each other.
+ """
+
+ def handler_1(msg):
+ msg.reply("Handler 1: %s" % msg['body']).send()
+
+ def handler_2(msg):
+ msg.reply("Handler 2: %s" % msg['body']).send()
+
+ def handler_3(msg):
+ msg.reply("Handler 3: %s" % msg['body']).send()
+
+ self.xmpp.add_event_handler('message', handler_1)
+ self.xmpp.add_event_handler('message', handler_2)
+ self.xmpp.add_event_handler('message', handler_3)
+
+ self.recv("""
+ <message to="tester@localhost" from="user@example.com">
+ <body>Testing</body>
+ </message>
+ """)
+
+
+ # This test is brittle, depending on the fact that handlers
+ # will be checked in the order they are registered.
+ self.send("""
+ <message to="user@example.com">
+ <body>Handler 1: Testing</body>
+ </message>
+ """)
+ self.send("""
+ <message to="user@example.com">
+ <body>Handler 2: Testing</body>
+ </message>
+ """)
+ self.send("""
+ <message to="user@example.com">
+ <body>Handler 3: Testing</body>
+ </message>
+ """)
+
+
+suite = unittest.TestLoader().loadTestsFromTestCase(TestHandlers)
diff --git a/tests/test_stream_presence.py b/tests/test_stream_presence.py
new file mode 100644
index 00000000..63ccb043
--- /dev/null
+++ b/tests/test_stream_presence.py
@@ -0,0 +1,380 @@
+import time
+from sleekxmpp.test import *
+
+
+class TestStreamPresence(SleekTest):
+ """
+ Test handling roster updates.
+ """
+
+ def tearDown(self):
+ self.stream_close()
+
+ def testInitialUnavailablePresences(self):
+ """
+ Test receiving unavailable presences from JIDs that
+ are not online.
+ """
+ events = set()
+
+ def got_offline(presence):
+ # The got_offline event should not be triggered.
+ events.add('got_offline')
+
+ def unavailable(presence):
+ # The presence_unavailable event should be triggered.
+ events.add('unavailable')
+
+ self.stream_start()
+ self.xmpp.add_event_handler('got_offline', got_offline)
+ self.xmpp.add_event_handler('presence_unavailable', unavailable)
+
+ self.recv("""
+ <presence type="unavailable"
+ from="otheruser@localhost"
+ to="tester@localhost"/>
+ """)
+
+ # Give event queue time to process.
+ time.sleep(0.1)
+
+ self.assertEqual(events, set(('unavailable',)),
+ "Got offline incorrectly triggered: %s." % events)
+
+ def testGotOffline(self):
+ """Test that got_offline is triggered properly."""
+ events = []
+
+ def got_offline(presence):
+ events.append('got_offline')
+
+ self.stream_start()
+ self.xmpp.add_event_handler('got_offline', got_offline)
+
+ # Setup roster. Use a 'set' instead of 'result' so we
+ # don't have to handle get_roster() blocking.
+ #
+ # We use the stream to initialize the roster to make
+ # the test independent of the roster implementation.
+ self.recv("""
+ <iq type="set">
+ <query xmlns="jabber:iq:roster">
+ <item jid="otheruser@localhost"
+ name="Other User"
+ subscription="both">
+ <group>Testers</group>
+ </item>
+ </query>
+ </iq>
+ """)
+
+ # Contact comes online.
+ self.recv("""
+ <presence from="otheruser@localhost/foobar"
+ to="tester@localhost" />
+ """)
+
+ # Contact goes offline, should trigger got_offline.
+ self.recv("""
+ <presence from="otheruser@localhost/foobar"
+ to="tester@localhost"
+ type="unavailable" />
+ """)
+
+ # Give event queue time to process.
+ time.sleep(0.1)
+
+ self.assertEqual(events, ['got_offline'],
+ "Got offline incorrectly triggered: %s" % events)
+
+ def testGotOnline(self):
+ """Test that got_online is triggered properly."""
+
+ events = set()
+
+ def presence_available(p):
+ events.add('presence_available')
+
+ def got_online(p):
+ events.add('got_online')
+
+ self.stream_start()
+ self.xmpp.add_event_handler('presence_available', presence_available)
+ self.xmpp.add_event_handler('got_online', got_online)
+
+ self.recv("""
+ <presence from="user@localhost"
+ to="tester@localhost" />
+ """)
+
+ # Give event queue time to process.
+ time.sleep(0.1)
+
+ expected = set(('presence_available', 'got_online'))
+ self.assertEqual(events, expected,
+ "Incorrect events triggered: %s" % events)
+
+ def testAutoAuthorizeAndSubscribe(self):
+ """
+ Test auto authorizing and auto subscribing
+ to subscription requests.
+ """
+
+ events = set()
+
+ def presence_subscribe(p):
+ events.add('presence_subscribe')
+
+ def changed_subscription(p):
+ events.add('changed_subscription')
+
+ self.stream_start(jid='tester@localhost')
+
+ self.xmpp.add_event_handler('changed_subscription',
+ changed_subscription)
+ self.xmpp.add_event_handler('presence_subscribe',
+ presence_subscribe)
+
+ # With these settings we should accept a subscription
+ # and request a subscription in return.
+ self.xmpp.auto_authorize = True
+ self.xmpp.auto_subscribe = True
+
+ self.recv("""
+ <presence from="user@localhost"
+ to="tester@localhost"
+ type="subscribe" />
+ """)
+
+ self.send("""
+ <presence to="user@localhost"
+ type="subscribed" />
+ """)
+
+ self.send("""
+ <presence to="user@localhost" />
+ """)
+
+ self.send("""
+ <presence to="user@localhost"
+ type="subscribe" />
+ """)
+
+ expected = set(('presence_subscribe', 'changed_subscription'))
+ self.assertEqual(events, expected,
+ "Incorrect events triggered: %s" % events)
+
+ def testNoAutoAuthorize(self):
+ """Test auto rejecting subscription requests."""
+
+ events = set()
+
+ def presence_subscribe(p):
+ events.add('presence_subscribe')
+
+ def changed_subscription(p):
+ events.add('changed_subscription')
+
+ self.stream_start(jid='tester@localhost')
+
+ self.xmpp.add_event_handler('changed_subscription',
+ changed_subscription)
+ self.xmpp.add_event_handler('presence_subscribe',
+ presence_subscribe)
+
+ # With this setting we should reject all subscriptions.
+ self.xmpp.roster['tester@localhost'].auto_authorize = False
+
+ self.recv("""
+ <presence from="user@localhost"
+ to="tester@localhost"
+ type="subscribe" />
+ """)
+
+ self.send("""
+ <presence to="user@localhost"
+ type="unsubscribed" />
+ """)
+
+ expected = set(('presence_subscribe', 'changed_subscription'))
+ 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)
+
+ def test_changed_status(self):
+ """Test that the changed_status event is handled properly."""
+ events = []
+ self.stream_start()
+
+ def changed_status(presence):
+ events.append(presence['type'])
+
+ self.xmpp.add_event_handler('changed_status', changed_status)
+
+ self.recv("""
+ <presence from="user@example.com" to="tester@localhost" />
+ """)
+
+ self.recv("""
+ <presence from="user@example.com" to="tester@localhost" />
+ """)
+
+ self.recv("""
+ <presence from="user@example.com" to="tester@localhost">
+ <show>away</show>
+ </presence>
+ """)
+
+ self.recv("""
+ <presence from="user@example.com" to="tester@localhost">
+ <show>away</show>
+ </presence>
+ """)
+
+ self.recv("""
+ <presence from="user@example.com" to="tester@localhost">
+ <show>dnd</show>
+ </presence>
+ """)
+
+ self.recv("""
+ <presence from="user@example.com" to="tester@localhost">
+ <show>dnd</show>
+ </presence>
+ """)
+
+ self.recv("""
+ <presence from="user@example.com" to="tester@localhost">
+ <show>chat</show>
+ </presence>
+ """)
+
+ self.recv("""
+ <presence from="user@example.com" to="tester@localhost">
+ <show>chat</show>
+ </presence>
+ """)
+
+ self.recv("""
+ <presence from="user@example.com" to="tester@localhost">
+ <show>xa</show>
+ </presence>
+ """)
+
+ self.recv("""
+ <presence from="user@example.com" to="tester@localhost">
+ <show>xa</show>
+ </presence>
+ """)
+
+ self.recv("""
+ <presence from="user@example.com"
+ to="tester@localhost"
+ type="unavailable" />
+ """)
+
+ self.recv("""
+ <presence from="user@example.com"
+ to="tester@localhost"
+ type="unavailable" />
+ """)
+
+ self.recv("""
+ <presence from="user@example.com" to="tester@localhost" />
+ """)
+
+ self.recv("""
+ <presence from="user@example.com" to="tester@localhost" />
+ """)
+
+ self.recv("""
+ <presence from="user@example.com" to="tester@localhost" />
+ """)
+
+ # Changed status text, so fire new event
+ self.recv("""
+ <presence from="user@example.com" to="tester@localhost">
+ <status>Testing!</status>
+ </presence>
+ """)
+
+ # No change in show/status values, no event
+ self.recv("""
+ <presence from="user@example.com" to="tester@localhost">
+ <status>Testing!</status>
+ </presence>
+ """)
+
+ self.recv("""
+ <presence from="user@example.com" to="tester@localhost">
+ <show>dnd</show>
+ <status>Testing!</status>
+ </presence>
+ """)
+
+ self.recv("""
+ <presence from="user@example.com" to="tester@localhost">
+ <show>dnd</show>
+ <status>Testing!</status>
+ </presence>
+ """)
+
+ time.sleep(0.3)
+
+ self.assertEqual(events, ['available', 'away', 'dnd', 'chat',
+ 'xa', 'unavailable', 'available',
+ 'available', 'dnd'],
+ "Changed status events incorrect: %s" % events)
+
+
+suite = unittest.TestLoader().loadTestsFromTestCase(TestStreamPresence)
diff --git a/tests/test_stream_roster.py b/tests/test_stream_roster.py
new file mode 100644
index 00000000..eb6d2f4f
--- /dev/null
+++ b/tests/test_stream_roster.py
@@ -0,0 +1,231 @@
+# -*- encoding:utf-8 -*-
+
+from __future__ import unicode_literals
+
+from sleekxmpp.test import *
+import time
+import threading
+
+
+class TestStreamRoster(SleekTest):
+ """
+ Test handling roster updates.
+ """
+
+ def tearDown(self):
+ self.stream_close()
+
+ def testGetRoster(self):
+ """Test handling roster requests."""
+ self.stream_start(mode='client', jid='tester@localhost')
+
+ events = []
+
+ def roster_received(iq):
+ events.append('roster_received')
+
+ self.xmpp.add_event_handler('roster_received', roster_received)
+
+ # Since get_roster blocks, we need to run it in a thread.
+ t = threading.Thread(name='get_roster', target=self.xmpp.get_roster)
+ t.start()
+
+ self.send("""
+ <iq type="get" id="1">
+ <query xmlns="jabber:iq:roster" />
+ </iq>
+ """)
+ self.recv("""
+ <iq to='tester@localhost' type="result" id="1">
+ <query xmlns="jabber:iq:roster">
+ <item jid="user@localhost"
+ name="User"
+ subscription="from"
+ ask="subscribe">
+ <group>Friends</group>
+ <group>Examples</group>
+ </item>
+ </query>
+ </iq>
+ """)
+
+ # Wait for get_roster to return.
+ t.join()
+
+ self.check_roster('tester@localhost', 'user@localhost',
+ name='User',
+ subscription='from',
+ afrom=True,
+ pending_out=True,
+ groups=['Friends', 'Examples'])
+
+ # Give the event queue time to process.
+ time.sleep(.1)
+
+ self.failUnless('roster_received' in events,
+ "Roster received event not triggered: %s" % events)
+
+ def testRosterSet(self):
+ """Test handling pushed roster updates."""
+ self.stream_start(mode='client')
+ events = []
+
+ def roster_update(e):
+ events.append('roster_update')
+
+ self.xmpp.add_event_handler('roster_update', roster_update)
+
+ self.recv("""
+ <iq to='tester@localhost' type="set" id="1">
+ <query xmlns="jabber:iq:roster">
+ <item jid="user@localhost"
+ name="User"
+ subscription="both">
+ <group>Friends</group>
+ <group>Examples</group>
+ </item>
+ </query>
+ </iq>
+ """)
+ self.send("""
+ <iq type="result" id="1">
+ <query xmlns="jabber:iq:roster" />
+ </iq>
+ """)
+
+ self.check_roster('tester@localhost', 'user@localhost',
+ name='User',
+ subscription='both',
+ groups=['Friends', 'Examples'])
+
+ # Give the event queue time to process.
+ time.sleep(.1)
+
+ self.failUnless('roster_update' in events,
+ "Roster updated event not triggered: %s" % events)
+
+ def testRosterTimeout(self):
+ """Test handling a timed out roster request."""
+ self.stream_start()
+
+ def do_test():
+ self.xmpp.get_roster(timeout=0)
+ time.sleep(.1)
+
+ self.assertRaises(IqTimeout, do_test)
+
+ def testRosterCallback(self):
+ """Test handling a roster request callback."""
+ self.stream_start()
+ events = []
+
+ def roster_callback(iq):
+ events.append('roster_callback')
+
+ # Since get_roster blocks, we need to run it in a thread.
+ t = threading.Thread(name='get_roster',
+ target=self.xmpp.get_roster,
+ kwargs={str('block'): False,
+ str('callback'): roster_callback})
+ t.start()
+
+ self.send("""
+ <iq type="get" id="1">
+ <query xmlns="jabber:iq:roster" />
+ </iq>
+ """)
+ self.recv("""
+ <iq type="result" id="1">
+ <query xmlns="jabber:iq:roster">
+ <item jid="user@localhost"
+ name="User"
+ subscription="both">
+ <group>Friends</group>
+ <group>Examples</group>
+ </item>
+ </query>
+ </iq>
+ """)
+
+ # Wait for get_roster to return.
+ t.join()
+
+ # Give the event queue time to process.
+ time.sleep(.1)
+
+ self.failUnless(events == ['roster_callback'],
+ "Roster timeout event not triggered: %s." % events)
+
+ def testRosterUnicode(self):
+ """Test that JIDs with Unicode values are handled properly."""
+ self.stream_start()
+ self.recv("""
+ <iq to="tester@localhost" type="set" id="1">
+ <query xmlns="jabber:iq:roster">
+ <item jid="andré@foo" subscription="both">
+ <group>Unicode</group>
+ </item>
+ </query>
+ </iq>
+ """)
+
+ # Give the event queue time to process.
+ time.sleep(.1)
+
+ self.check_roster('tester@localhost', 'andré@foo',
+ subscription='both',
+ groups=['Unicode'])
+
+ jids = list(self.xmpp.client_roster.keys())
+ self.failUnless(jids == ['andré@foo'],
+ "Too many roster entries found: %s" % jids)
+
+ self.recv("""
+ <presence to="tester@localhost" from="andré@foo/bar">
+ <show>away</show>
+ <status>Testing</status>
+ </presence>
+ """)
+
+ # Give the event queue time to process.
+ time.sleep(.1)
+
+ result = self.xmpp.client_roster['andré@foo'].resources
+ expected = {'bar': {'status':'Testing',
+ 'show':'away',
+ 'priority':0}}
+ self.failUnless(result == expected,
+ "Unexpected roster values: %s" % result)
+
+ def testSendLastPresence(self):
+ """Test that sending the last presence works."""
+ self.stream_start()
+ self.xmpp.send_presence(pshow='dnd')
+ self.xmpp.auto_authorize = True
+ self.xmpp.auto_subscribe = True
+
+ self.send("""
+ <presence>
+ <show>dnd</show>
+ </presence>
+ """)
+
+ self.recv("""
+ <presence from="user@localhost"
+ to="tester@localhost"
+ type="subscribe" />
+ """)
+
+ self.send("""
+ <presence to="user@localhost"
+ type="subscribed" />
+ """)
+
+ self.send("""
+ <presence to="user@localhost">
+ <show>dnd</show>
+ </presence>
+ """)
+
+
+suite = unittest.TestLoader().loadTestsFromTestCase(TestStreamRoster)
diff --git a/tests/test_stream_xep_0030.py b/tests/test_stream_xep_0030.py
new file mode 100644
index 00000000..dd43778a
--- /dev/null
+++ b/tests/test_stream_xep_0030.py
@@ -0,0 +1,576 @@
+import sys
+import time
+import threading
+
+from sleekxmpp.test import *
+
+
+class TestStreamDisco(SleekTest):
+
+ """
+ Test using the XEP-0030 plugin.
+ """
+
+ def tearDown(self):
+ self.stream_close()
+
+ def testInfoEmptyDefaultNode(self):
+ """
+ Info query result from an entity MUST have at least one identity
+ and feature, namely http://jabber.org/protocol/disco#info.
+
+ Since the XEP-0030 plugin is loaded, a disco response should
+ be generated and not an error result.
+ """
+ self.stream_start(mode='client',
+ plugins=['xep_0030'])
+
+ self.recv("""
+ <iq type="get" id="test">
+ <query xmlns="http://jabber.org/protocol/disco#info" />
+ </iq>
+ """)
+
+ self.send("""
+ <iq type="result" id="test">
+ <query xmlns="http://jabber.org/protocol/disco#info">
+ <identity category="client" type="bot" />
+ <feature var="http://jabber.org/protocol/disco#info" />
+ </query>
+ </iq>
+ """)
+
+ def testInfoEmptyDefaultNodeComponent(self):
+ """
+ Test requesting an empty, default node using a Component.
+ """
+ self.stream_start(mode='component',
+ jid='tester.localhost',
+ plugins=['xep_0030'])
+
+ self.recv("""
+ <iq type="get" id="test">
+ <query xmlns="http://jabber.org/protocol/disco#info" />
+ </iq>
+ """)
+
+ self.send("""
+ <iq type="result" id="test">
+ <query xmlns="http://jabber.org/protocol/disco#info">
+ <identity category="component" type="generic" />
+ <feature var="http://jabber.org/protocol/disco#info" />
+ </query>
+ </iq>
+ """)
+
+ def testInfoIncludeNode(self):
+ """
+ Results for info queries directed to a particular node MUST
+ include the node in the query response.
+ """
+ self.stream_start(mode='client',
+ plugins=['xep_0030'])
+
+
+ self.xmpp['xep_0030'].static.add_node(node='testing')
+
+ self.recv("""
+ <iq to="tester@localhost" type="get" id="test">
+ <query xmlns="http://jabber.org/protocol/disco#info"
+ node="testing" />
+ </iq>
+ """)
+
+ self.send("""
+ <iq type="result" id="test">
+ <query xmlns="http://jabber.org/protocol/disco#info"
+ node="testing">
+ </query>
+ </iq>""",
+ method='mask')
+
+ def testItemsIncludeNode(self):
+ """
+ Results for items queries directed to a particular node MUST
+ include the node in the query response.
+ """
+ self.stream_start(mode='client',
+ plugins=['xep_0030'])
+
+
+ self.xmpp['xep_0030'].static.add_node(node='testing')
+
+ self.recv("""
+ <iq to="tester@localhost" type="get" id="test">
+ <query xmlns="http://jabber.org/protocol/disco#items"
+ node="testing" />
+ </iq>
+ """)
+
+ self.send("""
+ <iq type="result" id="test">
+ <query xmlns="http://jabber.org/protocol/disco#items"
+ node="testing">
+ </query>
+ </iq>""",
+ method='mask')
+
+ def testDynamicInfoJID(self):
+ """
+ Test using a dynamic info handler for a particular JID.
+ """
+ self.stream_start(mode='client',
+ plugins=['xep_0030'])
+
+ def dynamic_jid(jid, node, ifrom, iq):
+ result = self.xmpp['xep_0030'].stanza.DiscoInfo()
+ result['node'] = node
+ result.add_identity('client', 'console', name='Dynamic Info')
+ return result
+
+ self.xmpp['xep_0030'].set_node_handler('get_info',
+ jid='tester@localhost',
+ handler=dynamic_jid)
+
+ self.recv("""
+ <iq type="get" id="test" to="tester@localhost">
+ <query xmlns="http://jabber.org/protocol/disco#info"
+ node="testing" />
+ </iq>
+ """)
+
+ self.send("""
+ <iq type="result" id="test">
+ <query xmlns="http://jabber.org/protocol/disco#info"
+ node="testing">
+ <identity category="client"
+ type="console"
+ name="Dynamic Info" />
+ </query>
+ </iq>
+ """)
+
+ def testDynamicInfoGlobal(self):
+ """
+ Test using a dynamic info handler for all requests.
+ """
+ self.stream_start(mode='component',
+ jid='tester.localhost',
+ plugins=['xep_0030'])
+
+ def dynamic_global(jid, node, ifrom, iq):
+ result = self.xmpp['xep_0030'].stanza.DiscoInfo()
+ result['node'] = node
+ result.add_identity('component', 'generic', name='Dynamic Info')
+ return result
+
+ self.xmpp['xep_0030'].set_node_handler('get_info',
+ handler=dynamic_global)
+
+ self.recv("""
+ <iq type="get" id="test"
+ to="user@tester.localhost"
+ from="tester@localhost">
+ <query xmlns="http://jabber.org/protocol/disco#info"
+ node="testing" />
+ </iq>
+ """)
+
+ self.send("""
+ <iq type="result" id="test"
+ to="tester@localhost"
+ from="user@tester.localhost">
+ <query xmlns="http://jabber.org/protocol/disco#info"
+ node="testing">
+ <identity category="component"
+ type="generic"
+ name="Dynamic Info" />
+ </query>
+ </iq>
+ """)
+
+ def testOverrideJIDInfoHandler(self):
+ """Test overriding a JID info handler."""
+ self.stream_start(mode='client',
+ plugins=['xep_0030'])
+
+ def dynamic_jid(jid, node, ifrom, iq):
+ result = self.xmpp['xep_0030'].stanza.DiscoInfo()
+ result['node'] = node
+ result.add_identity('client', 'console', name='Dynamic Info')
+ return result
+
+ self.xmpp['xep_0030'].set_node_handler('get_info',
+ jid='tester@localhost',
+ handler=dynamic_jid)
+
+
+ self.xmpp['xep_0030'].make_static(jid='tester@localhost',
+ node='testing')
+
+ self.xmpp['xep_0030'].add_identity(jid='tester@localhost',
+ node='testing',
+ category='automation',
+ itype='command-list')
+
+ self.recv("""
+ <iq type="get" id="test" to="tester@localhost">
+ <query xmlns="http://jabber.org/protocol/disco#info"
+ node="testing" />
+ </iq>
+ """)
+
+ self.send("""
+ <iq type="result" id="test">
+ <query xmlns="http://jabber.org/protocol/disco#info"
+ node="testing">
+ <identity category="automation"
+ type="command-list" />
+ </query>
+ </iq>
+ """)
+
+ def testOverrideGlobalInfoHandler(self):
+ """Test overriding the global JID info handler."""
+ self.stream_start(mode='component',
+ jid='tester.localhost',
+ plugins=['xep_0030'])
+
+ def dynamic_global(jid, node, ifrom, iq):
+ result = self.xmpp['xep_0030'].stanza.DiscoInfo()
+ result['node'] = node
+ result.add_identity('component', 'generic', name='Dynamic Info')
+ return result
+
+ self.xmpp['xep_0030'].set_node_handler('get_info',
+ handler=dynamic_global)
+
+ self.xmpp['xep_0030'].make_static(jid='user@tester.localhost',
+ node='testing')
+
+ self.xmpp['xep_0030'].add_feature(jid='user@tester.localhost',
+ node='testing',
+ feature='urn:xmpp:ping')
+
+ self.recv("""
+ <iq type="get" id="test"
+ to="user@tester.localhost"
+ from="tester@localhost">
+ <query xmlns="http://jabber.org/protocol/disco#info"
+ node="testing" />
+ </iq>
+ """)
+
+ self.send("""
+ <iq type="result" id="test"
+ to="tester@localhost"
+ from="user@tester.localhost">
+ <query xmlns="http://jabber.org/protocol/disco#info"
+ node="testing">
+ <feature var="urn:xmpp:ping" />
+ </query>
+ </iq>
+ """)
+
+ def testGetInfoRemote(self):
+ """
+ Test sending a disco#info query to another entity
+ and receiving the result.
+ """
+ self.stream_start(mode='client',
+ plugins=['xep_0030'])
+
+ events = set()
+
+ def handle_disco_info(iq):
+ events.add('disco_info')
+
+
+ self.xmpp.add_event_handler('disco_info', handle_disco_info)
+
+ t = threading.Thread(name="get_info",
+ target=self.xmpp['xep_0030'].get_info,
+ args=('user@localhost', 'foo'))
+ t.start()
+
+ self.send("""
+ <iq type="get" to="user@localhost" id="1">
+ <query xmlns="http://jabber.org/protocol/disco#info"
+ node="foo" />
+ </iq>
+ """)
+
+ self.recv("""
+ <iq type="result" to="tester@localhost" id="1">
+ <query xmlns="http://jabber.org/protocol/disco#info"
+ node="foo">
+ <identity category="client" type="bot" />
+ <feature var="urn:xmpp:ping" />
+ </query>
+ </iq>
+ """)
+
+ # Wait for disco#info request to be received.
+ t.join()
+
+ time.sleep(0.1)
+
+ self.assertEqual(events, set(('disco_info',)),
+ "Disco info event was not triggered: %s" % events)
+
+ def testDynamicItemsJID(self):
+ """
+ Test using a dynamic items handler for a particular JID.
+ """
+ self.stream_start(mode='client',
+ plugins=['xep_0030'])
+
+ def dynamic_jid(jid, node, ifrom, iq):
+ result = self.xmpp['xep_0030'].stanza.DiscoItems()
+ result['node'] = node
+ result.add_item('tester@localhost', node='foo', name='JID')
+ return result
+
+ self.xmpp['xep_0030'].set_node_handler('get_items',
+ jid='tester@localhost',
+ handler=dynamic_jid)
+
+ self.recv("""
+ <iq type="get" id="test" to="tester@localhost">
+ <query xmlns="http://jabber.org/protocol/disco#items"
+ node="testing" />
+ </iq>
+ """)
+
+ self.send("""
+ <iq type="result" id="test">
+ <query xmlns="http://jabber.org/protocol/disco#items"
+ node="testing">
+ <item jid="tester@localhost" node="foo" name="JID" />
+ </query>
+ </iq>
+ """)
+
+ def testDynamicItemsGlobal(self):
+ """
+ Test using a dynamic items handler for all requests.
+ """
+ self.stream_start(mode='component',
+ jid='tester.localhost',
+ plugins=['xep_0030'])
+
+ def dynamic_global(jid, node, ifrom, iq):
+ result = self.xmpp['xep_0030'].stanza.DiscoItems()
+ result['node'] = node
+ result.add_item('tester@localhost', node='foo', name='Global')
+ return result
+
+ self.xmpp['xep_0030'].set_node_handler('get_items',
+ handler=dynamic_global)
+
+ self.recv("""
+ <iq type="get" id="test"
+ to="user@tester.localhost"
+ from="tester@localhost">
+ <query xmlns="http://jabber.org/protocol/disco#items"
+ node="testing" />
+ </iq>
+ """)
+
+ self.send("""
+ <iq type="result" id="test"
+ to="tester@localhost"
+ from="user@tester.localhost">
+ <query xmlns="http://jabber.org/protocol/disco#items"
+ node="testing">
+ <item jid="tester@localhost" node="foo" name="Global" />
+ </query>
+ </iq>
+ """)
+
+ def testOverrideJIDItemsHandler(self):
+ """Test overriding a JID items handler."""
+ self.stream_start(mode='client',
+ plugins=['xep_0030'])
+
+ def dynamic_jid(jid, node, ifrom, iq):
+ result = self.xmpp['xep_0030'].stanza.DiscoItems()
+ result['node'] = node
+ result.add_item('tester@localhost', node='foo', name='Global')
+ return result
+
+ self.xmpp['xep_0030'].set_node_handler('get_items',
+ jid='tester@localhost',
+ handler=dynamic_jid)
+
+
+ self.xmpp['xep_0030'].make_static(jid='tester@localhost',
+ node='testing')
+
+ self.xmpp['xep_0030'].add_item(ijid='tester@localhost',
+ node='testing',
+ jid='tester@localhost',
+ subnode='foo',
+ name='Test')
+
+ self.recv("""
+ <iq type="get" id="test" to="tester@localhost">
+ <query xmlns="http://jabber.org/protocol/disco#items"
+ node="testing" />
+ </iq>
+ """)
+
+ self.send("""
+ <iq type="result" id="test">
+ <query xmlns="http://jabber.org/protocol/disco#items"
+ node="testing">
+ <item jid="tester@localhost" node="foo" name="Test" />
+ </query>
+ </iq>
+ """)
+
+ def testOverrideGlobalItemsHandler(self):
+ """Test overriding the global JID items handler."""
+ self.stream_start(mode='component',
+ jid='tester.localhost',
+ plugins=['xep_0030'])
+
+ def dynamic_global(jid, node, ifrom, iq):
+ result = self.xmpp['xep_0030'].stanza.DiscoItems()
+ result['node'] = node
+ result.add_item('tester.localhost', node='foo', name='Global')
+ return result
+
+ self.xmpp['xep_0030'].set_node_handler('get_items',
+ handler=dynamic_global)
+
+ self.xmpp['xep_0030'].make_static(jid='user@tester.localhost',
+ node='testing')
+
+ self.xmpp['xep_0030'].add_item(ijid='user@tester.localhost',
+ node='testing',
+ jid='user@tester.localhost',
+ subnode='foo',
+ name='Test')
+
+ self.recv("""
+ <iq type="get" id="test"
+ to="user@tester.localhost"
+ from="tester@localhost">
+ <query xmlns="http://jabber.org/protocol/disco#items"
+ node="testing" />
+ </iq>
+ """)
+
+ self.send("""
+ <iq type="result" id="test"
+ to="tester@localhost"
+ from="user@tester.localhost">
+ <query xmlns="http://jabber.org/protocol/disco#items"
+ node="testing">
+ <item jid="user@tester.localhost" node="foo" name="Test" />
+ </query>
+ </iq>
+ """)
+
+ def testGetItemsRemote(self):
+ """
+ Test sending a disco#items query to another entity
+ and receiving the result.
+ """
+ self.stream_start(mode='client',
+ plugins=['xep_0030'])
+
+ events = set()
+ results = set()
+
+ def handle_disco_items(iq):
+ events.add('disco_items')
+ results.update(iq['disco_items']['items'])
+
+
+ self.xmpp.add_event_handler('disco_items', handle_disco_items)
+
+ t = threading.Thread(name="get_items",
+ target=self.xmpp['xep_0030'].get_items,
+ args=('user@localhost', 'foo'))
+ t.start()
+
+ self.send("""
+ <iq type="get" to="user@localhost" id="1">
+ <query xmlns="http://jabber.org/protocol/disco#items"
+ node="foo" />
+ </iq>
+ """)
+
+ self.recv("""
+ <iq type="result" to="tester@localhost" id="1">
+ <query xmlns="http://jabber.org/protocol/disco#items"
+ node="foo">
+ <item jid="user@localhost" node="bar" name="Test" />
+ <item jid="user@localhost" node="baz" name="Test 2" />
+ </query>
+ </iq>
+ """)
+
+ # Wait for disco#items request to be received.
+ t.join()
+
+ time.sleep(0.1)
+
+ items = set([('user@localhost', 'bar', 'Test'),
+ ('user@localhost', 'baz', 'Test 2')])
+ self.assertEqual(events, set(('disco_items',)),
+ "Disco items event was not triggered: %s" % events)
+ self.assertEqual(results, items,
+ "Unexpected items: %s" % results)
+
+ def testGetItemsIterator(self):
+ """Test interaction between XEP-0030 and XEP-0059 plugins."""
+
+ raised_exceptions = []
+
+ self.stream_start(mode='client',
+ plugins=['xep_0030', 'xep_0059'])
+
+ results = self.xmpp['xep_0030'].get_items(jid='foo@localhost',
+ node='bar',
+ 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=run_test)
+ t.start()
+
+ self.send("""
+ <iq id="2" type="get" to="foo@localhost">
+ <query xmlns="http://jabber.org/protocol/disco#items"
+ node="bar">
+ <set xmlns="http://jabber.org/protocol/rsm">
+ <max>10</max>
+ </set>
+ </query>
+ </iq>
+ """)
+ self.recv("""
+ <iq id="2" type="result" to="tester@localhost">
+ <query xmlns="http://jabber.org/protocol/disco#items">
+ <set xmlns="http://jabber.org/protocol/rsm">
+ </set>
+ </query>
+ </iq>
+ """)
+
+ t.join()
+
+ self.assertEqual(raised_exceptions, [True],
+ "StopIteration was not raised: %s" % raised_exceptions)
+
+
+suite = unittest.TestLoader().loadTestsFromTestCase(TestStreamDisco)
diff --git a/tests/test_stream_xep_0050.py b/tests/test_stream_xep_0050.py
new file mode 100644
index 00000000..1931349d
--- /dev/null
+++ b/tests/test_stream_xep_0050.py
@@ -0,0 +1,726 @@
+import time
+import threading
+
+from sleekxmpp.test import *
+
+
+class TestAdHocCommands(SleekTest):
+
+ def setUp(self):
+ self.stream_start(mode='client',
+ plugins=['xep_0030', 'xep_0004', 'xep_0050'])
+
+ # Real session IDs don't make for nice tests, so use
+ # a dummy value.
+ self.xmpp['xep_0050'].new_session = lambda: '_sessionid_'
+
+ def tearDown(self):
+ self.stream_close()
+
+ def testZeroStepCommand(self):
+ """Test running a command with no steps."""
+
+ def handle_command(iq, session):
+ form = self.xmpp['xep_0004'].makeForm(ftype='result')
+ form.addField(var='foo', ftype='text-single',
+ label='Foo', value='bar')
+
+ session['payload'] = form
+ session['next'] = None
+ session['has_next'] = False
+
+ return session
+
+ self.xmpp['xep_0050'].add_command('tester@localhost', 'foo',
+ 'Do Foo', handle_command)
+
+ self.recv("""
+ <iq id="11" type="set" to="tester@localhost" from="foo@bar">
+ <command xmlns="http://jabber.org/protocol/commands"
+ node="foo"
+ action="execute" />
+ </iq>
+ """)
+
+ self.send("""
+ <iq id="11" type="result" to="foo@bar">
+ <command xmlns="http://jabber.org/protocol/commands"
+ node="foo"
+ status="completed"
+ sessionid="_sessionid_">
+ <x xmlns="jabber:x:data" type="result">
+ <field var="foo" label="Foo" type="text-single">
+ <value>bar</value>
+ </field>
+ </x>
+ </command>
+ </iq>
+ """)
+
+ def testOneStepCommand(self):
+ """Test running a single step command."""
+ results = []
+
+ def handle_command(iq, session):
+
+ def handle_form(form, session):
+ results.append(form['values']['foo'])
+
+ form = self.xmpp['xep_0004'].makeForm('form')
+ form.addField(var='foo', ftype='text-single', label='Foo')
+
+ session['payload'] = form
+ session['next'] = handle_form
+ session['has_next'] = False
+
+ return session
+
+ self.xmpp['xep_0050'].add_command('tester@localhost', 'foo',
+ 'Do Foo', handle_command)
+
+ self.recv("""
+ <iq id="11" type="set" to="tester@localhost" from="foo@bar">
+ <command xmlns="http://jabber.org/protocol/commands"
+ node="foo"
+ action="execute" />
+ </iq>
+ """)
+
+ self.send("""
+ <iq id="11" type="result" to="foo@bar">
+ <command xmlns="http://jabber.org/protocol/commands"
+ node="foo"
+ status="executing"
+ sessionid="_sessionid_">
+ <actions>
+ <complete />
+ </actions>
+ <x xmlns="jabber:x:data" type="form">
+ <field var="foo" label="Foo" type="text-single" />
+ </x>
+ </command>
+ </iq>
+ """)
+
+ self.recv("""
+ <iq id="12" type="set" to="tester@localhost" from="foo@bar">
+ <command xmlns="http://jabber.org/protocol/commands"
+ node="foo"
+ action="complete"
+ sessionid="_sessionid_">
+ <x xmlns="jabber:x:data" type="submit">
+ <field var="foo" label="Foo" type="text-single">
+ <value>blah</value>
+ </field>
+ </x>
+ </command>
+ </iq>
+ """)
+
+ self.send("""
+ <iq id="12" type="result" to="foo@bar">
+ <command xmlns="http://jabber.org/protocol/commands"
+ node="foo"
+ status="completed"
+ sessionid="_sessionid_" />
+ </iq>
+ """)
+
+ self.assertEqual(results, ['blah'],
+ "Command handler was not executed: %s" % results)
+
+ def testTwoStepCommand(self):
+ """Test using a two-stage command."""
+ results = []
+
+ def handle_command(iq, session):
+
+ def handle_step2(form, session):
+ results.append(form['values']['bar'])
+
+ def handle_step1(form, session):
+ results.append(form['values']['foo'])
+
+ form = self.xmpp['xep_0004'].makeForm('form')
+ form.addField(var='bar', ftype='text-single', label='Bar')
+
+ session['payload'] = form
+ session['next'] = handle_step2
+ session['has_next'] = False
+
+ return session
+
+ form = self.xmpp['xep_0004'].makeForm('form')
+ form.addField(var='foo', ftype='text-single', label='Foo')
+
+ session['payload'] = form
+ session['next'] = handle_step1
+ session['has_next'] = True
+
+ return session
+
+ self.xmpp['xep_0050'].add_command('tester@localhost', 'foo',
+ 'Do Foo', handle_command)
+
+ self.recv("""
+ <iq id="11" type="set" to="tester@localhost" from="foo@bar">
+ <command xmlns="http://jabber.org/protocol/commands"
+ node="foo"
+ action="execute" />
+ </iq>
+ """)
+
+ self.send("""
+ <iq id="11" type="result" to="foo@bar">
+ <command xmlns="http://jabber.org/protocol/commands"
+ node="foo"
+ status="executing"
+ sessionid="_sessionid_">
+ <actions>
+ <next />
+ </actions>
+ <x xmlns="jabber:x:data" type="form">
+ <field var="foo" label="Foo" type="text-single" />
+ </x>
+ </command>
+ </iq>
+ """)
+
+ self.recv("""
+ <iq id="12" type="set" to="tester@localhost" from="foo@bar">
+ <command xmlns="http://jabber.org/protocol/commands"
+ node="foo"
+ action="next"
+ sessionid="_sessionid_">
+ <x xmlns="jabber:x:data" type="submit">
+ <field var="foo" label="Foo" type="text-single">
+ <value>blah</value>
+ </field>
+ </x>
+ </command>
+ </iq>
+ """)
+
+ self.send("""
+ <iq id="12" type="result" to="foo@bar">
+ <command xmlns="http://jabber.org/protocol/commands"
+ node="foo"
+ status="executing"
+ sessionid="_sessionid_">
+ <actions>
+ <complete />
+ </actions>
+ <x xmlns="jabber:x:data" type="form">
+ <field var="bar" label="Bar" type="text-single" />
+ </x>
+ </command>
+ </iq>
+ """)
+
+ self.recv("""
+ <iq id="13" type="set" to="tester@localhost" from="foo@bar">
+ <command xmlns="http://jabber.org/protocol/commands"
+ node="foo"
+ action="complete"
+ sessionid="_sessionid_">
+ <x xmlns="jabber:x:data" type="submit">
+ <field var="bar" label="Bar" type="text-single">
+ <value>meh</value>
+ </field>
+ </x>
+ </command>
+ </iq>
+ """)
+ self.send("""
+ <iq id="13" type="result" to="foo@bar">
+ <command xmlns="http://jabber.org/protocol/commands"
+ node="foo"
+ status="completed"
+ sessionid="_sessionid_" />
+ </iq>
+ """)
+
+ self.assertEqual(results, ['blah', 'meh'],
+ "Command handler was not executed: %s" % results)
+
+ def testCancelCommand(self):
+ """Test canceling command."""
+ results = []
+
+ def handle_command(iq, session):
+
+ def handle_form(form, session):
+ results.append(form['values']['foo'])
+
+ def handle_cancel(iq, session):
+ results.append('canceled')
+
+ form = self.xmpp['xep_0004'].makeForm('form')
+ form.addField(var='foo', ftype='text-single', label='Foo')
+
+ session['payload'] = form
+ session['next'] = handle_form
+ session['cancel'] = handle_cancel
+ session['has_next'] = False
+
+ return session
+
+ self.xmpp['xep_0050'].add_command('tester@localhost', 'foo',
+ 'Do Foo', handle_command)
+
+ self.recv("""
+ <iq id="11" type="set" to="tester@localhost" from="foo@bar">
+ <command xmlns="http://jabber.org/protocol/commands"
+ node="foo"
+ action="execute" />
+ </iq>
+ """)
+
+ self.send("""
+ <iq id="11" type="result" to="foo@bar">
+ <command xmlns="http://jabber.org/protocol/commands"
+ node="foo"
+ status="executing"
+ sessionid="_sessionid_">
+ <actions>
+ <complete />
+ </actions>
+ <x xmlns="jabber:x:data" type="form">
+ <field var="foo" label="Foo" type="text-single" />
+ </x>
+ </command>
+ </iq>
+ """)
+
+ self.recv("""
+ <iq id="12" type="set" to="tester@localhost" from="foo@bar">
+ <command xmlns="http://jabber.org/protocol/commands"
+ node="foo"
+ action="cancel"
+ sessionid="_sessionid_">
+ <x xmlns="jabber:x:data" type="submit">
+ <field var="foo" label="Foo" type="text-single">
+ <value>blah</value>
+ </field>
+ </x>
+ </command>
+ </iq>
+ """)
+
+ self.send("""
+ <iq id="12" type="result" to="foo@bar">
+ <command xmlns="http://jabber.org/protocol/commands"
+ node="foo"
+ status="canceled"
+ sessionid="_sessionid_" />
+ </iq>
+ """)
+
+ self.assertEqual(results, ['canceled'],
+ "Cancelation handler not executed: %s" % results)
+
+ def testCommandNote(self):
+ """Test adding notes to commands."""
+
+ def handle_command(iq, session):
+ form = self.xmpp['xep_0004'].makeForm(ftype='result')
+ form.addField(var='foo', ftype='text-single',
+ label='Foo', value='bar')
+
+ session['payload'] = form
+ session['next'] = None
+ session['has_next'] = False
+ session['notes'] = [('info', 'testing notes')]
+
+ return session
+
+ self.xmpp['xep_0050'].add_command('tester@localhost', 'foo',
+ 'Do Foo', handle_command)
+
+ self.recv("""
+ <iq id="11" type="set" to="tester@localhost" from="foo@bar">
+ <command xmlns="http://jabber.org/protocol/commands"
+ node="foo"
+ action="execute" />
+ </iq>
+ """)
+
+ self.send("""
+ <iq id="11" type="result" to="foo@bar">
+ <command xmlns="http://jabber.org/protocol/commands"
+ node="foo"
+ status="completed"
+ sessionid="_sessionid_">
+ <note type="info">testing notes</note>
+ <x xmlns="jabber:x:data" type="result">
+ <field var="foo" label="Foo" type="text-single">
+ <value>bar</value>
+ </field>
+ </x>
+ </command>
+ </iq>
+ """)
+
+
+
+ def testMultiPayloads(self):
+ """Test using commands with multiple payloads."""
+ results = []
+
+ def handle_command(iq, session):
+
+ def handle_form(forms, session):
+ for form in forms:
+ results.append(form['values']['FORM_TYPE'])
+
+ form1 = self.xmpp['xep_0004'].makeForm('form')
+ form1.addField(var='FORM_TYPE', ftype='hidden', value='form_1')
+ form1.addField(var='foo', ftype='text-single', label='Foo')
+
+ form2 = self.xmpp['xep_0004'].makeForm('form')
+ form2.addField(var='FORM_TYPE', ftype='hidden', value='form_2')
+ form2.addField(var='foo', ftype='text-single', label='Foo')
+
+ session['payload'] = [form1, form2]
+ session['next'] = handle_form
+ session['has_next'] = False
+
+ return session
+
+ self.xmpp['xep_0050'].add_command('tester@localhost', 'foo',
+ 'Do Foo', handle_command)
+
+ self.recv("""
+ <iq id="11" type="set" to="tester@localhost" from="foo@bar">
+ <command xmlns="http://jabber.org/protocol/commands"
+ node="foo"
+ action="execute" />
+ </iq>
+ """)
+
+ self.send("""
+ <iq id="11" type="result" to="foo@bar">
+ <command xmlns="http://jabber.org/protocol/commands"
+ node="foo"
+ status="executing"
+ sessionid="_sessionid_">
+ <actions>
+ <complete />
+ </actions>
+ <x xmlns="jabber:x:data" type="form">
+ <field var="FORM_TYPE" type="hidden">
+ <value>form_1</value>
+ </field>
+ <field var="foo" label="Foo" type="text-single" />
+ </x>
+ <x xmlns="jabber:x:data" type="form">
+ <field var="FORM_TYPE" type="hidden">
+ <value>form_2</value>
+ </field>
+ <field var="foo" label="Foo" type="text-single" />
+ </x>
+ </command>
+ </iq>
+ """)
+
+ self.recv("""
+ <iq id="12" type="set" to="tester@localhost" from="foo@bar">
+ <command xmlns="http://jabber.org/protocol/commands"
+ node="foo"
+ action="complete"
+ sessionid="_sessionid_">
+ <x xmlns="jabber:x:data" type="submit">
+ <field var="FORM_TYPE" type="hidden">
+ <value>form_1</value>
+ </field>
+ <field var="foo" type="text-single">
+ <value>bar</value>
+ </field>
+ </x>
+ <x xmlns="jabber:x:data" type="submit">
+ <field var="FORM_TYPE" type="hidden">
+ <value>form_2</value>
+ </field>
+ <field var="foo" type="text-single">
+ <value>bar</value>
+ </field>
+ </x>
+ </command>
+ </iq>
+ """)
+
+ self.send("""
+ <iq id="12" type="result" to="foo@bar">
+ <command xmlns="http://jabber.org/protocol/commands"
+ node="foo"
+ status="completed"
+ sessionid="_sessionid_" />
+ </iq>
+ """)
+
+ self.assertEqual(results, [['form_1'], ['form_2']],
+ "Command handler was not executed: %s" % results)
+
+ def testClientAPI(self):
+ """Test using client-side API for commands."""
+ results = []
+
+ def handle_complete(iq, session):
+ for item in session['custom_data']:
+ results.append(item)
+
+ def handle_step2(iq, session):
+ form = self.xmpp['xep_0004'].makeForm(ftype='submit')
+ form.addField(var='bar', value='123')
+
+ session['custom_data'].append('baz')
+ session['payload'] = form
+ session['next'] = handle_complete
+ self.xmpp['xep_0050'].complete_command(session)
+
+ def handle_step1(iq, session):
+ form = self.xmpp['xep_0004'].makeForm(ftype='submit')
+ form.addField(var='foo', value='42')
+
+ session['custom_data'].append('bar')
+ session['payload'] = form
+ session['next'] = handle_step2
+ self.xmpp['xep_0050'].continue_command(session)
+
+ session = {'custom_data': ['foo'],
+ 'next': handle_step1}
+
+ self.xmpp['xep_0050'].start_command(
+ 'foo@example.com',
+ 'test_client',
+ session)
+
+ self.send("""
+ <iq id="1" to="foo@example.com" type="set">
+ <command xmlns="http://jabber.org/protocol/commands"
+ node="test_client"
+ action="execute" />
+ </iq>
+ """)
+
+ self.recv("""
+ <iq id="1" from="foo@example.com" type="result">
+ <command xmlns="http://jabber.org/protocol/commands"
+ node="test_client"
+ sessionid="_sessionid_"
+ status="executing">
+ <x xmlns="jabber:x:data" type="form">
+ <field var="foo" type="text-single" />
+ </x>
+ </command>
+ </iq>
+ """)
+
+ self.send("""
+ <iq id="2" to="foo@example.com" type="set">
+ <command xmlns="http://jabber.org/protocol/commands"
+ node="test_client"
+ sessionid="_sessionid_"
+ action="next">
+ <x xmlns="jabber:x:data" type="submit">
+ <field var="foo">
+ <value>42</value>
+ </field>
+ </x>
+ </command>
+ </iq>
+ """)
+
+ self.recv("""
+ <iq id="2" from="foo@example.com" type="result">
+ <command xmlns="http://jabber.org/protocol/commands"
+ node="test_client"
+ sessionid="_sessionid_"
+ status="executing">
+ <x xmlns="jabber:x:data" type="form">
+ <field var="bar" type="text-single" />
+ </x>
+ </command>
+ </iq>
+ """)
+
+ self.send("""
+ <iq id="3" to="foo@example.com" type="set">
+ <command xmlns="http://jabber.org/protocol/commands"
+ node="test_client"
+ sessionid="_sessionid_"
+ action="complete">
+ <x xmlns="jabber:x:data" type="submit">
+ <field var="bar">
+ <value>123</value>
+ </field>
+ </x>
+ </command>
+ </iq>
+ """)
+
+ self.recv("""
+ <iq id="3" from="foo@example.com" type="result">
+ <command xmlns="http://jabber.org/protocol/commands"
+ node="test_client"
+ sessionid="_sessionid_"
+ status="completed" />
+ </iq>
+ """)
+
+ # Give the event queue time to process
+ time.sleep(0.3)
+
+ self.failUnless(results == ['foo', 'bar', 'baz'],
+ 'Incomplete command workflow: %s' % results)
+
+ def testClientAPICancel(self):
+ """Test using client-side cancel API for commands."""
+ results = []
+
+ def handle_canceled(iq, session):
+ for item in session['custom_data']:
+ results.append(item)
+
+ def handle_step1(iq, session):
+ session['custom_data'].append('bar')
+ session['next'] = handle_canceled
+ self.xmpp['xep_0050'].cancel_command(session)
+
+ session = {'custom_data': ['foo'],
+ 'next': handle_step1}
+
+ self.xmpp['xep_0050'].start_command(
+ 'foo@example.com',
+ 'test_client',
+ session)
+
+ self.send("""
+ <iq id="1" to="foo@example.com" type="set">
+ <command xmlns="http://jabber.org/protocol/commands"
+ node="test_client"
+ action="execute" />
+ </iq>
+ """)
+
+ self.recv("""
+ <iq id="1" to="foo@example.com" type="result">
+ <command xmlns="http://jabber.org/protocol/commands"
+ node="test_client"
+ sessionid="_sessionid_"
+ status="executing">
+ <x xmlns="jabber:x:data" type="form">
+ <field var="foo" type="text-single" />
+ </x>
+ </command>
+ </iq>
+ """)
+
+ self.send("""
+ <iq id="2" to="foo@example.com" type="set">
+ <command xmlns="http://jabber.org/protocol/commands"
+ node="test_client"
+ sessionid="_sessionid_"
+ action="cancel" />
+ </iq>
+ """)
+
+ self.recv("""
+ <iq id="2" to="foo@example.com" type="result">
+ <command xmlns="http://jabber.org/protocol/commands"
+ node="test_client"
+ sessionid="_sessionid_"
+ status="canceled" />
+ </iq>
+ """)
+
+ # Give the event queue time to process
+ time.sleep(0.3)
+
+ self.failUnless(results == ['foo', 'bar'],
+ 'Incomplete command workflow: %s' % results)
+
+ def testClientAPIError(self):
+ """Test using client-side error API for commands."""
+ results = []
+
+ def handle_error(iq, session):
+ for item in session['custom_data']:
+ results.append(item)
+
+ session = {'custom_data': ['foo'],
+ 'error': handle_error}
+
+ self.xmpp['xep_0050'].start_command(
+ 'foo@example.com',
+ 'test_client',
+ session)
+
+ self.send("""
+ <iq id="1" to="foo@example.com" type="set">
+ <command xmlns="http://jabber.org/protocol/commands"
+ node="test_client"
+ action="execute" />
+ </iq>
+ """)
+
+ self.recv("""
+ <iq id="1" to="foo@example.com" type="error">
+ <command xmlns="http://jabber.org/protocol/commands"
+ node="test_client"
+ action="execute" />
+ <error type='cancel'>
+ <item-not-found xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/>
+ </error>
+ </iq>
+ """)
+
+ # Give the event queue time to process
+ time.sleep(0.3)
+
+ self.failUnless(results == ['foo'],
+ 'Incomplete command workflow: %s' % results)
+
+ def testClientAPIErrorStrippedResponse(self):
+ """Test errors that don't include the command substanza."""
+ results = []
+
+ def handle_error(iq, session):
+ for item in session['custom_data']:
+ results.append(item)
+
+ session = {'custom_data': ['foo'],
+ 'error': handle_error}
+
+ self.xmpp['xep_0050'].start_command(
+ 'foo@example.com',
+ 'test_client',
+ session)
+
+ self.send("""
+ <iq id="1" to="foo@example.com" type="set">
+ <command xmlns="http://jabber.org/protocol/commands"
+ node="test_client"
+ action="execute" />
+ </iq>
+ """)
+
+ self.recv("""
+ <iq id="1" to="foo@example.com" type="error">
+ <error type='cancel'>
+ <item-not-found xmlns='urn:ietf:params:xml:ns:xmpp-stanzas' />
+ </error>
+ </iq>
+ """)
+
+ # Give the event queue time to process
+ time.sleep(0.3)
+
+ self.failUnless(results == ['foo'],
+ 'Incomplete command workflow: %s' % results)
+
+
+
+
+
+suite = unittest.TestLoader().loadTestsFromTestCase(TestAdHocCommands)
diff --git a/tests/test_stream_xep_0059.py b/tests/test_stream_xep_0059.py
new file mode 100644
index 00000000..3a99842b
--- /dev/null
+++ b/tests/test_stream_xep_0059.py
@@ -0,0 +1,162 @@
+import threading
+
+from sleekxmpp.test import *
+from sleekxmpp.xmlstream import register_stanza_plugin
+from sleekxmpp.plugins.xep_0030 import DiscoItems
+from sleekxmpp.plugins.xep_0059 import ResultIterator, Set
+
+
+class TestStreamSet(SleekTest):
+
+ def setUp(self):
+ register_stanza_plugin(DiscoItems, Set)
+
+ def tearDown(self):
+ self.stream_close()
+
+ def iter(self, rev=False):
+ q = self.xmpp.Iq()
+ q['type'] = 'get'
+ it = ResultIterator(q, 'disco_items', '1', reverse=rev)
+ for i in it:
+ for j in i['disco_items']['items']:
+ self.items.append(j[0])
+
+ def testResultIterator(self):
+ self.items = []
+ self.stream_start(mode='client')
+ t = threading.Thread(target=self.iter)
+ t.start()
+ self.send("""
+ <iq type="get" id="2">
+ <query xmlns="http://jabber.org/protocol/disco#items">
+ <set xmlns="http://jabber.org/protocol/rsm">
+ <max>1</max>
+ </set>
+ </query>
+ </iq>
+ """)
+ self.recv("""
+ <iq type="result" id="2">
+ <query xmlns="http://jabber.org/protocol/disco#items">
+ <item jid="item1" />
+ <set xmlns="http://jabber.org/protocol/rsm">
+ <last>item1</last>
+ </set>
+ </query>
+ </iq>
+ """)
+ self.send("""
+ <iq type="get" id="3">
+ <query xmlns="http://jabber.org/protocol/disco#items">
+ <set xmlns="http://jabber.org/protocol/rsm">
+ <max>1</max>
+ <after>item1</after>
+ </set>
+ </query>
+ </iq>
+ """)
+ self.recv("""
+ <iq type="result" id="3">
+ <query xmlns="http://jabber.org/protocol/disco#items">
+ <item jid="item2" />
+ <set xmlns="http://jabber.org/protocol/rsm">
+ <last>item2</last>
+ </set>
+ </query>
+ </iq>
+ """)
+ self.send("""
+ <iq type="get" id="4">
+ <query xmlns="http://jabber.org/protocol/disco#items">
+ <set xmlns="http://jabber.org/protocol/rsm">
+ <max>1</max>
+ <after>item2</after>
+ </set>
+ </query>
+ </iq>
+ """)
+ self.recv("""
+ <iq type="result" id="4">
+ <query xmlns="http://jabber.org/protocol/disco#items">
+ <item jid="item2" />
+ <set xmlns="http://jabber.org/protocol/rsm">
+ </set>
+ </query>
+ </iq>
+ """)
+ t.join()
+ self.failUnless(self.items == ['item1', 'item2'])
+
+ def testResultIteratorReverse(self):
+ self.items = []
+ self.stream_start(mode='client')
+
+ t = threading.Thread(target=self.iter, args=(True,))
+ t.start()
+
+ self.send("""
+ <iq type="get" id="2">
+ <query xmlns="http://jabber.org/protocol/disco#items">
+ <set xmlns="http://jabber.org/protocol/rsm">
+ <max>1</max>
+ <before />
+ </set>
+ </query>
+ </iq>
+ """)
+ self.recv("""
+ <iq type="result" id="2">
+ <query xmlns="http://jabber.org/protocol/disco#items">
+ <item jid="item2" />
+ <set xmlns="http://jabber.org/protocol/rsm">
+ <first>item2</first>
+ </set>
+ </query>
+ </iq>
+ """)
+ self.send("""
+ <iq type="get" id="3">
+ <query xmlns="http://jabber.org/protocol/disco#items">
+ <set xmlns="http://jabber.org/protocol/rsm">
+ <max>1</max>
+ <before>item2</before>
+ </set>
+ </query>
+ </iq>
+ """)
+ self.recv("""
+ <iq type="result" id="3">
+ <query xmlns="http://jabber.org/protocol/disco#items">
+ <item jid="item1" />
+ <set xmlns="http://jabber.org/protocol/rsm">
+ <first>item1</first>
+ </set>
+ </query>
+ </iq>
+ """)
+ self.send("""
+ <iq type="get" id="4">
+ <query xmlns="http://jabber.org/protocol/disco#items">
+ <set xmlns="http://jabber.org/protocol/rsm">
+ <max>1</max>
+ <before>item1</before>
+ </set>
+ </query>
+ </iq>
+ """)
+ self.recv("""
+ <iq type="result" id="4">
+ <query xmlns="http://jabber.org/protocol/disco#items">
+ <item jid="item1" />
+ <set xmlns="http://jabber.org/protocol/rsm">
+ </set>
+ </query>
+ </iq>
+ """)
+
+ t.join()
+ self.failUnless(self.items == ['item2', 'item1'])
+
+
+suite = unittest.TestLoader().loadTestsFromTestCase(TestStreamSet)
diff --git a/tests/test_stream_xep_0060.py b/tests/test_stream_xep_0060.py
new file mode 100644
index 00000000..e0936660
--- /dev/null
+++ b/tests/test_stream_xep_0060.py
@@ -0,0 +1,794 @@
+import sys
+import time
+import threading
+
+from sleekxmpp.test import *
+from sleekxmpp.stanza.atom import AtomEntry
+from sleekxmpp.xmlstream import register_stanza_plugin
+
+
+class TestStreamPubsub(SleekTest):
+
+ """
+ Test using the XEP-0030 plugin.
+ """
+
+ def setUp(self):
+ self.stream_start()
+
+ def tearDown(self):
+ self.stream_close()
+
+ def testCreateInstantNode(self):
+ """Test creating an instant node"""
+ t = threading.Thread(name='create_node',
+ target=self.xmpp['xep_0060'].create_node,
+ args=('pubsub.example.com', None))
+ t.start()
+
+ self.send("""
+ <iq type="set" id="1" to="pubsub.example.com">
+ <pubsub xmlns="http://jabber.org/protocol/pubsub">
+ <create />
+ </pubsub>
+ </iq>
+ """)
+
+ self.recv("""
+ <iq type="result" id="1"
+ to="tester@localhost" from="pubsub.example.com">
+ <pubsub xmlns="http://jabber.org/protocol/pubsub">
+ <create node="25e3d37dabbab9541f7523321421edc5bfeb2dae" />
+ </pubsub>
+ </iq>
+ """)
+
+ t.join()
+
+ def testCreateNodeNoConfig(self):
+ """Test creating a node without a config"""
+ self.xmpp['xep_0060'].create_node(
+ 'pubsub.example.com',
+ 'princely_musings',
+ block=False)
+ self.send("""
+ <iq type="set" id="1" to="pubsub.example.com">
+ <pubsub xmlns="http://jabber.org/protocol/pubsub">
+ <create node="princely_musings" />
+ </pubsub>
+ </iq>
+ """)
+
+ def testCreateNodeConfig(self):
+ """Test creating a node with a config"""
+ form = self.xmpp['xep_0004'].stanza.Form()
+ form['type'] = 'submit'
+ form.add_field(var='pubsub#access_model', value='whitelist')
+
+ self.xmpp['xep_0060'].create_node(
+ 'pubsub.example.com',
+ 'princely_musings',
+ config=form, block=False)
+
+ self.send("""
+ <iq type="set" id="1" to="pubsub.example.com">
+ <pubsub xmlns="http://jabber.org/protocol/pubsub">
+ <create node="princely_musings" />
+ <configure>
+ <x xmlns="jabber:x:data" type="submit">
+ <field var="pubsub#access_model">
+ <value>whitelist</value>
+ </field>
+ <field var="FORM_TYPE">
+ <value>http://jabber.org/protocol/pubsub#node_config</value>
+ </field>
+ </x>
+ </configure>
+ </pubsub>
+ </iq>
+ """)
+
+ def testDeleteNode(self):
+ """Test deleting a node"""
+ self.xmpp['xep_0060'].delete_node(
+ 'pubsub.example.com',
+ 'some_node',
+ block=False)
+ self.send("""
+ <iq type="set" to="pubsub.example.com" id="1">
+ <pubsub xmlns="http://jabber.org/protocol/pubsub#owner">
+ <delete node="some_node" />
+ </pubsub>
+ </iq>
+ """)
+
+ def testSubscribeCase1(self):
+ """
+ Test subscribing to a node: Case 1:
+ No subscribee, default 'from' JID, bare JID
+ """
+ self.xmpp['xep_0060'].subscribe(
+ 'pubsub.example.com',
+ 'somenode',
+ block=False)
+ self.send("""
+ <iq type="set" id="1" to="pubsub.example.com">
+ <pubsub xmlns="http://jabber.org/protocol/pubsub">
+ <subscribe node="somenode" jid="tester@localhost" />
+ </pubsub>
+ </iq>
+ """)
+
+ def testSubscribeCase2(self):
+ """
+ Test subscribing to a node: Case 2:
+ No subscribee, given 'from' JID, bare JID
+ """
+ self.xmpp['xep_0060'].subscribe(
+ 'pubsub.example.com',
+ 'somenode',
+ ifrom='foo@comp.example.com/bar',
+ block=False)
+ self.send("""
+ <iq type="set" id="1"
+ to="pubsub.example.com" from="foo@comp.example.com/bar">
+ <pubsub xmlns="http://jabber.org/protocol/pubsub">
+ <subscribe node="somenode" jid="foo@comp.example.com" />
+ </pubsub>
+ </iq>
+ """)
+
+ def testSubscribeCase3(self):
+ """
+ Test subscribing to a node: Case 3:
+ No subscribee, given 'from' JID, full JID
+ """
+ self.xmpp['xep_0060'].subscribe(
+ 'pubsub.example.com',
+ 'somenode',
+ ifrom='foo@comp.example.com/bar',
+ bare=False,
+ block=False)
+ self.send("""
+ <iq type="set" id="1"
+ to="pubsub.example.com" from="foo@comp.example.com/bar">
+ <pubsub xmlns="http://jabber.org/protocol/pubsub">
+ <subscribe node="somenode" jid="foo@comp.example.com/bar" />
+ </pubsub>
+ </iq>
+ """)
+
+ def testSubscribeCase4(self):
+ """
+ Test subscribing to a node: Case 4:
+ No subscribee, no 'from' JID, full JID
+ """
+ self.stream_close()
+ self.stream_start(jid='tester@localhost/full')
+
+ self.xmpp['xep_0060'].subscribe(
+ 'pubsub.example.com',
+ 'somenode',
+ bare=False,
+ block=False)
+ self.send("""
+ <iq type="set" id="1"
+ to="pubsub.example.com">
+ <pubsub xmlns="http://jabber.org/protocol/pubsub">
+ <subscribe node="somenode" jid="tester@localhost/full" />
+ </pubsub>
+ </iq>
+ """)
+
+ def testSubscribeCase5(self):
+ """
+ Test subscribing to a node: Case 5:
+ Subscribee given
+ """
+ self.xmpp['xep_0060'].subscribe(
+ 'pubsub.example.com',
+ 'somenode',
+ subscribee='user@example.com/foo',
+ ifrom='foo@comp.example.com/bar',
+ block=False)
+ self.send("""
+ <iq type="set" id="1"
+ to="pubsub.example.com" from="foo@comp.example.com/bar">
+ <pubsub xmlns="http://jabber.org/protocol/pubsub">
+ <subscribe node="somenode" jid="user@example.com/foo" />
+ </pubsub>
+ </iq>
+ """)
+
+ def testSubscribeWithOptions(self):
+ """Test subscribing to a node, with options."""
+ opts = self.xmpp['xep_0004'].make_form()
+ opts.add_field(
+ var='FORM_TYPE',
+ value='http://jabber.org/protocol/pubsub#subscribe_options',
+ ftype='hidden')
+ opts.add_field(
+ var='pubsub#digest',
+ value=False,
+ ftype='boolean')
+ opts['type'] = 'submit'
+
+ self.xmpp['xep_0060'].subscribe(
+ 'pubsub.example.com',
+ 'somenode',
+ options=opts,
+ block=False)
+ self.send("""
+ <iq type="set" id="1" to="pubsub.example.com">
+ <pubsub xmlns="http://jabber.org/protocol/pubsub">
+ <subscribe node="somenode" jid="tester@localhost" />
+ <options>
+ <x xmlns="jabber:x:data" type="submit">
+ <field var="FORM_TYPE">
+ <value>http://jabber.org/protocol/pubsub#subscribe_options</value>
+ </field>
+ <field var="pubsub#digest">
+ <value>0</value>
+ </field>
+ </x>
+ </options>
+ </pubsub>
+ </iq>
+ """)
+
+ def testUnsubscribeCase1(self):
+ """
+ Test unsubscribing from a node: Case 1:
+ No subscribee, default 'from' JID, bare JID
+ """
+ self.xmpp['xep_0060'].unsubscribe(
+ 'pubsub.example.com',
+ 'somenode',
+ block=False)
+ self.send("""
+ <iq type="set" id="1" to="pubsub.example.com">
+ <pubsub xmlns="http://jabber.org/protocol/pubsub">
+ <unsubscribe node="somenode" jid="tester@localhost" />
+ </pubsub>
+ </iq>
+ """)
+
+ def testUnsubscribeCase2(self):
+ """
+ Test unsubscribing from a node: Case 2:
+ No subscribee, given 'from' JID, bare JID
+ """
+ self.xmpp['xep_0060'].unsubscribe(
+ 'pubsub.example.com',
+ 'somenode',
+ ifrom='foo@comp.example.com/bar',
+ block=False)
+ self.send("""
+ <iq type="set" id="1"
+ to="pubsub.example.com" from="foo@comp.example.com/bar">
+ <pubsub xmlns="http://jabber.org/protocol/pubsub">
+ <unsubscribe node="somenode" jid="foo@comp.example.com" />
+ </pubsub>
+ </iq>
+ """)
+
+ def testUnsubscribeCase3(self):
+ """
+ Test unsubscribing from a node: Case 3:
+ No subscribee, given 'from' JID, full JID
+ """
+ self.xmpp['xep_0060'].unsubscribe(
+ 'pubsub.example.com',
+ 'somenode',
+ ifrom='foo@comp.example.com/bar',
+ bare=False,
+ block=False)
+ self.send("""
+ <iq type="set" id="1"
+ to="pubsub.example.com" from="foo@comp.example.com/bar">
+ <pubsub xmlns="http://jabber.org/protocol/pubsub">
+ <unsubscribe node="somenode" jid="foo@comp.example.com/bar" />
+ </pubsub>
+ </iq>
+ """)
+
+ def testUnsubscribeCase4(self):
+ """
+ Test unsubscribing from a node: Case 4:
+ No subscribee, no 'from' JID, full JID
+ """
+ self.stream_close()
+ self.stream_start(jid='tester@localhost/full')
+
+ self.xmpp['xep_0060'].unsubscribe(
+ 'pubsub.example.com',
+ 'somenode',
+ bare=False,
+ block=False)
+ self.send("""
+ <iq type="set" id="1"
+ to="pubsub.example.com">
+ <pubsub xmlns="http://jabber.org/protocol/pubsub">
+ <unsubscribe node="somenode" jid="tester@localhost/full" />
+ </pubsub>
+ </iq>
+ """)
+
+ def testUnsubscribeCase5(self):
+ """
+ Test unsubscribing from a node: Case 5:
+ Subscribee given
+ """
+ self.xmpp['xep_0060'].unsubscribe(
+ 'pubsub.example.com',
+ 'somenode',
+ subscribee='user@example.com/foo',
+ ifrom='foo@comp.example.com/bar',
+ block=False)
+ self.send("""
+ <iq type="set" id="1"
+ to="pubsub.example.com" from="foo@comp.example.com/bar">
+ <pubsub xmlns="http://jabber.org/protocol/pubsub">
+ <unsubscribe node="somenode" jid="user@example.com/foo" />
+ </pubsub>
+ </iq>
+ """)
+
+ def testGetDefaultNodeConfig(self):
+ """Test retrieving the default node config for a pubsub service."""
+ self.xmpp['xep_0060'].get_node_config(
+ 'pubsub.example.com',
+ block=False)
+ self.send("""
+ <iq type="get" id="1" to="pubsub.example.com">
+ <pubsub xmlns="http://jabber.org/protocol/pubsub#owner">
+ <default />
+ </pubsub>
+ </iq>
+ """, use_values=False)
+
+ def testGetNodeConfig(self):
+ """Test getting the config for a given node."""
+ self.xmpp['xep_0060'].get_node_config(
+ 'pubsub.example.com',
+ 'somenode',
+ block=False)
+ self.send("""
+ <iq type="get" id="1" to="pubsub.example.com">
+ <pubsub xmlns="http://jabber.org/protocol/pubsub#owner">
+ <configure node="somenode" />
+ </pubsub>
+ </iq>
+ """, use_values=False)
+
+ def testSetNodeConfig(self):
+ """Test setting the configuration for a node."""
+ form = self.xmpp['xep_0004'].make_form()
+ form.add_field(var='FORM_TYPE', ftype='hidden',
+ value='http://jabber.org/protocol/pubsub#node_config')
+ form.add_field(var='pubsub#title', ftype='text-single',
+ value='This is awesome!')
+ form['type'] = 'submit'
+
+ self.xmpp['xep_0060'].set_node_config(
+ 'pubsub.example.com',
+ 'somenode',
+ form,
+ block=False)
+ self.send("""
+ <iq type="set" id="1" to="pubsub.example.com">
+ <pubsub xmlns="http://jabber.org/protocol/pubsub#owner">
+ <configure node="somenode">
+ <x xmlns="jabber:x:data" type="submit">
+ <field var="FORM_TYPE">
+ <value>http://jabber.org/protocol/pubsub#node_config</value>
+ </field>
+ <field var="pubsub#title">
+ <value>This is awesome!</value>
+ </field>
+ </x>
+ </configure>
+ </pubsub>
+ </iq>
+ """)
+
+ def testPublishNoItems(self):
+ """Test publishing no items (in order to generate events)"""
+ self.xmpp['xep_0060'].publish(
+ 'pubsub.example.com',
+ 'somenode',
+ block=False)
+ self.send("""
+ <iq type="set" id="1" to="pubsub.example.com">
+ <pubsub xmlns="http://jabber.org/protocol/pubsub">
+ <publish node="somenode" />
+ </pubsub>
+ </iq>
+ """)
+
+ def testPublishSingle(self):
+ """Test publishing a single item."""
+ payload = AtomEntry()
+ payload['title'] = 'Test'
+
+ register_stanza_plugin(self.xmpp['xep_0060'].stanza.Item, AtomEntry)
+
+ self.xmpp['xep_0060'].publish(
+ 'pubsub.example.com',
+ 'somenode',
+ id='id42',
+ payload=payload,
+ block=False)
+ self.send("""
+ <iq type="set" id="1" to="pubsub.example.com">
+ <pubsub xmlns="http://jabber.org/protocol/pubsub">
+ <publish node="somenode">
+ <item id="id42">
+ <entry xmlns="http://www.w3.org/2005/Atom">
+ <title>Test</title>
+ </entry>
+ </item>
+ </publish>
+ </pubsub>
+ </iq>
+ """)
+
+ def testPublishSingleOptions(self):
+ """Test publishing a single item, with options."""
+ payload = AtomEntry()
+ payload['title'] = 'Test'
+
+ register_stanza_plugin(self.xmpp['xep_0060'].stanza.Item, AtomEntry)
+
+ options = self.xmpp['xep_0004'].make_form()
+ options.add_field(var='FORM_TYPE', ftype='hidden',
+ value='http://jabber.org/protocol/pubsub#publish-options')
+ options.add_field(var='pubsub#access_model', ftype='text-single',
+ value='presence')
+ options['type'] = 'submit'
+
+ self.xmpp['xep_0060'].publish(
+ 'pubsub.example.com',
+ 'somenode',
+ id='ID42',
+ payload=payload,
+ options=options,
+ block=False)
+ self.send("""
+ <iq type="set" id="1" to="pubsub.example.com">
+ <pubsub xmlns="http://jabber.org/protocol/pubsub">
+ <publish node="somenode">
+ <item id="ID42">
+ <entry xmlns="http://www.w3.org/2005/Atom">
+ <title>Test</title>
+ </entry>
+ </item>
+ </publish>
+ <publish-options>
+ <x xmlns="jabber:x:data" type="submit">
+ <field var="FORM_TYPE">
+ <value>http://jabber.org/protocol/pubsub#publish-options</value>
+ </field>
+ <field var="pubsub#access_model">
+ <value>presence</value>
+ </field>
+ </x>
+ </publish-options>
+ </pubsub>
+ </iq>
+ """, use_values=False)
+
+ def testRetract(self):
+ """Test deleting an item."""
+ self.xmpp['xep_0060'].retract(
+ 'pubsub.example.com',
+ 'somenode',
+ 'ID1',
+ notify=True,
+ block=False)
+ self.send("""
+ <iq type="set" id="1" to="pubsub.example.com">
+ <pubsub xmlns="http://jabber.org/protocol/pubsub">
+ <retract node="somenode" notify="true">
+ <item id="ID1" />
+ </retract>
+ </pubsub>
+ </iq>
+ """)
+
+ def testRetract(self):
+ """Test deleting an item."""
+ self.xmpp['xep_0060'].retract(
+ 'pubsub.example.com',
+ 'somenode',
+ 'ID1',
+ block=False)
+ self.send("""
+ <iq type="set" id="1" to="pubsub.example.com">
+ <pubsub xmlns="http://jabber.org/protocol/pubsub">
+ <retract node="somenode">
+ <item id="ID1" />
+ </retract>
+ </pubsub>
+ </iq>
+ """)
+
+ def testPurge(self):
+ """Test removing all items from a node."""
+ self.xmpp['xep_0060'].purge(
+ 'pubsub.example.com',
+ 'somenode',
+ block=False)
+ self.send("""
+ <iq type="set" id="1" to="pubsub.example.com">
+ <pubsub xmlns="http://jabber.org/protocol/pubsub#owner">
+ <purge node="somenode" />
+ </pubsub>
+ </iq>
+ """)
+
+ def testGetItem(self):
+ """Test retrieving a single item."""
+ self.xmpp['xep_0060'].get_item(
+ 'pubsub.example.com',
+ 'somenode',
+ 'id42',
+ block=False)
+ self.send("""
+ <iq type="get" id="1" to="pubsub.example.com">
+ <pubsub xmlns="http://jabber.org/protocol/pubsub">
+ <items node="somenode">
+ <item id="id42" />
+ </items>
+ </pubsub>
+ </iq>
+ """)
+
+ def testGetLatestItems(self):
+ """Test retrieving the most recent N items."""
+ self.xmpp['xep_0060'].get_items(
+ 'pubsub.example.com',
+ 'somenode',
+ max_items=3,
+ block=False)
+ self.send("""
+ <iq type="get" id="1" to="pubsub.example.com">
+ <pubsub xmlns="http://jabber.org/protocol/pubsub">
+ <items node="somenode" max_items="3" />
+ </pubsub>
+ </iq>
+ """)
+
+ def testGetAllItems(self):
+ """Test retrieving all items."""
+ self.xmpp['xep_0060'].get_items(
+ 'pubsub.example.com',
+ 'somenode',
+ block=False)
+ self.send("""
+ <iq type="get" id="1" to="pubsub.example.com">
+ <pubsub xmlns="http://jabber.org/protocol/pubsub">
+ <items node="somenode" />
+ </pubsub>
+ </iq>
+ """)
+
+ def testGetSpecificItems(self):
+ """Test retrieving a specific set of items."""
+ self.xmpp['xep_0060'].get_items(
+ 'pubsub.example.com',
+ 'somenode',
+ item_ids=['A', 'B', 'C'],
+ block=False)
+ self.send("""
+ <iq type="get" id="1" to="pubsub.example.com">
+ <pubsub xmlns="http://jabber.org/protocol/pubsub">
+ <items node="somenode">
+ <item id="A" />
+ <item id="B" />
+ <item id="C" />
+ </items>
+ </pubsub>
+ </iq>
+ """)
+
+ def testGetSubscriptionGlobalDefaultOptions(self):
+ """Test getting the subscription options for a node/JID."""
+ self.xmpp['xep_0060'].get_subscription_options(
+ 'pubsub.example.com',
+ block=False)
+ self.send("""
+ <iq type="get" id="1" to="pubsub.example.com">
+ <pubsub xmlns="http://jabber.org/protocol/pubsub">
+ <default />
+ </pubsub>
+ </iq>
+ """, use_values=False)
+
+ def testGetSubscriptionNodeDefaultOptions(self):
+ """Test getting the subscription options for a node/JID."""
+ self.xmpp['xep_0060'].get_subscription_options(
+ 'pubsub.example.com',
+ node='somenode',
+ block=False)
+ self.send("""
+ <iq type="get" id="1" to="pubsub.example.com">
+ <pubsub xmlns="http://jabber.org/protocol/pubsub">
+ <default node="somenode" />
+ </pubsub>
+ </iq>
+ """, use_values=False)
+
+ def testGetSubscriptionOptions(self):
+ """Test getting the subscription options for a node/JID."""
+ self.xmpp['xep_0060'].get_subscription_options(
+ 'pubsub.example.com',
+ 'somenode',
+ 'tester@localhost',
+ block=False)
+ self.send("""
+ <iq type="get" id="1" to="pubsub.example.com">
+ <pubsub xmlns="http://jabber.org/protocol/pubsub">
+ <options node="somenode" jid="tester@localhost" />
+ </pubsub>
+ </iq>
+ """, use_values=False)
+
+ def testSetSubscriptionOptions(self):
+ """Test setting the subscription options for a node/JID."""
+ opts = self.xmpp['xep_0004'].make_form()
+ opts.add_field(
+ var='FORM_TYPE',
+ value='http://jabber.org/protocol/pubsub#subscribe_options',
+ ftype='hidden')
+ opts.add_field(
+ var='pubsub#digest',
+ value=False,
+ ftype='boolean')
+ opts['type'] = 'submit'
+
+ self.xmpp['xep_0060'].set_subscription_options(
+ 'pubsub.example.com',
+ 'somenode',
+ 'tester@localhost',
+ opts,
+ block=False)
+ self.send("""
+ <iq type="get" id="1" to="pubsub.example.com">
+ <pubsub xmlns="http://jabber.org/protocol/pubsub">
+ <options node="somenode" jid="tester@localhost">
+ <x xmlns="jabber:x:data" type="submit">
+ <field var="FORM_TYPE">
+ <value>http://jabber.org/protocol/pubsub#subscribe_options</value>
+ </field>
+ <field var="pubsub#digest">
+ <value>0</value>
+ </field>
+ </x>
+ </options>
+ </pubsub>
+ </iq>
+ """)
+
+ def testGetNodeSubscriptions(self):
+ """Test retrieving all subscriptions for a node."""
+ self.xmpp['xep_0060'].get_node_subscriptions(
+ 'pubsub.example.com',
+ 'somenode',
+ block=False)
+ self.send("""
+ <iq type="get" id="1" to="pubsub.example.com">
+ <pubsub xmlns="http://jabber.org/protocol/pubsub#owner">
+ <subscriptions node="somenode" />
+ </pubsub>
+ </iq>
+ """)
+
+ def testGetSubscriptions(self):
+ """Test retrieving a users's subscriptions."""
+ self.xmpp['xep_0060'].get_subscriptions(
+ 'pubsub.example.com',
+ block=False)
+ self.send("""
+ <iq type="get" id="1" to="pubsub.example.com">
+ <pubsub xmlns="http://jabber.org/protocol/pubsub">
+ <subscriptions />
+ </pubsub>
+ </iq>
+ """)
+
+ def testGetSubscriptionsForNode(self):
+ """Test retrieving a users's subscriptions for a given node."""
+ self.xmpp['xep_0060'].get_subscriptions(
+ 'pubsub.example.com',
+ node='somenode',
+ block=False)
+ self.send("""
+ <iq type="get" id="1" to="pubsub.example.com">
+ <pubsub xmlns="http://jabber.org/protocol/pubsub">
+ <subscriptions node="somenode" />
+ </pubsub>
+ </iq>
+ """)
+
+ def testGetAffiliations(self):
+ """Test retrieving a users's affiliations."""
+ self.xmpp['xep_0060'].get_affiliations(
+ 'pubsub.example.com',
+ block=False)
+ self.send("""
+ <iq type="get" id="1" to="pubsub.example.com">
+ <pubsub xmlns="http://jabber.org/protocol/pubsub">
+ <affiliations />
+ </pubsub>
+ </iq>
+ """)
+
+ def testGetAffiliatinssForNode(self):
+ """Test retrieving a users's affiliations for a given node."""
+ self.xmpp['xep_0060'].get_affiliations(
+ 'pubsub.example.com',
+ node='somenode',
+ block=False)
+ self.send("""
+ <iq type="get" id="1" to="pubsub.example.com">
+ <pubsub xmlns="http://jabber.org/protocol/pubsub">
+ <affiliations node="somenode" />
+ </pubsub>
+ </iq>
+ """)
+
+ def testGetNodeAffiliations(self):
+ """Test getting the affiliations for a node."""
+ self.xmpp['xep_0060'].get_node_affiliations(
+ 'pubsub.example.com',
+ 'somenode',
+ block=False)
+ self.send("""
+ <iq type="get" id="1" to="pubsub.example.com">
+ <pubsub xmlns="http://jabber.org/protocol/pubsub#owner">
+ <affiliations node="somenode" />
+ </pubsub>
+ </iq>
+ """)
+
+ def testModifySubscriptions(self):
+ """Test owner modifying node subscriptions."""
+ self.xmpp['xep_0060'].modify_subscriptions(
+ 'pubsub.example.com',
+ 'somenode',
+ subscriptions=[('user@example.com', 'subscribed'),
+ ('foo@example.net', 'none')],
+ block=False)
+ self.send("""
+ <iq type="set" id="1" to="pubsub.example.com">
+ <pubsub xmlns="http://jabber.org/protocol/pubsub#owner">
+ <subscriptions node="somenode">
+ <subscription jid="user@example.com" subscription="subscribed" />
+ <subscription jid="foo@example.net" subscription="none" />
+ </subscriptions>
+ </pubsub>
+ </iq>
+ """)
+
+ def testModifyAffiliations(self):
+ """Test owner modifying node affiliations."""
+ self.xmpp['xep_0060'].modify_affiliations(
+ 'pubsub.example.com',
+ 'somenode',
+ affiliations=[('user@example.com', 'publisher'),
+ ('foo@example.net', 'none')],
+ block=False)
+ self.send("""
+ <iq type="set" id="1" to="pubsub.example.com">
+ <pubsub xmlns="http://jabber.org/protocol/pubsub#owner">
+ <affiliations node="somenode">
+ <affiliation jid="user@example.com" affiliation="publisher" />
+ <affiliation jid="foo@example.net" affiliation="none" />
+ </affiliations>
+ </pubsub>
+ </iq>
+ """)
+
+
+suite = unittest.TestLoader().loadTestsFromTestCase(TestStreamPubsub)
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_0085.py b/tests/test_stream_xep_0085.py
new file mode 100644
index 00000000..2a814805
--- /dev/null
+++ b/tests/test_stream_xep_0085.py
@@ -0,0 +1,59 @@
+import threading
+import time
+
+from sleekxmpp.test import *
+
+
+class TestStreamChatStates(SleekTest):
+
+ def tearDown(self):
+ self.stream_close()
+
+ def testChatStates(self):
+ self.stream_start(mode='client', plugins=['xep_0030', 'xep_0085'])
+
+ results = []
+
+ def handle_state(msg):
+ results.append(msg['chat_state'])
+
+ self.xmpp.add_event_handler('chatstate_active', handle_state)
+ self.xmpp.add_event_handler('chatstate_inactive', handle_state)
+ self.xmpp.add_event_handler('chatstate_paused', handle_state)
+ self.xmpp.add_event_handler('chatstate_gone', handle_state)
+ self.xmpp.add_event_handler('chatstate_composing', handle_state)
+
+ self.recv("""
+ <message>
+ <active xmlns="http://jabber.org/protocol/chatstates" />
+ </message>
+ """)
+ self.recv("""
+ <message>
+ <inactive xmlns="http://jabber.org/protocol/chatstates" />
+ </message>
+ """)
+ self.recv("""
+ <message>
+ <paused xmlns="http://jabber.org/protocol/chatstates" />
+ </message>
+ """)
+ self.recv("""
+ <message>
+ <composing xmlns="http://jabber.org/protocol/chatstates" />
+ </message>
+ """)
+ self.recv("""
+ <message>
+ <gone xmlns="http://jabber.org/protocol/chatstates" />
+ </message>
+ """)
+
+ # Give event queue time to process
+ time.sleep(0.3)
+ expected = ['active', 'inactive', 'paused', 'composing', 'gone']
+ self.failUnless(results == expected,
+ "Chat state event not handled: %s" % results)
+
+
+suite = unittest.TestLoader().loadTestsFromTestCase(TestStreamChatStates)
diff --git a/tests/test_stream_xep_0092.py b/tests/test_stream_xep_0092.py
new file mode 100644
index 00000000..4a038558
--- /dev/null
+++ b/tests/test_stream_xep_0092.py
@@ -0,0 +1,69 @@
+import threading
+
+from sleekxmpp.test import *
+
+
+class TestStreamSet(SleekTest):
+
+ def tearDown(self):
+ self.stream_close()
+
+ def testHandleSoftwareVersionRequest(self):
+ self.stream_start(mode='client', plugins=['xep_0030', 'xep_0092'])
+
+ self.xmpp['xep_0092'].name = 'SleekXMPP'
+ self.xmpp['xep_0092'].version = 'dev'
+ self.xmpp['xep_0092'].os = 'Linux'
+
+ self.recv("""
+ <iq type="get" id="1">
+ <query xmlns="jabber:iq:version" />
+ </iq>
+ """)
+
+ self.send("""
+ <iq type="result" id="1">
+ <query xmlns="jabber:iq:version">
+ <name>SleekXMPP</name>
+ <version>dev</version>
+ <os>Linux</os>
+ </query>
+ </iq>
+ """)
+
+ def testMakeSoftwareVersionRequest(self):
+ results = []
+
+ def query():
+ r = self.xmpp['xep_0092'].get_version('foo@bar')
+ results.append(r)
+
+ self.stream_start(mode='client', plugins=['xep_0030', 'xep_0092'])
+
+ t = threading.Thread(target=query)
+ t.start()
+
+ self.send("""
+ <iq type="get" id="1" to="foo@bar">
+ <query xmlns="jabber:iq:version" />
+ </iq>
+ """)
+
+ self.recv("""
+ <iq type="result" id="1" from="foo@bar" to="tester@localhost">
+ <query xmlns="jabber:iq:version">
+ <name>Foo</name>
+ <version>1.0</version>
+ <os>Linux</os>
+ </query>
+ </iq>
+ """)
+
+ t.join()
+
+ expected = [{'name': 'Foo', 'version': '1.0', 'os':'Linux'}]
+ self.assertEqual(results, expected,
+ "Did not receive expected results: %s" % results)
+
+
+suite = unittest.TestLoader().loadTestsFromTestCase(TestStreamSet)
diff --git a/tests/test_stream_xep_0128.py b/tests/test_stream_xep_0128.py
new file mode 100644
index 00000000..42fc9143
--- /dev/null
+++ b/tests/test_stream_xep_0128.py
@@ -0,0 +1,105 @@
+import sys
+import time
+import threading
+
+from sleekxmpp.test import *
+from sleekxmpp.xmlstream import ElementBase
+
+
+class TestStreamExtendedDisco(SleekTest):
+
+ """
+ Test using the XEP-0128 plugin.
+ """
+
+ def tearDown(self):
+ self.stream_close()
+
+ def testUsingExtendedInfo(self):
+ self.stream_start(mode='client',
+ jid='tester@localhost',
+ plugins=['xep_0030',
+ 'xep_0004',
+ 'xep_0128'])
+
+ form = self.xmpp['xep_0004'].makeForm(ftype='result')
+ form.addField(var='FORM_TYPE', ftype='hidden', value='testing')
+
+ info_ns = 'http://jabber.org/protocol/disco#info'
+ self.xmpp['xep_0030'].add_identity(node='test',
+ category='client',
+ itype='bot')
+ self.xmpp['xep_0030'].add_feature(node='test', feature=info_ns)
+ self.xmpp['xep_0128'].set_extended_info(node='test', data=form)
+
+ self.recv("""
+ <iq type="get" id="test" to="tester@localhost">
+ <query xmlns="http://jabber.org/protocol/disco#info"
+ node="test" />
+ </iq>
+ """)
+
+ self.send("""
+ <iq type="result" id="test">
+ <query xmlns="http://jabber.org/protocol/disco#info"
+ node="test">
+ <identity category="client" type="bot" />
+ <feature var="http://jabber.org/protocol/disco#info" />
+ <x xmlns="jabber:x:data" type="result">
+ <field var="FORM_TYPE" type="hidden">
+ <value>testing</value>
+ </field>
+ </x>
+ </query>
+ </iq>
+ """)
+
+ def testUsingMultipleExtendedInfo(self):
+ self.stream_start(mode='client',
+ jid='tester@localhost',
+ plugins=['xep_0030',
+ 'xep_0004',
+ 'xep_0128'])
+
+ form1 = self.xmpp['xep_0004'].makeForm(ftype='result')
+ form1.addField(var='FORM_TYPE', ftype='hidden', value='testing')
+
+ form2 = self.xmpp['xep_0004'].makeForm(ftype='result')
+ form2.addField(var='FORM_TYPE', ftype='hidden', value='testing_2')
+
+ info_ns = 'http://jabber.org/protocol/disco#info'
+ self.xmpp['xep_0030'].add_identity(node='test',
+ category='client',
+ itype='bot')
+ self.xmpp['xep_0030'].add_feature(node='test', feature=info_ns)
+ self.xmpp['xep_0128'].set_extended_info(node='test', data=[form1, form2])
+
+ self.recv("""
+ <iq type="get" id="test" to="tester@localhost">
+ <query xmlns="http://jabber.org/protocol/disco#info"
+ node="test" />
+ </iq>
+ """)
+
+ self.send("""
+ <iq type="result" id="test">
+ <query xmlns="http://jabber.org/protocol/disco#info"
+ node="test">
+ <identity category="client" type="bot" />
+ <feature var="http://jabber.org/protocol/disco#info" />
+ <x xmlns="jabber:x:data" type="result">
+ <field var="FORM_TYPE" type="hidden">
+ <value>testing</value>
+ </field>
+ </x>
+ <x xmlns="jabber:x:data" type="result">
+ <field var="FORM_TYPE" type="hidden">
+ <value>testing_2</value>
+ </field>
+ </x>
+ </query>
+ </iq>
+ """)
+
+
+suite = unittest.TestLoader().loadTestsFromTestCase(TestStreamExtendedDisco)
diff --git a/tests/test_stream_xep_0249.py b/tests/test_stream_xep_0249.py
new file mode 100644
index 00000000..9a25253f
--- /dev/null
+++ b/tests/test_stream_xep_0249.py
@@ -0,0 +1,63 @@
+import sys
+import time
+import threading
+
+from sleekxmpp.test import *
+from sleekxmpp.xmlstream import ElementBase
+
+
+class TestStreamDirectInvite(SleekTest):
+
+ """
+ Test using the XEP-0249 plugin.
+ """
+
+ def tearDown(self):
+ self.stream_close()
+
+ def testReceiveInvite(self):
+ self.stream_start(mode='client',
+ plugins=['xep_0030',
+ 'xep_0249'])
+
+ events = []
+
+ def handle_invite(msg):
+ events.append(True)
+
+ self.xmpp.add_event_handler('groupchat_direct_invite',
+ handle_invite)
+
+ self.recv("""
+ <message>
+ <x xmlns="jabber:x:conference"
+ jid="sleek@conference.jabber.org"
+ password="foo"
+ reason="For testing" />
+ </message>
+ """)
+
+ time.sleep(.5)
+
+ self.failUnless(events == [True],
+ "Event not raised: %s" % events)
+
+ def testSentDirectInvite(self):
+ self.stream_start(mode='client',
+ plugins=['xep_0030',
+ 'xep_0249'])
+
+ self.xmpp['xep_0249'].send_invitation('user@example.com',
+ 'sleek@conference.jabber.org',
+ reason='Need to test Sleek')
+
+ self.send("""
+ <message to="user@example.com">
+ <x xmlns="jabber:x:conference"
+ jid="sleek@conference.jabber.org"
+ reason="Need to test Sleek" />
+ </message>
+ """)
+
+
+suite = unittest.TestLoader().loadTestsFromTestCase(TestStreamDirectInvite)
diff --git a/tests/test_tostring.py b/tests/test_tostring.py
new file mode 100644
index 00000000..e456d28e
--- /dev/null
+++ b/tests/test_tostring.py
@@ -0,0 +1,132 @@
+from sleekxmpp.test import *
+from sleekxmpp.stanza import Message
+from sleekxmpp.xmlstream.stanzabase import ET, ElementBase
+from sleekxmpp.xmlstream.tostring import tostring, xml_escape
+
+
+class TestToString(SleekTest):
+
+ """
+ Test the implementation of sleekxmpp.xmlstream.tostring
+ """
+
+ def tearDown(self):
+ self.stream_close()
+
+ def tryTostring(self, original='', expected=None, message='', **kwargs):
+ """
+ Compare the result of calling tostring against an
+ expected result.
+ """
+ if not expected:
+ expected=original
+ if isinstance(original, str):
+ xml = ET.fromstring(original)
+ else:
+ xml=original
+ result = tostring(xml, **kwargs)
+ self.failUnless(result == expected, "%s: %s" % (message, result))
+
+ def testXMLEscape(self):
+ """Test escaping XML special characters."""
+ original = """<foo bar="baz">'Hi & welcome!'</foo>"""
+ escaped = xml_escape(original)
+ desired = """&lt;foo bar=&quot;baz&quot;&gt;&apos;Hi"""
+ desired += """ &amp; welcome!&apos;&lt;/foo&gt;"""
+
+ self.failUnless(escaped == desired,
+ "XML escaping did not work: %s." % escaped)
+
+ def testEmptyElement(self):
+ """Test converting an empty element to a string."""
+ self.tryTostring(
+ original='<bar xmlns="foo" />',
+ message="Empty element not serialized correctly")
+
+ def testEmptyElementWrapped(self):
+ """Test converting an empty element inside another element."""
+ self.tryTostring(
+ original='<bar xmlns="foo"><baz /></bar>',
+ message="Wrapped empty element not serialized correctly")
+
+ def testEmptyElementWrappedText(self):
+ """
+ Test converting an empty element wrapped with text
+ inside another element.
+ """
+ self.tryTostring(
+ original='<bar xmlns="foo">Some text. <baz /> More text.</bar>',
+ message="Text wrapped empty element serialized incorrectly")
+
+ def testMultipleChildren(self):
+ """Test converting multiple child elements to a Unicode string."""
+ self.tryTostring(
+ original='<bar xmlns="foo"><baz><qux /></baz><quux /></bar>',
+ message="Multiple child elements not serialized correctly")
+
+ def testXMLNS(self):
+ """
+ Test using xmlns tostring parameter, which will prevent adding
+ an xmlns attribute to the serialized element if the element's
+ namespace is the same.
+ """
+ self.tryTostring(
+ original='<bar xmlns="foo" />',
+ expected='<bar />',
+ message="The xmlns parameter was not used properly.",
+ xmlns='foo')
+
+ def testTailContent(self):
+ """
+ Test that elements of the form <a>foo <b>bar</b> baz</a> only
+ include " baz" once.
+ """
+ self.tryTostring(
+ original='<a>foo <b>bar</b> baz</a>',
+ message='Element tail content is incorrect.')
+
+
+ def testStanzaNs(self):
+ """
+ Test using the stanza_ns tostring parameter, which will prevent
+ adding an xmlns attribute to the serialized element if the
+ element's namespace is the same.
+ """
+ self.tryTostring(
+ original='<bar xmlns="foo" />',
+ expected='<bar />',
+ message="The stanza_ns parameter was not used properly.",
+ stanza_ns='foo')
+
+ def testStanzaStr(self):
+ """
+ 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 = self.Message()
+ msg['body'] = utf8_message.decode('utf-8')
+ expected = '<message><body>\xe0\xb2\xa0_\xe0\xb2\xa0</body></message>'
+ result = msg.__str__()
+ self.failUnless(result == expected,
+ "Stanza Unicode handling is incorrect: %s" % result)
+
+ def testXMLLang(self):
+ """Test that serializing xml:lang works."""
+
+ self.stream_start()
+
+ msg = self.Message()
+ msg._set_attr('{%s}lang' % msg.xml_ns, "no")
+
+ expected = '<message xml:lang="no" />'
+ result = msg.__str__()
+ self.failUnless(expected == result,
+ "Serialization with xml:lang failed: %s" % result)
+
+
+suite = unittest.TestLoader().loadTestsFromTestCase(TestToString)
diff --git a/tox.ini b/tox.ini
new file mode 100644
index 00000000..8576f4eb
--- /dev/null
+++ b/tox.ini
@@ -0,0 +1,5 @@
+[tox]
+envlist = py26,py27,py31,py32
+[testenv]
+deps = nose
+commands = nosetests --where=tests --exclude=live -i sleektest.py