Compare commits

...

131 Commits

Author SHA1 Message Date
Lance Stout
2a80824076 Remove extra debugging code that made it into a commit. 2011-08-23 14:14:21 -07:00
Lance Stout
f92f96325a Make Iq exceptions more discoverable and simpler to use.
IqError and IqTimeout now extend XMPPError, so if you don't care
about the difference, you can use:

    try:
        self.do_something_with_iqs()
    except XMPPError:
        # Error? Timeout? I don't care!
        pass

If you do need to distinguish between timeouts and error replies,
you can still continue to use:

    try:
        self.do_somethin_with_iqs()
    except IqError as err:
        pass
    except IqTimeout:
        pass

If you don't catch any Iq errors and you're processing a stanza
then an error response will be sent, just like normal if you raise
XMPPError or any other exception, except that the error messages
will be generic to prevent leaking too much information.
2011-08-19 01:04:20 -07:00
Lance Stout
b98555c512 Update the README
Now includes how to generate the docs, run tests, and the basic
boilerplate for Sleek based projects.
2011-08-18 16:32:06 -07:00
Lance Stout
e02a42a008 Route all unhandled exceptions through XMLStream.exception.
Or through an equivalent override.
2011-08-18 16:12:51 -07:00
Lance Stout
3e51126e18 PEP8 edits 2011-08-18 02:46:48 -07:00
Lance Stout
a714fa82b2 Remove extra, unhelpful presence debug log. 2011-08-18 02:46:08 -07:00
Lance Stout
e86e6eae81 Up the timeout to 30sec instead of 10sec. 2011-08-18 01:10:25 -07:00
Lance Stout
592c25f352 Merge branch 'develop' of github.com:fritzy/SleekXMPP into develop 2011-08-18 01:07:21 -07:00
Lance Stout
015f662249 Update examples to work with Python3 (raw_input vs input) 2011-08-18 01:06:59 -07:00
Lance Stout
8d998d71a3 Update README (renamed to README.rst so Github will render it) 2011-08-18 01:04:59 -07:00
Nathan Fritz
f75b6bf955 added inline documentation for new dns methods 2011-08-18 01:04:01 -07:00
Nathan Fritz
fb78bf0996 fixed manual address definition 2011-08-18 00:59:27 -07:00
Lance Stout
cd7cd30b4c Fix exceptions for Python3 2011-08-18 00:47:07 -07:00
Nathan Fritz
4ea22ff69b Merge branch 'develop' of github.com:fritzy/SleekXMPP into develop 2011-08-18 00:35:37 -07:00
Nathan Fritz
3853898ab3 DNS is now properly checked and different answers are tried for each reconnect until exhausted 2011-08-18 00:35:18 -07:00
Lance Stout
7d8aa4157b Add an example for dumping the roster to the command line. 2011-08-18 00:08:52 -07:00
Lance Stout
3fc20e10f5 Add some convenience methods to rosters.
Can now use len(self.client_roster) to get the number of JIDs in
the roster, and self.client_roster.groups() to get a dict of
groups and the JIDs in those groups.
2011-08-18 00:07:37 -07:00
Lance Stout
004eabf809 Update plugins that use Iq stanzas to work with new exceptions. 2011-08-17 21:30:47 -07:00
Lance Stout
62230fc970 Return '' instead of None from form fields with no values. 2011-08-17 21:22:03 -07:00
Lance Stout
961668d420 Add guide for sending a message and then disconnecting. 2011-08-17 21:21:37 -07:00
Lance Stout
01061a0355 More documentation!
Finished the echo bot quickstart.

Added placeholders for other guides we need.
2011-08-13 20:58:53 -07:00
Lance Stout
9fdd85d9f1 I've seen people complain about Sleek taking so long to disconnect.
Added logging to say that we're waiting for the server to end the stream
from its end.
2011-08-13 08:58:07 -07:00
Lance Stout
331db30f8f Add form.field back in for backwards compatibility. 2011-08-13 08:34:23 -07:00
Lance Stout
017d7ec62b Add tests for setting a form's type to 'submit' or 'cancel'.
Form fields now remember their current type if the type is deleted. This
allows for fields to properly format their values if set after the form
has been changed to the 'submit' type.
2011-08-13 01:28:18 -07:00
Lance Stout
76826b5495 The todo1.0 list isn't needed any longer :)
The last few plugins that were in the list will be cleaned up in 1.1
or 1.2 - XEP-0012, XEP-0009, XEP-0033, XEP-0045, and gmail_notify.
2011-08-13 00:16:36 -07:00
Lance Stout
de315ff6d8 Fix typo. 2011-08-13 00:16:25 -07:00
Lance Stout
c26b716164 Update XEP-0050 to use new IQ exceptions.
IqError is now caught and forwarded to the command error handler referenced
in the session.

Errors are now caught and processed by the session's error handler
whether or not the results Iq stanza includes the <command> substanza.

Added the option for blocking command calls. The blocking option is set
during start_command with block=True. Subsequent command flow methods use
session['block'] to determine their blocking behaviour.

If you use blocking commands, then you will need to wrap your command calls
in a try/except block for IqTimeout exceptions.
2011-08-13 00:10:06 -07:00
Lance Stout
dcaddb8042 Include new XEP-0004 directories in setup.py 2011-08-12 23:56:14 -07:00
Lance Stout
5ef197e5fd Start of docs for 1.0 2011-08-12 17:33:32 -07:00
Lance Stout
52ed02bd06 Update .gitignore 2011-08-12 17:22:27 -07:00
Lance Stout
bd427849fb Reduce the maximum delay between connection retries to 10min. 2011-08-12 17:17:05 -07:00
Lance Stout
127d7acb91 Missing commas in setup.py 2011-08-12 16:53:08 -07:00
Lance Stout
484efff156 Merge branch 'develop' into roster
Conflicts:
	setup.py
	sleekxmpp/clientxmpp.py
2011-08-12 16:47:58 -07:00
Nathan Fritz
8f1d0e7a79 Merge branch 'develop' of github.com:fritzy/SleekXMPP into develop 2011-08-12 16:36:03 -07:00
Nathan Fritz
88184ff955 fixed indenting and merged in exceptions branch 2011-08-12 16:35:36 -07:00
Nathan Fritz
bd8c110f00 Merge branch 'exceptions' into develop 2011-08-12 16:35:15 -07:00
Nathan Fritz
0050c51124 updated pubsub plugin to use stanzas 2011-08-12 16:32:09 -07:00
Lance Stout
9b7ed73f95 Reorganize XEP-0004.
Changes:
    May now use underscored method names
    form.field is replaced by form['fields']
    form.get_fields no longer accepts use_dict parameter, it always
        returns an OrderedDict now
    form.set_fields will accept either an OrderedDict, or a list
        of (var, dict) tuples as before.
    Setting the form type to 'submit' will remove extra meta data
        from the form fields, leaving just the 'var' and 'value'
    Setting the form type to 'cancel' will remove all fields.
2011-08-11 21:59:55 -07:00
Nathan Fritz
a189cb8333 Merge branch 'develop' of github.com:fritzy/SleekXMPP into develop 2011-08-10 13:37:49 -07:00
Nathan Fritz
0d4825d3ea added send_client example 2011-08-10 13:37:36 -07:00
Lance Stout
156b3200e3 Don't include ping stanza in the ping result. 2011-08-10 09:05:43 -07:00
Lance Stout
572becad44 Enable forcing a specififc SASL mech:
xmpp = ClientXMPP(jid, password, {
    'feature_mechanisms': {'use_mech':'PLAIN'}})
2011-08-09 00:51:49 -07:00
Lance Stout
75f23d1130 Fix XEP-0078 using the new stream feature workflow.
Honestly, this is mainly just a demo/proof of concept that we
can handle dependencies and ordering issues with stream features.

DON'T use XEP-0078 if you are able to use the normal SASL method,
which should be the case unless you are dealing with a very old
XMPP server implementation.
2011-08-06 12:30:56 -07:00
Lance Stout
e83fae3a6f Save the stream ID when the stream starts. 2011-08-06 00:44:32 -07:00
Lance Stout
5be5b8c02b If no config for a plugin is given, try using self.plugin_config.
Sleek loads a few plugins by default, which made it difficult to
configure or even disable them.

Now, if a plugin is registered without any configuration, then
sleek will try finding a configuration in self.plugin_config.
2011-08-06 00:41:14 -07:00
Lance Stout
6c4cb2bf00 Merge branch 'master' into develop
Adds hotfix for ANONYMOUS mech support.

Conflicts:
	sleekxmpp/__init__.py
2011-08-05 14:08:32 -07:00
Lance Stout
ea95811c4c The next release will be 1.0 RC1 2011-08-05 09:01:50 -07:00
Lance Stout
08cb5f42e7 Update the info in setup.py
I thought I had done this a long time ago, but it must have been in a
lost branch. *shrug*

It's too late for Beta6, so I've manually updated the PyPI entry.
2011-08-05 09:00:55 -07:00
Lance Stout
9abf37bbd1 Ignore Manifest and dist dir in git. 2011-08-05 09:00:42 -07:00
Lance Stout
89cffd43f4 Merge branch 'develop' into roster
Conflicts:
	setup.py
2011-08-04 11:52:17 -07:00
Lance Stout
d7fe724145 Merge branch 'develop' into exceptions 2011-07-27 19:36:04 -07:00
Lance Stout
ad978700fc Merge branch 'develop' into roster 2011-07-27 19:35:42 -07:00
Lance Stout
20df6348a4 Merge branch 'develop' into exceptions 2011-07-03 00:39:14 -07:00
Lance Stout
48fb7006f7 Merge branch 'develop' into roster 2011-07-03 00:39:02 -07:00
Lance Stout
7057984831 Merge branch 'develop' into roster 2011-07-01 15:19:05 -07:00
Lance Stout
847510c6b5 Merge branch 'develop' into exceptions 2011-06-20 16:27:39 -07:00
Lance Stout
774e0f2022 Merge branch 'develop' into roster 2011-06-20 16:27:25 -07:00
Lance Stout
d1e12cd46f Need to store unavailable presence as last sent if broadcasted. 2011-06-18 14:39:17 -07:00
Lance Stout
adf6d49fd1 Store unavailable presence as last sent presence. 2011-06-18 14:36:19 -07:00
Lance Stout
0826a44d4b Added roster package to setup.py 2011-06-18 14:32:09 -07:00
Lance Stout
ce145b04ac Integrate roster with ClientXMPP.
Roster updates are now passed through to the roster when using
self.update_roster, etc.
2011-06-16 16:09:15 -07:00
Lance Stout
29d775e675 Integrate roster with BaseXMPP.
Last sent stanzas are saved regardless of if the roster is used
directly or self.send_presence
2011-06-16 16:03:31 -07:00
Lance Stout
251a47db8c Split roster.py into a directory. 2011-06-16 14:15:22 -07:00
Lance Stout
e4b3a191d6 Merge branch 'develop' into roster
Conflicts:
	sleekxmpp/basexmpp.py
2011-06-15 10:57:25 -07:00
Lance Stout
393d702e77 Merge branch 'develop' into exceptions 2011-06-15 10:56:49 -07:00
Lance Stout
a3597d6deb Merge branch 'develop' into roster
Conflicts:
	sleekxmpp/basexmpp.py
2011-06-14 14:24:25 -07:00
Lance Stout
8fada4d015 Merge branch 'develop' into exceptions 2011-06-14 14:18:40 -07:00
Lance Stout
6d59f55fd4 Merge branch 'develop' into exceptions 2011-06-10 15:15:01 -07:00
Lance Stout
937dce8e65 Merge branch 'develop' into roster 2011-06-10 15:14:37 -07:00
Lance Stout
5c1562f36b Merge branch 'develop' into exceptions 2011-06-08 10:02:38 -07:00
Lance Stout
8eb59072b4 Merge branch 'develop' into roster 2011-06-08 10:01:52 -07:00
Lance Stout
20d053807d IqTimeout now references the original sent stanza.
A little extra bit of docs for IqError.
2011-06-01 15:28:33 -07:00
Lance Stout
8aa4396e44 Begin experimental use of exceptions.
Provides IqTimeout and IqError which are raised when an Iq response
does not arrive in time, or it arrives with type='error'.
2011-06-01 15:17:22 -07:00
Lance Stout
788a5b73f9 Merge branch 'develop' into roster 2011-06-01 15:17:04 -07:00
Lance Stout
5ed27bf5f6 Merge branch 'develop' into roster 2011-05-31 10:59:14 -07:00
Lance Stout
62bdaab7c7 Merge branch 'develop' into roster 2011-05-25 15:53:24 -07:00
Lance Stout
bb2bc64d15 Merge branch 'develop' into roster 2011-05-20 21:40:37 -04:00
Lance Stout
baa1eaf73a Fix test for Python3.
Issue of dict_keys vs list data types.
2011-05-20 21:36:09 -04:00
Lance Stout
4c7da3899e Merge branch 'develop' into roster
Conflicts:
	tests/test_stream_roster.py
2011-05-20 21:26:45 -04:00
Lance Stout
4d3593ac86 Merge branch 'develop' into roster
Conflicts:
	tests/test_stream_roster.py
2011-05-20 21:12:53 -04:00
Lance Stout
c49a8e9114 Save progress 2011-05-20 17:42:40 -04:00
Lance Stout
d3bd9cd31d Merge branch 'develop' into roster 2011-05-20 13:46:36 -04:00
Lance Stout
e3b14bc5a9 Merge branch 'develop' into roster
Conflicts:
	sleekxmpp/clientxmpp.py
	tests/test_stream_roster.py
2011-05-20 13:23:48 -04:00
Lance Stout
8e46aa7054 Merge branch 'develop' into roster 2011-04-26 16:33:32 -04:00
Nathan Fritz
8a22597180 added has_jid to roster 2011-04-15 17:43:12 -07:00
Nathan Fritz
e919906c8c Pubsub/Unsubscribe was not getting registered 2011-04-14 17:34:33 -07:00
Nathan Fritz
46dc6eac88 remove roster item state responsibility from clients 2011-04-14 16:27:27 -07:00
Lance Stout
b9bf30e095 Merge branch 'develop' into roster 2011-04-11 14:23:39 -04:00
Lance Stout
b60c51ef13 Merge branch 'develop' into roster 2011-04-08 16:52:20 -04:00
Lance Stout
6b05938573 Merge branch 'develop' into roster 2011-04-08 16:41:45 -04:00
Lance Stout
77601f7262 Merge branch 'develop' into roster 2011-03-24 13:15:00 -04:00
Lance Stout
d9be51b2ef Merge branch 'develop' into roster 2011-03-24 09:36:26 -04:00
Lance Stout
393259c24b Merge branch 'develop' into roster 2011-03-23 10:01:10 -04:00
Lance Stout
756c4c032f Merge branch 'develop' into roster 2011-03-22 20:48:09 -04:00
Lance Stout
e1360ae049 Merge branch 'develop' into roster 2011-03-22 12:00:01 -04:00
Lance Stout
b048f8d733 Merge branch 'develop' into roster 2011-03-18 17:36:35 -04:00
Lance Stout
f65f88325b Merge branch 'develop' into roster 2011-03-18 15:51:27 -04:00
Lance Stout
42c8f6ae87 Merge branch 'develop' into roster 2011-02-24 16:19:45 -05:00
Lance Stout
e4f3b777f9 Reset the roster on disconnect instead of replacing it. 2011-02-14 16:24:49 -05:00
Lance Stout
a278f79bdb Merge branch 'develop' into roster
Conflicts:
	sleekxmpp/clientxmpp.py
2011-02-14 16:18:44 -05:00
Lance Stout
606c369173 Some more roster tweaks. 2011-02-08 19:15:50 -05:00
Lance Stout
3c871920b1 Make the roster backend settable. 2011-02-02 12:05:34 -05:00
Lance Stout
de6170a13d Merge branch 'develop' into roster
Conflicts:
	sleekxmpp/basexmpp.py
2011-02-02 09:13:22 -05:00
Lance Stout
65931bb384 Merge branch 'develop' into roster 2011-01-13 22:16:46 -05:00
Lance Stout
a5d53b3349 Fix method call error. 2011-01-10 16:33:30 -05:00
Lance Stout
4487a90623 Add self.client_roster to make things simpler for clients. 2011-01-10 16:28:49 -05:00
Lance Stout
05da8cc3d1 Merge branch 'develop' into roster 2011-01-10 16:27:18 -05:00
Lance Stout
23e499998f Merge branch 'develop' into roster 2011-01-09 10:04:09 -05:00
Lance Stout
c156a4f723 Merge branch 'develop' into roster 2011-01-05 16:53:33 -05:00
Lance Stout
3657bf6636 Merge branch 'develop' into roster 2010-12-21 11:33:40 -05:00
Lance Stout
adade2e5ec Merge branch 'develop' into roster 2010-12-16 22:03:56 -05:00
Lance Stout
c16913c999 Merge branch 'develop' into roster
Conflicts:
	sleekxmpp/basexmpp.py
2010-12-13 14:36:53 -05:00
Lance Stout
12b61365ad Merge branch 'develop' of github.com:fritzy/SleekXMPP into roster 2010-11-18 01:18:06 -05:00
Lance Stout
9214dc6610 Add rename_node method to disco plugin. 2010-11-18 01:12:11 -05:00
Lance Stout
58b95e4ae4 Merge branch 'develop' of github.com:fritzy/SleekXMPP into roster 2010-11-18 00:44:51 -05:00
Lance Stout
debf909359 Merge branch 'develop' into roster 2010-11-17 14:33:09 -05:00
Lance Stout
0d4d84b2fa Cleaned basexmpp.py 2010-11-17 10:13:45 -05:00
Lance Stout
d2aca3e7e0 Remove extraneous files. 2010-11-17 02:04:36 -05:00
Lance Stout
26aca2b789 Merge branch 'roster' of github.com:fritzy/SleekXMPP into roster
Conflicts:
	sleekxmpp/basexmpp.py
	sleekxmpp/roster.py
	sleekxmpp/test/sleektest.py
	tests/test_stream_presence.py
	tests/test_stream_roster.py
2010-11-17 02:01:12 -05:00
Lance Stout
5424ede413 More cleanup. 2010-11-17 01:54:30 -05:00
Lance Stout
20112f8e16 More docs! 2010-11-17 01:49:51 -05:00
Lance Stout
4260a754e5 Added more docs. 2010-11-17 01:49:51 -05:00
Lance Stout
ce30f72738 Added docs for main Roster class. 2010-11-17 01:49:51 -05:00
Lance Stout
69d430dd75 Cleaned up names. 2010-11-17 01:49:51 -05:00
Lance Stout
673545c7e4 First pass at integrating the new roster manager. 2010-11-17 01:49:19 -05:00
Lance Stout
4f69a03bb1 More cleanup. 2010-10-27 15:02:21 -04:00
Lance Stout
c2c18acd6a More docs! 2010-10-27 13:36:23 -04:00
Lance Stout
12ead36f96 Added more docs. 2010-10-27 10:51:58 -04:00
Lance Stout
41a642e06c Added docs for main Roster class. 2010-10-27 09:27:00 -04:00
Lance Stout
c6ed4b8a1d Cleaned up names. 2010-10-27 08:09:50 -04:00
Lance Stout
65aa6573df First pass at integrating the new roster manager. 2010-10-26 23:47:17 -04:00
107 changed files with 8791 additions and 1522 deletions

4
.gitignore vendored
View File

@@ -1,2 +1,6 @@
*.pyc
build/
dist/
MANIFEST
docs/_build/
*.swp

View File

@@ -21,8 +21,8 @@ THE SOFTWARE.
Licences of Bundled Third Pary Code
-----------------------------------
Licenses of Bundled Third Party Code
------------------------------------
dateutil - Extensions to the standard python 2.3+ datetime module.
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

50
README
View File

@@ -1,50 +0,0 @@
SleekXMPP is an XMPP library written for Python 3.1+ (with 2.6 compatibility).
Hosted at http://wiki.github.com/fritzy/SleekXMPP/
Featured in examples in XMPP: The Definitive Guide by Kevin Smith, Remko Tronçon, and Peter Saint-Andre
If you're coming here from The Definitive Guide, please read http://wiki.github.com/fritzy/SleekXMPP/xmpp-the-definitive-guide
Requirements:
We try to keep requirements to a minimum, but we suggest that you install http://dnspython.org although it isn't strictly required.
If you do not install this library, you may need to specify the server/port for services that use SRV records (like GTalk).
"sudo pip install dnspython" on a *nix system with pip installed.
SleekXMPP has several design goals/philosophies:
- Low number of dependencies.
- Every XEP as a plugin.
- Rewarding to work with.
The goals for 1.0 include (and we're getting close):
- Nearly Full test coverage of stanzas.
- Wide range of functional tests.
- Stanza objects for all interaction with the stream
- Documentation on using and extending SleekXMPP.
- Complete documentation on all implemented stanza objects
- Documentation on all examples used in XMPP: The Definitive Guide
1.1 will include:
- More functional and unit tests
- PEP-8 compliance
- XEP-225 support
Since 0.2, here's the Changelog:
- MANY bugfixes
- Re-implementation of handlers/threading to greatly simplify and remove bugs (no more spawning threads in handlers)
- Stanza objects for jabber:client and all implemented XEPs
- Raising XMPPError for jabber:client and extended errors in handlers
- Robust error handling and better insurance of iq responses
- Stanza objects have made life a lot easier!
- Massive audit/cleanup.
Credits
----------------
Main Author: Nathan Fritz fritz@netflint.net
Contributors: Kevin Smith & Lance Stout
Patches: Remko Tronçon
Dave Cridland, for his Suelta SASL library.
Feel free to add fritzy@netflint.net to your roster for direct support and comments.
Join sleekxmpp-discussion@googlegroups.com / http://groups.google.com/group/sleekxmpp-discussion for email discussion.
Join sleek@conference.jabber.org for groupchat discussion.

169
README.rst Normal file
View File

@@ -0,0 +1,169 @@
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
------------
.. code-block:: sh
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.
**Stable Releases**
- `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>`_
**Develop Releases**
- `Latest Develop Version <http://github.com/fritzy/SleekXMPP/zipball/develop>`_
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.start)
self.add_event_handler("message", self.message)
def start(self, event):
self.send_presence()
# Most get_* 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.register_plugin('xep_0030') # Service Discovery
xmpp.register_plugin('xep_0199') # XMPP Ping
# If you are working with an OpenFire server, you will need
# to useuterborg Larsson version:
# xmppissl_version = ssl.PROTOCOL_SSLv3
if xmpp.connect():
xmpp.process(block=True)
else:
print("Unable to connect.")
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 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>`_)

130
docs/Makefile Normal file
View File

@@ -0,0 +1,130 @@
# Makefile for Sphinx documentation
#
# You can set these variables from the command line.
SPHINXOPTS =
SPHINXBUILD = sphinx-build
PAPER =
BUILDDIR = _build
# Internal variables.
PAPEROPT_a4 = -D latex_paper_size=a4
PAPEROPT_letter = -D latex_paper_size=letter
ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest
help:
@echo "Please use \`make <target>' where <target> is one of"
@echo " html to make standalone HTML files"
@echo " dirhtml to make HTML files named index.html in directories"
@echo " singlehtml to make a single large HTML file"
@echo " pickle to make pickle files"
@echo " json to make JSON files"
@echo " htmlhelp to make HTML files and a HTML help project"
@echo " qthelp to make HTML files and a qthelp project"
@echo " devhelp to make HTML files and a Devhelp project"
@echo " epub to make an epub"
@echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
@echo " latexpdf to make LaTeX files and run them through pdflatex"
@echo " text to make text files"
@echo " man to make manual pages"
@echo " changes to make an overview of all changed/added/deprecated items"
@echo " linkcheck to check all external links for integrity"
@echo " doctest to run all doctests embedded in the documentation (if enabled)"
clean:
-rm -rf $(BUILDDIR)/*
html:
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
dirhtml:
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
singlehtml:
$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
@echo
@echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
pickle:
$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
@echo
@echo "Build finished; now you can process the pickle files."
json:
$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
@echo
@echo "Build finished; now you can process the JSON files."
htmlhelp:
$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
@echo
@echo "Build finished; now you can run HTML Help Workshop with the" \
".hhp project file in $(BUILDDIR)/htmlhelp."
qthelp:
$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
@echo
@echo "Build finished; now you can run "qcollectiongenerator" with the" \
".qhcp project file in $(BUILDDIR)/qthelp, like this:"
@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/SleekXMPP.qhcp"
@echo "To view the help file:"
@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/SleekXMPP.qhc"
devhelp:
$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
@echo
@echo "Build finished."
@echo "To view the help file:"
@echo "# mkdir -p $$HOME/.local/share/devhelp/SleekXMPP"
@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/SleekXMPP"
@echo "# devhelp"
epub:
$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
@echo
@echo "Build finished. The epub file is in $(BUILDDIR)/epub."
latex:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo
@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
@echo "Run \`make' in that directory to run these through (pdf)latex" \
"(use \`make latexpdf' here to do that automatically)."
latexpdf:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo "Running LaTeX files through pdflatex..."
make -C $(BUILDDIR)/latex all-pdf
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
text:
$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
@echo
@echo "Build finished. The text files are in $(BUILDDIR)/text."
man:
$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
@echo
@echo "Build finished. The manual pages are in $(BUILDDIR)/man."
changes:
$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
@echo
@echo "The overview file is in $(BUILDDIR)/changes."
linkcheck:
$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
@echo
@echo "Link check complete; look for any errors in the above output " \
"or in $(BUILDDIR)/linkcheck/output.txt."
doctest:
$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
@echo "Testing of doctests in the sources finished, look at the " \
"results in $(BUILDDIR)/doctest/output.txt."

452
docs/_static/agogo.css vendored Normal file
View File

@@ -0,0 +1,452 @@
/*
* agogo.css_t
* ~~~~~~~~~~~
*
* Sphinx stylesheet -- agogo theme.
*
* :copyright: Copyright 2007-2011 by the Sphinx team, see AUTHORS.
* :license: BSD, see LICENSE for details.
*
*/
* {
margin: 0px;
padding: 0px;
}
body {
font-family: "Verdana", Arial, sans-serif;
line-height: 1.4em;
color: black;
background-color: #eeeeec;
}
/* Page layout */
div.header, div.content, div.footer {
width: 70em;
margin-left: auto;
margin-right: auto;
}
div.header-wrapper {
background: url(bgtop.png) top left repeat-x;
border-bottom: 3px solid #2e3436;
}
/* Default body styles */
a {
color: #ce5c00;
}
div.bodywrapper a, div.footer a {
text-decoration: underline;
}
.clearer {
clear: both;
}
.left {
float: left;
}
.right {
float: right;
}
.line-block {
display: block;
margin-top: 1em;
margin-bottom: 1em;
}
.line-block .line-block {
margin-top: 0;
margin-bottom: 0;
margin-left: 1.5em;
}
h1, h2, h3, h4 {
font-family: "Georgia", "Times New Roman", serif;
font-weight: normal;
color: #3465a4;
margin-bottom: .8em;
}
h1 {
color: #204a87;
}
h2 {
padding-bottom: .5em;
border-bottom: 1px solid #3465a4;
}
a.headerlink {
visibility: hidden;
color: #dddddd;
padding-left: .3em;
}
h1:hover > a.headerlink,
h2:hover > a.headerlink,
h3:hover > a.headerlink,
h4:hover > a.headerlink,
h5:hover > a.headerlink,
h6:hover > a.headerlink,
dt:hover > a.headerlink {
visibility: visible;
}
img {
border: 0;
}
div.admonition {
margin-top: 10px;
margin-bottom: 10px;
padding: 2px 7px 1px 7px;
border-left: 0.2em solid black;
}
p.admonition-title {
margin: 0px 10px 5px 0px;
font-weight: bold;
}
dt:target, .highlighted {
background-color: #fbe54e;
}
/* Header */
div.header {
padding-top: 10px;
padding-bottom: 10px;
}
div.header h1 {
font-family: "Georgia", "Times New Roman", serif;
font-weight: normal;
font-size: 180%;
letter-spacing: .08em;
}
div.header h1 a {
color: white;
}
div.header div.rel {
margin-top: 1em;
}
div.header div.rel a {
color: #fcaf3e;
letter-spacing: .1em;
text-transform: uppercase;
}
p.logo {
float: right;
}
img.logo {
border: 0;
}
/* Content */
div.content-wrapper {
background-color: white;
padding-top: 20px;
padding-bottom: 20px;
}
div.document {
width: 50em;
float: left;
}
div.body {
padding-right: 2em;
text-align: justify;
}
div.document ul {
margin: 1.5em;
list-style-type: square;
}
div.document dd {
margin-left: 1.2em;
margin-top: .4em;
margin-bottom: 1em;
}
div.document .section {
margin-top: 1.7em;
}
div.document .section:first-child {
margin-top: 0px;
}
div.document div.highlight {
padding: 3px;
background-color: #eeeeec;
border-top: 2px solid #dddddd;
border-bottom: 2px solid #dddddd;
margin-top: .8em;
margin-bottom: .8em;
}
div.document h2 {
margin-top: .7em;
}
div.document p {
margin-bottom: .5em;
}
div.document li.toctree-l1 {
margin-bottom: 1em;
}
div.document .descname {
font-weight: bold;
}
div.document .docutils.literal {
background-color: #eeeeec;
padding: 1px;
}
div.document .docutils.xref.literal {
background-color: transparent;
padding: 0px;
}
div.document blockquote {
margin: 1em;
}
div.document ol {
margin: 1.5em;
}
/* Sidebar */
div.sidebar {
width: 20em;
float: right;
font-size: .9em;
}
div.sidebar a, div.header a {
text-decoration: none;
}
div.sidebar a:hover, div.header a:hover {
text-decoration: underline;
}
div.sidebar h3 {
color: #2e3436;
text-transform: uppercase;
font-size: 130%;
letter-spacing: .1em;
}
div.sidebar ul {
list-style-type: none;
}
div.sidebar li.toctree-l1 a {
display: block;
padding: 1px;
border: 1px solid #dddddd;
background-color: #eeeeec;
margin-bottom: .4em;
padding-left: 3px;
color: #2e3436;
}
div.sidebar li.toctree-l2 a {
background-color: transparent;
border: none;
margin-left: 1em;
border-bottom: 1px solid #dddddd;
}
div.sidebar li.toctree-l3 a {
background-color: transparent;
border: none;
margin-left: 2em;
border-bottom: 1px solid #dddddd;
}
div.sidebar li.toctree-l2:last-child a {
border-bottom: none;
}
div.sidebar li.toctree-l1.current a {
border-right: 5px solid #fcaf3e;
}
div.sidebar li.toctree-l1.current li.toctree-l2 a {
border-right: none;
}
/* Footer */
div.footer-wrapper {
background: url(bgfooter.png) top left repeat-x;
border-top: 4px solid #babdb6;
padding-top: 10px;
padding-bottom: 10px;
min-height: 80px;
}
div.footer, div.footer a {
color: #888a85;
}
div.footer .right {
text-align: right;
}
div.footer .left {
text-transform: uppercase;
}
/* Styles copied from basic theme */
img.align-left, .figure.align-left, object.align-left {
clear: left;
float: left;
margin-right: 1em;
}
img.align-right, .figure.align-right, object.align-right {
clear: right;
float: right;
margin-left: 1em;
}
img.align-center, .figure.align-center, object.align-center {
display: block;
margin-left: auto;
margin-right: auto;
}
.align-left {
text-align: left;
}
.align-center {
clear: both;
text-align: center;
}
.align-right {
text-align: right;
}
/* -- search page ----------------------------------------------------------- */
ul.search {
margin: 10px 0 0 20px;
padding: 0;
}
ul.search li {
padding: 5px 0 5px 20px;
background-image: url(file.png);
background-repeat: no-repeat;
background-position: 0 7px;
}
ul.search li a {
font-weight: bold;
}
ul.search li div.context {
color: #888;
margin: 2px 0 0 30px;
text-align: left;
}
ul.keywordmatches li.goodmatch a {
font-weight: bold;
}
/* -- index page ------------------------------------------------------------ */
table.contentstable {
width: 90%;
}
table.contentstable p.biglink {
line-height: 150%;
}
a.biglink {
font-size: 1.3em;
}
span.linkdescr {
font-style: italic;
padding-top: 5px;
font-size: 90%;
}
/* -- general index --------------------------------------------------------- */
table.indextable td {
text-align: left;
vertical-align: top;
}
table.indextable dl, table.indextable dd {
margin-top: 0;
margin-bottom: 0;
}
table.indextable tr.pcap {
height: 10px;
}
table.indextable tr.cap {
margin-top: 10px;
background-color: #f2f2f2;
}
img.toggler {
margin-right: 3px;
margin-top: 3px;
cursor: pointer;
}
/* -- viewcode extension ---------------------------------------------------- */
.viewcode-link {
float: right;
}
.viewcode-back {
float: right;
font-family:: "Verdana", Arial, sans-serif;
}
div.viewcode-block:target {
margin: -1px -3px;
padding: 0 3px;
background-color: #f4debf;
border-top: 1px solid #ac9;
border-bottom: 1px solid #ac9;
}

532
docs/_static/basic.css vendored Normal file
View File

@@ -0,0 +1,532 @@
/*
* basic.css
* ~~~~~~~~~
*
* Sphinx stylesheet -- basic theme.
*
* :copyright: Copyright 2007-2011 by the Sphinx team, see AUTHORS.
* :license: BSD, see LICENSE for details.
*
*/
/* -- main layout ----------------------------------------------------------- */
div.clearer {
clear: both;
}
/* -- relbar ---------------------------------------------------------------- */
div.related {
width: 100%;
font-size: 90%;
}
div.related h3 {
display: none;
}
div.related ul {
margin: 0;
padding: 0 0 0 10px;
list-style: none;
}
div.related li {
display: inline;
}
div.related li.right {
float: right;
margin-right: 5px;
}
/* -- sidebar --------------------------------------------------------------- */
div.sphinxsidebarwrapper {
padding: 10px 5px 0 10px;
}
div.sphinxsidebar {
float: left;
width: 230px;
margin-left: -100%;
font-size: 90%;
}
div.sphinxsidebar ul {
list-style: none;
}
div.sphinxsidebar ul ul,
div.sphinxsidebar ul.want-points {
margin-left: 20px;
list-style: square;
}
div.sphinxsidebar ul ul {
margin-top: 0;
margin-bottom: 0;
}
div.sphinxsidebar form {
margin-top: 10px;
}
div.sphinxsidebar input {
border: 1px solid #98dbcc;
font-family: sans-serif;
font-size: 1em;
}
img {
border: 0;
}
/* -- search page ----------------------------------------------------------- */
ul.search {
margin: 10px 0 0 20px;
padding: 0;
}
ul.search li {
padding: 5px 0 5px 20px;
background-image: url(file.png);
background-repeat: no-repeat;
background-position: 0 7px;
}
ul.search li a {
font-weight: bold;
}
ul.search li div.context {
color: #888;
margin: 2px 0 0 30px;
text-align: left;
}
ul.keywordmatches li.goodmatch a {
font-weight: bold;
}
/* -- index page ------------------------------------------------------------ */
table.contentstable {
width: 90%;
}
table.contentstable p.biglink {
line-height: 150%;
}
a.biglink {
font-size: 1.3em;
}
span.linkdescr {
font-style: italic;
padding-top: 5px;
font-size: 90%;
}
/* -- general index --------------------------------------------------------- */
table.indextable {
width: 100%;
}
table.indextable td {
text-align: left;
vertical-align: top;
}
table.indextable dl, table.indextable dd {
margin-top: 0;
margin-bottom: 0;
}
table.indextable tr.pcap {
height: 10px;
}
table.indextable tr.cap {
margin-top: 10px;
background-color: #f2f2f2;
}
img.toggler {
margin-right: 3px;
margin-top: 3px;
cursor: pointer;
}
div.modindex-jumpbox {
border-top: 1px solid #ddd;
border-bottom: 1px solid #ddd;
margin: 1em 0 1em 0;
padding: 0.4em;
}
div.genindex-jumpbox {
border-top: 1px solid #ddd;
border-bottom: 1px solid #ddd;
margin: 1em 0 1em 0;
padding: 0.4em;
}
/* -- general body styles --------------------------------------------------- */
a.headerlink {
visibility: hidden;
}
h1:hover > a.headerlink,
h2:hover > a.headerlink,
h3:hover > a.headerlink,
h4:hover > a.headerlink,
h5:hover > a.headerlink,
h6:hover > a.headerlink,
dt:hover > a.headerlink {
visibility: visible;
}
div.body p.caption {
text-align: inherit;
}
div.body td {
text-align: left;
}
.field-list ul {
padding-left: 1em;
}
.first {
margin-top: 0 !important;
}
p.rubric {
margin-top: 30px;
font-weight: bold;
}
img.align-left, .figure.align-left, object.align-left {
clear: left;
float: left;
margin-right: 1em;
}
img.align-right, .figure.align-right, object.align-right {
clear: right;
float: right;
margin-left: 1em;
}
img.align-center, .figure.align-center, object.align-center {
display: block;
margin-left: auto;
margin-right: auto;
}
.align-left {
text-align: left;
}
.align-center {
clear: both;
text-align: center;
}
.align-right {
text-align: right;
}
/* -- sidebars -------------------------------------------------------------- */
div.sidebar {
margin: 0 0 0.5em 1em;
border: 1px solid #ddb;
padding: 7px 7px 0 7px;
background-color: #efefef;
width: 40%;
float: right;
-mox-border-radius: 4px;
-webkit-border-radius: 4px;
border-radius: 4px;
}
p.sidebar-title {
font-weight: bold;
text-transform: uppercase;
}
/* -- topics ---------------------------------------------------------------- */
div.topic {
border: 1px solid #ccc;
padding: 7px 7px 0 7px;
margin: 10px 0 10px 0;
}
p.topic-title {
font-size: 1.1em;
font-weight: bold;
margin-top: 10px;
}
/* -- admonitions ----------------------------------------------------------- */
div.admonition {
margin-top: 10px;
margin-bottom: 10px;
padding: 7px;
}
div.admonition dt {
font-weight: bold;
}
div.admonition dl {
margin-bottom: 0;
}
p.admonition-title {
margin: 0px 10px 5px 0px;
font-weight: bold;
}
div.body p.centered {
text-align: center;
margin-top: 25px;
}
/* -- tables ---------------------------------------------------------------- */
table.docutils {
border: 0;
border-collapse: collapse;
}
table.docutils td, table.docutils th {
padding: 1px 8px 1px 5px;
border-top: 0;
border-left: 0;
border-right: 0;
border-bottom: 1px solid #aaa;
}
table.field-list td, table.field-list th {
border: 0 !important;
}
table.footnote td, table.footnote th {
border: 0 !important;
}
th {
text-align: left;
padding-right: 5px;
}
table.citation {
border-left: solid 1px gray;
margin-left: 1px;
}
table.citation td {
border-bottom: none;
}
/* -- other body styles ----------------------------------------------------- */
ol.arabic {
list-style: decimal;
}
ol.loweralpha {
list-style: lower-alpha;
}
ol.upperalpha {
list-style: upper-alpha;
}
ol.lowerroman {
list-style: lower-roman;
}
ol.upperroman {
list-style: upper-roman;
}
dl {
margin-bottom: 15px;
}
dd p {
margin-top: 0px;
}
dd ul, dd table {
margin-bottom: 10px;
}
dd {
margin-top: 3px;
margin-bottom: 10px;
margin-left: 30px;
}
dt:target, .highlighted {
}
dl.glossary dt {
font-weight: bold;
font-size: 1.1em;
}
.field-list ul {
margin: 0;
padding-left: 1em;
}
.field-list p {
margin: 0;
}
.refcount {
color: #060;
}
.optional {
font-size: 1.3em;
}
.versionmodified {
font-style: italic;
}
.system-message {
background-color: #fda;
padding: 5px;
border: 3px solid red;
}
.footnote:target {
background-color: #ffa;
}
.line-block {
display: block;
margin-top: 1em;
margin-bottom: 1em;
}
.line-block .line-block {
margin-top: 0;
margin-bottom: 0;
margin-left: 1.5em;
}
.guilabel, .menuselection {
font-family: sans-serif;
}
.accelerator {
text-decoration: underline;
}
.classifier {
font-style: oblique;
}
/* -- code displays --------------------------------------------------------- */
pre {
overflow: auto;
overflow-y: hidden; /* fixes display issues on Chrome browsers */
}
td.linenos pre {
padding: 5px 0px;
border: 0;
background-color: transparent;
color: #aaa;
}
table.highlighttable {
margin-left: 0.5em;
}
table.highlighttable td {
padding: 0 0.5em 0 0.5em;
}
tt.descname {
background-color: transparent;
font-weight: bold;
font-size: 1.2em;
}
tt.descclassname {
background-color: transparent;
}
tt.xref, a tt {
background-color: transparent;
font-weight: bold;
}
h1 tt, h2 tt, h3 tt, h4 tt, h5 tt, h6 tt {
background-color: transparent;
}
.viewcode-link {
float: right;
}
.viewcode-back {
float: right;
font-family: sans-serif;
}
div.viewcode-block:target {
margin: -1px -10px;
padding: 0 10px;
}
/* -- math display ---------------------------------------------------------- */
img.math {
vertical-align: middle;
}
div.body div.math p {
text-align: center;
}
span.eqno {
float: right;
}
/* -- printout stylesheet --------------------------------------------------- */
@media print {
div.document,
div.documentwrapper,
div.bodywrapper {
margin: 0 !important;
width: 100%;
}
div.sphinxsidebar,
div.related,
div.footer,
#top-link {
display: none;
}
}

256
docs/_static/default.css vendored Normal file
View File

@@ -0,0 +1,256 @@
/*
* default.css_t
* ~~~~~~~~~~~~~
*
* Sphinx stylesheet -- default theme.
*
* :copyright: Copyright 2007-2011 by the Sphinx team, see AUTHORS.
* :license: BSD, see LICENSE for details.
*
*/
@import url("basic.css");
/* -- page layout ----------------------------------------------------------- */
body {
font-family: sans-serif;
font-size: 100%;
background-color: #11303d;
color: #000;
margin: 0;
padding: 0;
}
div.document {
background-color: #1c4e63;
}
div.documentwrapper {
float: left;
width: 100%;
}
div.bodywrapper {
margin: 0 0 0 230px;
}
div.body {
background-color: #ffffff;
color: #000000;
padding: 0 20px 30px 20px;
}
div.footer {
color: #ffffff;
width: 100%;
padding: 9px 0 9px 0;
text-align: center;
font-size: 75%;
}
div.footer a {
color: #ffffff;
text-decoration: underline;
}
div.related {
background-color: #133f52;
line-height: 30px;
color: #ffffff;
}
div.related a {
color: #ffffff;
}
div.sphinxsidebar {
}
div.sphinxsidebar h3 {
font-family: 'Trebuchet MS', sans-serif;
color: #ffffff;
font-size: 1.4em;
font-weight: normal;
margin: 0;
padding: 0;
}
div.sphinxsidebar h3 a {
color: #ffffff;
}
div.sphinxsidebar h4 {
font-family: 'Trebuchet MS', sans-serif;
color: #ffffff;
font-size: 1.3em;
font-weight: normal;
margin: 5px 0 0 0;
padding: 0;
}
div.sphinxsidebar p {
color: #ffffff;
}
div.sphinxsidebar p.topless {
margin: 5px 10px 10px 10px;
}
div.sphinxsidebar ul {
margin: 10px;
padding: 0;
color: #ffffff;
}
div.sphinxsidebar a {
color: #98dbcc;
}
div.sphinxsidebar input {
border: 1px solid #98dbcc;
font-family: sans-serif;
font-size: 1em;
}
/* -- hyperlink styles ------------------------------------------------------ */
a {
color: #355f7c;
text-decoration: none;
}
a:visited {
color: #355f7c;
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
/* -- body styles ----------------------------------------------------------- */
div.body h1,
div.body h2,
div.body h3,
div.body h4,
div.body h5,
div.body h6 {
font-family: 'Trebuchet MS', sans-serif;
background-color: #f2f2f2;
font-weight: normal;
color: #20435c;
border-bottom: 1px solid #ccc;
margin: 20px -20px 10px -20px;
padding: 3px 0 3px 10px;
}
div.body h1 { margin-top: 0; font-size: 200%; }
div.body h2 { font-size: 160%; }
div.body h3 { font-size: 140%; }
div.body h4 { font-size: 120%; }
div.body h5 { font-size: 110%; }
div.body h6 { font-size: 100%; }
a.headerlink {
color: #c60f0f;
font-size: 0.8em;
padding: 0 4px 0 4px;
text-decoration: none;
}
a.headerlink:hover {
background-color: #c60f0f;
color: white;
}
div.body p, div.body dd, div.body li {
text-align: justify;
line-height: 130%;
}
div.admonition p.admonition-title + p {
display: inline;
}
div.admonition p {
margin-bottom: 5px;
}
div.admonition pre {
margin-bottom: 5px;
}
div.admonition ul, div.admonition ol {
margin-bottom: 5px;
}
div.note {
background-color: #eee;
border: 1px solid #ccc;
}
div.seealso {
background-color: #ffc;
border: 1px solid #ff6;
}
div.topic {
background-color: #eee;
}
div.warning {
background-color: #ffe4e4;
border: 1px solid #f66;
}
p.admonition-title {
display: inline;
}
p.admonition-title:after {
content: ":";
}
pre {
padding: 5px;
background-color: #eeffcc;
color: #333333;
line-height: 120%;
border: 1px solid #ac9;
border-left: none;
border-right: none;
}
tt {
background-color: #ecf0f3;
padding: 0 1px 0 1px;
font-size: 0.95em;
}
th {
background-color: #ede;
}
.warning tt {
background: #efc2c2;
}
.note tt {
background: #d6d6d6;
}
.viewcode-back {
font-family: sans-serif;
}
div.viewcode-block:target {
background-color: #f4debf;
border-top: 1px solid #ac9;
border-bottom: 1px solid #ac9;
}

BIN
docs/_static/fonts/Museo_Slab_500.otf vendored Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
docs/_static/fonts/OFLGoudyStMTT.ttf vendored Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

406
docs/_static/haiku.css vendored Normal file
View File

@@ -0,0 +1,406 @@
/*
* haiku.css_t
* ~~~~~~~~~~~
*
* Sphinx stylesheet -- haiku theme.
*
* Adapted from http://haiku-os.org/docs/Haiku-doc.css.
* Original copyright message:
*
* Copyright 2008-2009, Haiku. All rights reserved.
* Distributed under the terms of the MIT License.
*
* Authors:
* Francois Revol <revol@free.fr>
* Stephan Assmus <superstippi@gmx.de>
* Braden Ewing <brewin@gmail.com>
* Humdinger <humdingerb@gmail.com>
*
* :copyright: Copyright 2007-2011 by the Sphinx team, see AUTHORS.
* :license: BSD, see LICENSE for details.
*
*/
@import url("basic.css");
@font-face {
font-family: "Museo Slab";
font-weight: normal;
font-style: normal;
src: local("Museo Slab"),
url("fonts/Museo_Slab_500.otf") format("opentype");
}
@font-face {
font-family: "Yanone Kaffeesatz";
font-weight: bold;
font-style: normal;
src: local("Yanone Kaffeesatz"),
url("fonts/YanoneKaffeesatz-Bold.ttf") format("truetype");
}
@font-face {
font-family: "Yanone Kaffeesatz";
font-weight: lighter;
font-style: normal;
src: local("Yanone Kaffeesatz"),
url("fonts/YanoneKaffeesatz-Regular.ttf") format("truetype");
}
html {
margin: 0px;
padding: 0px;
background: #FFF url(header.png) top left repeat-x;
}
body {
line-height: 1.5;
margin: auto;
padding: 0px;
font-family: "Helvetica Neueu", Helvetica, sans-serif;
min-width: 59em;
max-width: 70em;
color: #444;
}
div.footer {
padding: 8px;
font-size: 11px;
text-align: center;
letter-spacing: 0.5px;
}
/* link colors and text decoration */
a:link {
font-weight: bold;
text-decoration: none;
color: #00ADEE;
}
a:visited {
font-weight: bold;
text-decoration: none;
color: #00ADEE;
}
a:hover, a:active {
text-decoration: underline;
color: #F46DBA;
}
/* Some headers act as anchors, don't give them a hover effect */
h1 a:hover, a:active {
text-decoration: none;
color: #CFCFCF;
}
h2 a:hover, a:active {
text-decoration: none;
color: #CFCFCF;
}
h3 a:hover, a:active {
text-decoration: none;
color: #CFCFCF;
}
h4 a:hover, a:active {
text-decoration: none;
color: #CFCFCF;
}
a.headerlink {
color: #a7ce38;
padding-left: 5px;
}
a.headerlink:hover {
color: #a7ce38;
}
/* basic text elements */
div.content {
margin-top: 20px;
margin-left: 40px;
margin-right: 40px;
margin-bottom: 50px;
font-size: 0.9em;
}
/* heading and navigation */
div.header {
position: relative;
margin-top: 125px;
height: 85px;
padding: 0 40px;
font-family: "Yanone Kaffeesatz";
}
div.header h1 {
font-size: 2.6em;
font-weight: normal;
letter-spacing: 1px;
color: #CFCFCF;
border: 0;
margin: 0;
padding-top: 15px;
font-family: "Yanone Kaffeesatz";
text-shadow: 1px 1px 1px rgba(175, 175, 175, .8);
font-variant: small-caps;
}
div.header h1 a {
font-weight: normal;
color: #00ADEE;
}
div.header h2 {
font-size: 1.3em;
font-weight: normal;
letter-spacing: 1px;
text-transform: uppercase;
color: #aaa;
border: 0;
margin-top: -3px;
padding: 0;
font-family: "Yanone Kaffeesatz";
}
div.header img.rightlogo {
float: right;
}
div.title {
font-size: 1.3em;
font-weight: bold;
color: #CFCFCF;
border-bottom: dotted thin #e0e0e0;
margin-bottom: 25px;
}
div.topnav {
position: relative;
z-index: 0;
}
div.topnav p {
margin-top: 0;
margin-left: 40px;
margin-right: 40px;
margin-bottom: 0px;
text-align: right;
font-size: 0.8em;
}
div.bottomnav {
background: #eeeeee;
}
div.bottomnav p {
margin-right: 40px;
text-align: right;
font-size: 0.8em;
}
a.uplink {
font-weight: normal;
}
/* contents box */
table.index {
margin: 0px 0px 30px 30px;
padding: 1px;
border-width: 1px;
border-style: dotted;
border-color: #e0e0e0;
}
table.index tr.heading {
background-color: #e0e0e0;
text-align: center;
font-weight: bold;
font-size: 1.1em;
}
table.index tr.index {
background-color: #eeeeee;
}
table.index td {
padding: 5px 20px;
}
table.index a:link, table.index a:visited {
font-weight: normal;
text-decoration: none;
color: #4A7389;
}
table.index a:hover, table.index a:active {
text-decoration: underline;
color: #ff4500;
}
/* Haiku User Guide styles and layout */
/* Rounded corner boxes */
/* Common declarations */
div.admonition {
-webkit-border-radius: 10px;
-khtml-border-radius: 10px;
-moz-border-radius: 10px;
border-radius: 10px;
border-style: dotted;
border-width: thin;
border-color: #dcdcdc;
padding: 10px 15px 10px 15px;
margin-bottom: 15px;
margin-top: 15px;
}
div.note {
padding: 10px 15px 10px 15px;
background-color: #e4ffde;
/*background: #e4ffde url(alert_info_32.png) 15px 15px no-repeat;*/
min-height: 42px;
}
div.warning {
padding: 10px 15px 10px 15px;
background-color: #fffbc6;
/*background: #fffbc6 url(alert_warning_32.png) 15px 15px no-repeat;*/
min-height: 42px;
}
div.seealso {
background: #e4ffde;
}
/* More layout and styles */
h1 {
font-size: 1.6em;
color: #aaa;
border-bottom: dotted thin #e0e0e0;
margin-top: 30px;
font-family: "Museo Slab";
text-shadow: 1px 1px 1px rgba(175, 175, 175, .25);
}
h2 {
font-size: 1.5em;
font-weight: normal;
color: #aaa;
border-bottom: dotted thin #e0e0e0;
margin-top: 30px;
font-family: "Museo Slab";
text-shadow: 1px 1px 1px rgba(175, 175, 175, .25);
}
h3 {
font-size: 1.4em;
font-weight: normal;
color: #aaa;
margin-top: 30px;
font-family: "Museo Slab";
text-shadow: 1px 1px 1px rgba(175, 175, 175, .25);
}
h4 {
font-size: 1.3em;
font-weight: normal;
color: #CFCFCF;
margin-top: 30px;
}
p {
text-align: justify;
}
p.last {
margin-bottom: 0;
}
ol {
padding-left: 20px;
}
ul {
padding-left: 5px;
margin-top: 3px;
}
li {
line-height: 1.3;
}
div.content ul > li {
-moz-background-clip:border;
-moz-background-inline-policy:continuous;
-moz-background-origin:padding;
background: transparent url(bullet_orange.png) no-repeat scroll left 0.45em;
list-style-image: none;
list-style-type: none;
padding: 0 0 0 1.666em;
margin-bottom: 3px;
}
td {
vertical-align: top;
}
tt {
background-color: #e2e2e2;
font-size: 1.0em;
font-family: monospace;
}
pre {
font-size: 1.1em;
margin: 0 0 12px 0;
padding: 0.8em;
background-image: url(noise_dk.png);
background-color: #222;
}
hr {
border-top: 1px solid #ccc;
border-bottom: 0;
border-right: 0;
border-left: 0;
margin-bottom: 10px;
margin-top: 20px;
}
/* printer only pretty stuff */
@media print {
.noprint {
display: none;
}
/* for acronyms we want their definitions inlined at print time */
acronym[title]:after {
font-size: small;
content: " (" attr(title) ")";
font-style: italic;
}
/* and not have mozilla dotted underline */
acronym {
border: none;
}
div.topnav, div.bottomnav, div.header, table.index {
display: none;
}
div.content {
margin: 0px;
padding: 0px;
}
html {
background: #FFF;
}
}
.viewcode-back {
font-family: "DejaVu Sans", Arial, Helvetica, sans-serif;
}
div.viewcode-block:target {
border-top: 1px solid #ac9;
border-bottom: 1px solid #ac9;
margin: -1px -12px;
padding: 0 12px;
}

BIN
docs/_static/header.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
docs/_static/images/arch_layers.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

70
docs/_static/ir_black.css vendored Normal file
View File

@@ -0,0 +1,70 @@
.highlight .hll { background-color: #ffffcc }
.highlight { background: #000000; color: #f6f3e8; }
.highlight .c { color: #7C7C7C; } /* Comment */
.highlight .err { color: #f6f3e8; } /* Error */
.highlight .g { color: #f6f3e8; } /* Generic */
.highlight .k { color: #00ADEE; } /* Keyword */
.highlight .l { color: #f6f3e8; } /* Literal */
.highlight .n { color: #f6f3e8; } /* Name */
.highlight .o { color: #f6f3e8; } /* Operator */
.highlight .x { color: #f6f3e8; } /* Other */
.highlight .p { color: #f6f3e8; } /* Punctuation */
.highlight .cm { color: #7C7C7C; } /* Comment.Multiline */
.highlight .cp { color: #96CBFE; } /* Comment.Preproc */
.highlight .c1 { color: #7C7C7C; } /* Comment.Single */
.highlight .cs { color: #7C7C7C; } /* Comment.Special */
.highlight .gd { color: #f6f3e8; } /* Generic.Deleted */
.highlight .ge { color: #f6f3e8; } /* Generic.Emph */
.highlight .gr { color: #ffffff; background-color: #ff0000 } /* Generic.Error */
.highlight .gh { color: #f6f3e8; font-weight: bold; } /* Generic.Heading */
.highlight .gi { color: #f6f3e8; } /* Generic.Inserted */
.highlight .go { color: #070707; } /* Generic.Output */
.highlight .gp { color: #f6f3e8; } /* Generic.Prompt */
.highlight .gs { color: #f6f3e8; } /* Generic.Strong */
.highlight .gu { color: #f6f3e8; font-weight: bold; } /* Generic.Subheading */
.highlight .gt { color: #ffffff; font-weight: bold; background-color: #FF6C60 } /* Generic.Traceback */
.highlight .kc { color: #6699CC; } /* Keyword.Constant */
.highlight .kd { color: #6699CC; } /* Keyword.Declaration */
.highlight .kn { color: #6699CC; } /* Keyword.Namespace */
.highlight .kp { color: #6699CC; } /* Keyword.Pseudo */
.highlight .kr { color: #6699CC; } /* Keyword.Reserved */
.highlight .kt { color: #FFFFB6; } /* Keyword.Type */
.highlight .ld { color: #f6f3e8; } /* Literal.Date */
.highlight .m { color: #FF73FD; } /* Literal.Number */
.highlight .s { color: #F46DBA;/*#A8FF60;*/ } /* Literal.String */
.highlight .na { color: #f6f3e8; } /* Name.Attribute */
.highlight .nb { color: #f6f3e8; } /* Name.Builtin */
.highlight .nc { color: #f6f3e8; } /* Name.Class */
.highlight .no { color: #99CC99; } /* Name.Constant */
.highlight .nd { color: #f6f3e8; } /* Name.Decorator */
.highlight .ni { color: #E18964; } /* Name.Entity */
.highlight .ne { color: #f6f3e8; } /* Name.Exception */
.highlight .nf { color: #F64DBA; } /* Name.Function */
.highlight .nl { color: #f6f3e8; } /* Name.Label */
.highlight .nn { color: #f6f3e8; } /* Name.Namespace */
.highlight .nx { color: #f6f3e8; } /* Name.Other */
.highlight .py { color: #f6f3e8; } /* Name.Property */
.highlight .nt { color: #00ADEE; } /* Name.Tag */
.highlight .nv { color: #C6C5FE; } /* Name.Variable */
.highlight .ow { color: #ffffff; } /* Operator.Word */
.highlight .w { color: #f6f3e8; } /* Text.Whitespace */
.highlight .mf { color: #FF73FD; } /* Literal.Number.Float */
.highlight .mh { color: #FF73FD; } /* Literal.Number.Hex */
.highlight .mi { color: #FF73FD; } /* Literal.Number.Integer */
.highlight .mo { color: #FF73FD; } /* Literal.Number.Oct */
.highlight .sb { color: #A8FF60; } /* Literal.String.Backtick */
.highlight .sc { color: #A8FF60; } /* Literal.String.Char */
.highlight .sd { color: #A8FF60; } /* Literal.String.Doc */
.highlight .s2 { color: #A8FF60; } /* Literal.String.Double */
.highlight .se { color: #A8FF60; } /* Literal.String.Escape */
.highlight .sh { color: #A8FF60; } /* Literal.String.Heredoc */
.highlight .si { color: #A8FF60; } /* Literal.String.Interpol */
.highlight .sx { color: #A8FF60; } /* Literal.String.Other */
.highlight .sr { color: #A8FF60; } /* Literal.String.Regex */
.highlight .s1 { color: #A8FF60; } /* Literal.String.Single */
.highlight .ss { color: #A8FF60; } /* Literal.String.Symbol */
.highlight .bp { color: #f6f3e8; } /* Name.Builtin.Pseudo */
.highlight .vc { color: #C6C5FE; } /* Name.Variable.Class */
.highlight .vg { color: #C6C5FE; } /* Name.Variable.Global */
.highlight .vi { color: #C6C5FE; } /* Name.Variable.Instance */
.highlight .il { color: #FF73FD; } /* Literal.Number.Integer.Long */

245
docs/_static/nature.css vendored Normal file
View File

@@ -0,0 +1,245 @@
/*
* nature.css_t
* ~~~~~~~~~~~~
*
* Sphinx stylesheet -- nature theme.
*
* :copyright: Copyright 2007-2011 by the Sphinx team, see AUTHORS.
* :license: BSD, see LICENSE for details.
*
*/
@import url("basic.css");
/* -- page layout ----------------------------------------------------------- */
body {
font-family: Arial, sans-serif;
font-size: 100%;
background-color: #111;
color: #555;
margin: 0;
padding: 0;
}
div.documentwrapper {
float: left;
width: 100%;
}
div.bodywrapper {
margin: 0 0 0 230px;
}
hr {
border: 1px solid #B1B4B6;
}
div.document {
background-color: #eee;
}
div.body {
background-color: #ffffff;
color: #3E4349;
padding: 0 30px 30px 30px;
font-size: 0.9em;
}
div.footer {
color: #555;
width: 100%;
padding: 13px 0;
text-align: center;
font-size: 75%;
}
div.footer a {
color: #444;
text-decoration: underline;
}
div.related {
background-color: #6BA81E;
line-height: 32px;
color: #fff;
text-shadow: 0px 1px 0 #444;
font-size: 0.9em;
}
div.related a {
color: #E2F3CC;
}
div.sphinxsidebar {
font-size: 0.75em;
line-height: 1.5em;
}
div.sphinxsidebarwrapper{
padding: 20px 0;
}
div.sphinxsidebar h3,
div.sphinxsidebar h4 {
font-family: Arial, sans-serif;
color: #222;
font-size: 1.2em;
font-weight: normal;
margin: 0;
padding: 5px 10px;
background-color: #ddd;
text-shadow: 1px 1px 0 white
}
div.sphinxsidebar h4{
font-size: 1.1em;
}
div.sphinxsidebar h3 a {
color: #444;
}
div.sphinxsidebar p {
color: #888;
padding: 5px 20px;
}
div.sphinxsidebar p.topless {
}
div.sphinxsidebar ul {
margin: 10px 20px;
padding: 0;
color: #000;
}
div.sphinxsidebar a {
color: #444;
}
div.sphinxsidebar input {
border: 1px solid #ccc;
font-family: sans-serif;
font-size: 1em;
}
div.sphinxsidebar input[type=text]{
margin-left: 20px;
}
/* -- body styles ----------------------------------------------------------- */
a {
color: #005B81;
text-decoration: none;
}
a:hover {
color: #E32E00;
text-decoration: underline;
}
div.body h1,
div.body h2,
div.body h3,
div.body h4,
div.body h5,
div.body h6 {
font-family: Arial, sans-serif;
background-color: #BED4EB;
font-weight: normal;
color: #212224;
margin: 30px 0px 10px 0px;
padding: 5px 0 5px 10px;
text-shadow: 0px 1px 0 white
}
div.body h1 { border-top: 20px solid white; margin-top: 0; font-size: 200%; }
div.body h2 { font-size: 150%; background-color: #C8D5E3; }
div.body h3 { font-size: 120%; background-color: #D8DEE3; }
div.body h4 { font-size: 110%; background-color: #D8DEE3; }
div.body h5 { font-size: 100%; background-color: #D8DEE3; }
div.body h6 { font-size: 100%; background-color: #D8DEE3; }
a.headerlink {
color: #c60f0f;
font-size: 0.8em;
padding: 0 4px 0 4px;
text-decoration: none;
}
a.headerlink:hover {
background-color: #c60f0f;
color: white;
}
div.body p, div.body dd, div.body li {
line-height: 1.5em;
}
div.admonition p.admonition-title + p {
display: inline;
}
div.highlight{
background-color: white;
}
div.note {
background-color: #eee;
border: 1px solid #ccc;
}
div.seealso {
background-color: #ffc;
border: 1px solid #ff6;
}
div.topic {
background-color: #eee;
}
div.warning {
background-color: #ffe4e4;
border: 1px solid #f66;
}
p.admonition-title {
display: inline;
}
p.admonition-title:after {
content: ":";
}
pre {
padding: 10px;
background-color: White;
color: #222;
line-height: 1.2em;
border: 1px solid #C6C9CB;
font-size: 1.1em;
margin: 1.5em 0 1.5em 0;
-webkit-box-shadow: 1px 1px 1px #d8d8d8;
-moz-box-shadow: 1px 1px 1px #d8d8d8;
}
tt {
background-color: #ecf0f3;
color: #222;
/* padding: 1px 2px; */
font-size: 1.1em;
font-family: monospace;
}
.viewcode-back {
font-family: Arial, sans-serif;
}
div.viewcode-block:target {
background-color: #f4debf;
border-top: 1px solid #ac9;
border-bottom: 1px solid #ac9;
}

BIN
docs/_static/noise_dk.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

70
docs/_static/pygments.css vendored Normal file
View File

@@ -0,0 +1,70 @@
.highlight .hll { background-color: #ffffcc }
.highlight { background: #000000; color: #f6f3e8; }
.highlight .c { color: #7C7C7C; } /* Comment */
.highlight .err { color: #f6f3e8; } /* Error */
.highlight .g { color: #f6f3e8; } /* Generic */
.highlight .k { color: #00ADEE; } /* Keyword */
.highlight .l { color: #f6f3e8; } /* Literal */
.highlight .n { color: #f6f3e8; } /* Name */
.highlight .o { color: #f6f3e8; } /* Operator */
.highlight .x { color: #f6f3e8; } /* Other */
.highlight .p { color: #f6f3e8; } /* Punctuation */
.highlight .cm { color: #7C7C7C; } /* Comment.Multiline */
.highlight .cp { color: #96CBFE; } /* Comment.Preproc */
.highlight .c1 { color: #7C7C7C; } /* Comment.Single */
.highlight .cs { color: #7C7C7C; } /* Comment.Special */
.highlight .gd { color: #f6f3e8; } /* Generic.Deleted */
.highlight .ge { color: #f6f3e8; } /* Generic.Emph */
.highlight .gr { color: #ffffff; background-color: #ff0000 } /* Generic.Error */
.highlight .gh { color: #f6f3e8; font-weight: bold; } /* Generic.Heading */
.highlight .gi { color: #f6f3e8; } /* Generic.Inserted */
.highlight .go { color: #070707; } /* Generic.Output */
.highlight .gp { color: #f6f3e8; } /* Generic.Prompt */
.highlight .gs { color: #f6f3e8; } /* Generic.Strong */
.highlight .gu { color: #f6f3e8; font-weight: bold; } /* Generic.Subheading */
.highlight .gt { color: #ffffff; font-weight: bold; background-color: #FF6C60 } /* Generic.Traceback */
.highlight .kc { color: #6699CC; } /* Keyword.Constant */
.highlight .kd { color: #6699CC; } /* Keyword.Declaration */
.highlight .kn { color: #6699CC; } /* Keyword.Namespace */
.highlight .kp { color: #6699CC; } /* Keyword.Pseudo */
.highlight .kr { color: #6699CC; } /* Keyword.Reserved */
.highlight .kt { color: #FFFFB6; } /* Keyword.Type */
.highlight .ld { color: #f6f3e8; } /* Literal.Date */
.highlight .m { color: #FF73FD; } /* Literal.Number */
.highlight .s { color: #F46DBA;/*#A8FF60;*/ } /* Literal.String */
.highlight .na { color: #f6f3e8; } /* Name.Attribute */
.highlight .nb { color: #f6f3e8; } /* Name.Builtin */
.highlight .nc { color: #f6f3e8; } /* Name.Class */
.highlight .no { color: #99CC99; } /* Name.Constant */
.highlight .nd { color: #f6f3e8; } /* Name.Decorator */
.highlight .ni { color: #E18964; } /* Name.Entity */
.highlight .ne { color: #f6f3e8; } /* Name.Exception */
.highlight .nf { color: #F64DBA; } /* Name.Function */
.highlight .nl { color: #f6f3e8; } /* Name.Label */
.highlight .nn { color: #f6f3e8; } /* Name.Namespace */
.highlight .nx { color: #f6f3e8; } /* Name.Other */
.highlight .py { color: #f6f3e8; } /* Name.Property */
.highlight .nt { color: #00ADEE; } /* Name.Tag */
.highlight .nv { color: #C6C5FE; } /* Name.Variable */
.highlight .ow { color: #ffffff; } /* Operator.Word */
.highlight .w { color: #f6f3e8; } /* Text.Whitespace */
.highlight .mf { color: #FF73FD; } /* Literal.Number.Float */
.highlight .mh { color: #FF73FD; } /* Literal.Number.Hex */
.highlight .mi { color: #FF73FD; } /* Literal.Number.Integer */
.highlight .mo { color: #FF73FD; } /* Literal.Number.Oct */
.highlight .sb { color: #A8FF60; } /* Literal.String.Backtick */
.highlight .sc { color: #A8FF60; } /* Literal.String.Char */
.highlight .sd { color: #A8FF60; } /* Literal.String.Doc */
.highlight .s2 { color: #A8FF60; } /* Literal.String.Double */
.highlight .se { color: #A8FF60; } /* Literal.String.Escape */
.highlight .sh { color: #A8FF60; } /* Literal.String.Heredoc */
.highlight .si { color: #A8FF60; } /* Literal.String.Interpol */
.highlight .sx { color: #A8FF60; } /* Literal.String.Other */
.highlight .sr { color: #A8FF60; } /* Literal.String.Regex */
.highlight .s1 { color: #A8FF60; } /* Literal.String.Single */
.highlight .ss { color: #A8FF60; } /* Literal.String.Symbol */
.highlight .bp { color: #f6f3e8; } /* Name.Builtin.Pseudo */
.highlight .vc { color: #C6C5FE; } /* Name.Variable.Class */
.highlight .vg { color: #C6C5FE; } /* Name.Variable.Global */
.highlight .vi { color: #C6C5FE; } /* Name.Variable.Instance */
.highlight .il { color: #FF73FD; } /* Literal.Number.Integer.Long */

339
docs/_static/sphinxdoc.css vendored Normal file
View File

@@ -0,0 +1,339 @@
/*
* sphinxdoc.css_t
* ~~~~~~~~~~~~~~~
*
* Sphinx stylesheet -- sphinxdoc theme. Originally created by
* Armin Ronacher for Werkzeug.
*
* :copyright: Copyright 2007-2011 by the Sphinx team, see AUTHORS.
* :license: BSD, see LICENSE for details.
*
*/
@import url("basic.css");
/* -- page layout ----------------------------------------------------------- */
body {
font-family: 'Lucida Grande', 'Lucida Sans Unicode', 'Geneva',
'Verdana', sans-serif;
font-size: 14px;
letter-spacing: -0.01em;
line-height: 150%;
text-align: center;
background-color: #BFD1D4;
color: black;
padding: 0;
border: 1px solid #aaa;
margin: 0px 80px 0px 80px;
min-width: 740px;
}
div.document {
background-color: white;
text-align: left;
background-image: url(contents.png);
background-repeat: repeat-x;
}
div.bodywrapper {
margin: 0 240px 0 0;
border-right: 1px solid #ccc;
}
div.body {
margin: 0;
padding: 0.5em 20px 20px 20px;
}
div.related {
font-size: 1em;
}
div.related ul {
background-image: url(navigation.png);
height: 2em;
border-top: 1px solid #ddd;
border-bottom: 1px solid #ddd;
}
div.related ul li {
margin: 0;
padding: 0;
height: 2em;
float: left;
}
div.related ul li.right {
float: right;
margin-right: 5px;
}
div.related ul li a {
margin: 0;
padding: 0 5px 0 5px;
line-height: 1.75em;
color: #EE9816;
}
div.related ul li a:hover {
color: #3CA8E7;
}
div.sphinxsidebarwrapper {
padding: 0;
}
div.sphinxsidebar {
margin: 0;
padding: 0.5em 15px 15px 0;
width: 210px;
float: right;
font-size: 1em;
text-align: left;
}
div.sphinxsidebar h3, div.sphinxsidebar h4 {
margin: 1em 0 0.5em 0;
font-size: 1em;
padding: 0.1em 0 0.1em 0.5em;
color: white;
border: 1px solid #86989B;
background-color: #AFC1C4;
}
div.sphinxsidebar h3 a {
color: white;
}
div.sphinxsidebar ul {
padding-left: 1.5em;
margin-top: 7px;
padding: 0;
line-height: 130%;
}
div.sphinxsidebar ul ul {
margin-left: 20px;
}
div.footer {
background-color: #E3EFF1;
color: #86989B;
padding: 3px 8px 3px 0;
clear: both;
font-size: 0.8em;
text-align: right;
}
div.footer a {
color: #86989B;
text-decoration: underline;
}
/* -- body styles ----------------------------------------------------------- */
p {
margin: 0.8em 0 0.5em 0;
}
a {
color: #CA7900;
text-decoration: none;
}
a:hover {
color: #2491CF;
}
div.body a {
text-decoration: underline;
}
h1 {
margin: 0;
padding: 0.7em 0 0.3em 0;
font-size: 1.5em;
color: #11557C;
}
h2 {
margin: 1.3em 0 0.2em 0;
font-size: 1.35em;
padding: 0;
}
h3 {
margin: 1em 0 -0.3em 0;
font-size: 1.2em;
}
div.body h1 a, div.body h2 a, div.body h3 a, div.body h4 a, div.body h5 a, div.body h6 a {
color: black!important;
}
h1 a.anchor, h2 a.anchor, h3 a.anchor, h4 a.anchor, h5 a.anchor, h6 a.anchor {
display: none;
margin: 0 0 0 0.3em;
padding: 0 0.2em 0 0.2em;
color: #aaa!important;
}
h1:hover a.anchor, h2:hover a.anchor, h3:hover a.anchor, h4:hover a.anchor,
h5:hover a.anchor, h6:hover a.anchor {
display: inline;
}
h1 a.anchor:hover, h2 a.anchor:hover, h3 a.anchor:hover, h4 a.anchor:hover,
h5 a.anchor:hover, h6 a.anchor:hover {
color: #777;
background-color: #eee;
}
a.headerlink {
color: #c60f0f!important;
font-size: 1em;
margin-left: 6px;
padding: 0 4px 0 4px;
text-decoration: none!important;
}
a.headerlink:hover {
background-color: #ccc;
color: white!important;
}
cite, code, tt {
font-family: 'Consolas', 'Deja Vu Sans Mono',
'Bitstream Vera Sans Mono', monospace;
font-size: 0.95em;
letter-spacing: 0.01em;
}
tt {
background-color: #f2f2f2;
border-bottom: 1px solid #ddd;
color: #333;
}
tt.descname, tt.descclassname, tt.xref {
border: 0;
}
hr {
border: 1px solid #abc;
margin: 2em;
}
a tt {
border: 0;
color: #CA7900;
}
a tt:hover {
color: #2491CF;
}
pre {
font-family: 'Consolas', 'Deja Vu Sans Mono',
'Bitstream Vera Sans Mono', monospace;
font-size: 0.95em;
letter-spacing: 0.015em;
line-height: 120%;
padding: 0.5em;
border: 1px solid #ccc;
background-color: #f8f8f8;
}
pre a {
color: inherit;
text-decoration: underline;
}
td.linenos pre {
padding: 0.5em 0;
}
div.quotebar {
background-color: #f8f8f8;
max-width: 250px;
float: right;
padding: 2px 7px;
border: 1px solid #ccc;
}
div.topic {
background-color: #f8f8f8;
}
table {
border-collapse: collapse;
margin: 0 -0.5em 0 -0.5em;
}
table td, table th {
padding: 0.2em 0.5em 0.2em 0.5em;
}
div.admonition, div.warning {
font-size: 0.9em;
margin: 1em 0 1em 0;
border: 1px solid #86989B;
background-color: #f7f7f7;
padding: 0;
}
div.admonition p, div.warning p {
margin: 0.5em 1em 0.5em 1em;
padding: 0;
}
div.admonition pre, div.warning pre {
margin: 0.4em 1em 0.4em 1em;
}
div.admonition p.admonition-title,
div.warning p.admonition-title {
margin: 0;
padding: 0.1em 0 0.1em 0.5em;
color: white;
border-bottom: 1px solid #86989B;
font-weight: bold;
background-color: #AFC1C4;
}
div.warning {
border: 1px solid #940000;
}
div.warning p.admonition-title {
background-color: #CF0000;
border-bottom-color: #940000;
}
div.admonition ul, div.admonition ol,
div.warning ul, div.warning ol {
margin: 0.1em 0.5em 0.5em 3em;
padding: 0;
}
div.versioninfo {
margin: 1em 0 0 0;
border: 1px solid #ccc;
background-color: #DDEAF0;
padding: 8px;
line-height: 1.3em;
font-size: 0.9em;
}
.viewcode-back {
font-family: 'Lucida Grande', 'Lucida Sans Unicode', 'Geneva',
'Verdana', sans-serif;
}
div.viewcode-block:target {
background-color: #f4debf;
border-top: 1px solid #ac9;
border-bottom: 1px solid #ac9;
}

35
docs/_templates/defindex.html vendored Normal file
View File

@@ -0,0 +1,35 @@
{#
basic/defindex.html
~~~~~~~~~~~~~~~~~~~
Default template for the "index" page.
:copyright: Copyright 2007-2011 by the Sphinx team, see AUTHORS.
:license: BSD, see LICENSE for details.
#}
{% extends "layout.html" %}
{% set title = _('Overview') %}
{% block body %}
<h1>{{ docstitle|e }}</h1>
<p>
Welcome! This is
{% block description %}the documentation for {{ project|e }}
{{ release|e }}{% if last_updated %}, last updated {{ last_updated|e }}{% endif %}{% endblock %}.
</p>
{% block tables %}
<p><strong>{{ _('Indices and tables:') }}</strong></p>
<table class="contentstable" align="center"><tr>
<td width="50%">
<p class="biglink"><a class="biglink" href="{{ pathto("contents") }}">{{ _('Complete Table of Contents') }}</a><br>
<span class="linkdescr">{{ _('lists all sections and subsections') }}</span></p>
<p class="biglink"><a class="biglink" href="{{ pathto("search") }}">{{ _('Search Page') }}</a><br>
<span class="linkdescr">{{ _('search this documentation') }}</span></p>
</td><td width="50%">
<p class="biglink"><a class="biglink" href="{{ pathto("modindex") }}">{{ _('Global Module Index') }}</a><br>
<span class="linkdescr">{{ _('quick access to all modules') }}</span></p>
<p class="biglink"><a class="biglink" href="{{ pathto("genindex") }}">{{ _('General Index') }}</a><br>
<span class="linkdescr">{{ _('all functions, classes, terms') }}</span></p>
</td></tr>
</table>
{% endblock %}
{% endblock %}

61
docs/_templates/indexcontent.html vendored Normal file
View File

@@ -0,0 +1,61 @@
{% extends "defindex.html" %}
{% block tables %}
<p><strong>Parts of the documentation:</strong></p>
<table class="contentstable" align="center"><tr>
<td width="50%">
<p class="biglink"><a class="biglink" href="{{ pathto("whatsnew/" + version) }}">What's new in Python {{ version }}?</a><br/>
<span class="linkdescr">or <a href="{{ pathto("whatsnew/index") }}">all "What's new" documents</a> since 2.0</span></p>
<p class="biglink"><a class="biglink" href="{{ pathto("tutorial/index") }}">Tutorial</a><br/>
<span class="linkdescr">start here</span></p>
<p class="biglink"><a class="biglink" href="{{ pathto("library/index") }}">Library Reference</a><br/>
<span class="linkdescr">keep this under your pillow</span></p>
<p class="biglink"><a class="biglink" href="{{ pathto("reference/index") }}">Language Reference</a><br/>
<span class="linkdescr">describes syntax and language elements</span></p>
<p class="biglink"><a class="biglink" href="{{ pathto("using/index") }}">Python Setup and Usage</a><br/>
<span class="linkdescr">how to use Python on different platforms</span></p>
<p class="biglink"><a class="biglink" href="{{ pathto("howto/index") }}">Python HOWTOs</a><br/>
<span class="linkdescr">in-depth documents on specific topics</span></p>
</td><td width="50%">
<p class="biglink"><a class="biglink" href="{{ pathto("extending/index") }}">Extending and Embedding</a><br/>
<span class="linkdescr">tutorial for C/C++ programmers</span></p>
<p class="biglink"><a class="biglink" href="{{ pathto("c-api/index") }}">Python/C API</a><br/>
<span class="linkdescr">reference for C/C++ programmers</span></p>
<p class="biglink"><a class="biglink" href="{{ pathto("install/index") }}">Installing Python Modules</a><br/>
<span class="linkdescr">information for installers &amp; sys-admins</span></p>
<p class="biglink"><a class="biglink" href="{{ pathto("distutils/index") }}">Distributing Python Modules</a><br/>
<span class="linkdescr">sharing modules with others</span></p>
<p class="biglink"><a class="biglink" href="{{ pathto("documenting/index") }}">Documenting Python</a><br/>
<span class="linkdescr">guide for documentation authors</span></p>
<p class="biglink"><a class="biglink" href="{{ pathto("faq/index") }}">FAQs</a><br/>
<span class="linkdescr">frequently asked questions (with answers!)</span></p>
</td></tr>
</table>
<p><strong>Indices and tables:</strong></p>
<table class="contentstable" align="center"><tr>
<td width="50%">
<p class="biglink"><a class="biglink" href="{{ pathto("py-modindex") }}">Global Module Index</a><br/>
<span class="linkdescr">quick access to all modules</span></p>
<p class="biglink"><a class="biglink" href="{{ pathto("genindex") }}">General Index</a><br/>
<span class="linkdescr">all functions, classes, terms</span></p>
<p class="biglink"><a class="biglink" href="{{ pathto("glossary") }}">Glossary</a><br/>
<span class="linkdescr">the most important terms explained</span></p>
</td><td width="50%">
<p class="biglink"><a class="biglink" href="{{ pathto("search") }}">Search page</a><br/>
<span class="linkdescr">search this documentation</span></p>
<p class="biglink"><a class="biglink" href="{{ pathto("contents") }}">Complete Table of Contents</a><br/>
<span class="linkdescr">lists all sections and subsections</span></p>
</td></tr>
</table>
<p><strong>Meta information:</strong></p>
<table class="contentstable" align="center"><tr>
<td width="50%">
<p class="biglink"><a class="biglink" href="{{ pathto("bugs") }}">Reporting bugs</a></p>
<p class="biglink"><a class="biglink" href="{{ pathto("about") }}">About the documentation</a></p>
</td><td width="50%">
<p class="biglink"><a class="biglink" href="{{ pathto("license") }}">History and License of Python</a></p>
<p class="biglink"><a class="biglink" href="{{ pathto("copyright") }}">Copyright</a></p>
</td></tr>
</table>
{% endblock %}

69
docs/_templates/layout.html vendored Normal file
View File

@@ -0,0 +1,69 @@
{#
haiku/layout.html
~~~~~~~~~~~~~~~~~
Sphinx layout template for the haiku theme.
:copyright: Copyright 2007-2011 by the Sphinx team, see AUTHORS.
:license: BSD, see LICENSE for details.
#}
{% extends "basic/layout.html" %}
{% set script_files = script_files + ['_static/theme_extras.js'] %}
{% set css_files = css_files + ['_static/print.css'] %}
{# do not display relbars #}
{% block relbar1 %}{% endblock %}
{% block relbar2 %}{% endblock %}
{% macro nav() %}
<p>
{%- block haikurel1 %}
{%- endblock %}
{%- if prev %}
«&#160;&#160;<a href="{{ prev.link|e }}">{{ prev.title }}</a>
&#160;&#160;::&#160;&#160;
{%- endif %}
<a class="uplink" href="{{ pathto(master_doc) }}">{{ _('Contents') }}</a>
{%- if next %}
&#160;&#160;::&#160;&#160;
<a href="{{ next.link|e }}">{{ next.title }}</a>&#160;&#160;»
{%- endif %}
{%- block haikurel2 %}
{%- endblock %}
</p>
{% endmacro %}
{% block content %}
<div class="header">
{%- block haikuheader %}
{%- if theme_full_logo != "false" %}
<a href="{{ pathto('index') }}">
<img class="logo" src="{{ pathto('_static/' + logo, 1) }}" alt="Logo"/>
</a>
{%- else %}
{%- if logo -%}
<img class="rightlogo" src="{{ pathto('_static/' + logo, 1) }}" alt="Logo"/>
{%- endif -%}
<h1 class="heading">
<a href="{{ pathto('index') }}"><span>{{ project|e }}</span></a>
</h1>
<h2 class="heading"><span>{{ shorttitle|e }}</span></h2>
{%- endif %}
{%- endblock %}
</div>
<div class="topnav">
{{ nav() }}
</div>
<div class="content">
{#{%- if display_toc %}
<div id="toc">
<h3>Table Of Contents</h3>
{{ toc }}
</div>
{%- endif %}#}
{% block body %}{% endblock %}
</div>
<div class="bottomnav">
{{ nav() }}
</div>
{% endblock %}

8
docs/api/basexmpp.rst Normal file
View File

@@ -0,0 +1,8 @@
========
basexmpp
========
.. module:: sleekxmpp.basexmpp
.. autoclass:: BaseXMPP
:members:

19
docs/api/clientxmpp.rst Normal file
View File

@@ -0,0 +1,19 @@
==========
clientxmpp
==========
.. module:: sleekxmpp.clientxmpp
.. autodata:: SRV_SUPPORT
.. autoclass:: ClientXMPP
.. automethod:: connect
.. automethod:: register_feature
.. automethod:: get_roster
.. automethod:: update_roster
.. automethod:: del_roster_item

8
docs/api/xmlstream.rst Normal file
View File

@@ -0,0 +1,8 @@
=========
xmlstream
=========
.. module:: sleekxmpp.xmlstream
.. autoclass:: XMLStream
:members:

269
docs/architecture.rst Normal file
View File

@@ -0,0 +1,269 @@
.. index:: XMLStream, BaseXMPP, ClientXMPP, ComponentXMPP
SleekXMPP Architecture
======================
The core of SleekXMPP is contained in four classes: ``XMLStream``,
``BaseXMPP``, ``ClientXMPP``, and ``ComponentXMPP``. Along side this
stack is a library for working with XML objects that eliminates most
of the tedium of creating/manipulating XML.
.. image:: _static/images/arch_layers.png
:height: 300px
:align: center
.. index:: XMLStream
The Foundation: XMLStream
-------------------------
``XMLStream`` is a mostly XMPP-agnostic class whose purpose is to read
and write from a bi-directional XML stream. It also allows for callback
functions to execute when XML matching given patterns is received; these
callbacks are also referred to as :term:`stream handlers <stream handler>`.
The class also provides a basic eventing system which can be triggered
either manually or on a timed schedule.
The Main Threads
~~~~~~~~~~~~~~~~
``XMLStream`` instances run using at least three background threads: the
send thread, the read thread, and the scheduler thread. The send thread is
in charge of monitoring the send queue and writing text to the outgoing
XML stream. The read thread pulls text off of the incoming XML stream and
stores the results in an event queue. The scheduler thread is used to emit
events after a given period of time.
Additionally, the main event processing loop may be executed in its
own thread if SleekXMPP is being used in the background for another
application.
Short-lived threads may also be spawned as requested for threaded
:term:`event handlers <event handler>`.
How XML Text is Turned into Action
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
To demonstrate the flow of information, let's consider what happens
when this bit of XML is received (with an assumed namespace of
``jabber:client``):
.. code-block:: xml
<message to="user@example.com" from="friend@example.net">
<body>Hej!</body>
</message>
1. **Convert XML strings into objects.**
Incoming text is parsed and converted into XML objects (using
ElementTree) which are then wrapped into what are referred to as
:term:`Stanza objects <stanza object>`. The appropriate class for the
new object is determined using a map of namespaced element names to
classes.
Our incoming XML is thus turned into a ``Message`` :term:`stanza object`
because the namespaced element name ``{jabber:client}message`` is
associated with the class ``sleekxmpp.stanza.Message``.
2. **Match stanza objects to callbacks.**
These objects are then compared against the stored patterns associated
with the registered callback handlers. For each match, a copy of the
:term:`stanza object` is paired with a reference to the handler and
placed into the event queue.
Our ``Message`` object is thus paired with the message stanza handler
``BaseXMPP._handle_message`` to create the tuple::
('stanza', stanza_obj, handler)
3. **Process the event queue.**
The event queue is the heart of SleekXMPP. Nearly every action that
takes place is first inserted into this queue, whether that be received
stanzas, custom events, or scheduled events.
When the stanza is pulled out of the event queue with an associated
callback, the callback function is executed with the stanza as its only
parameter.
.. warning::
The callback, aka :term:`stream handler`, is executed in the main
processing thread. If the handler blocks, event processing will also
block.
4. **Raise Custom Events**
Since a :term:`stream handler` shouldn't block, if extensive processing
for a stanza is required (such as needing to send and receive an
``Iq`` stanza), then custom events must be used. These events are not
explicitly tied to the incoming XML stream and may be raised at any
time. Importantly, these events may be handled in their own thread.
When the event is raised, a copy of the stanza is created for each
handler registered for the event. In contrast to :term:`stream handlers <stream handler>`,
these functions are referred to as :term:`event handlers <event handler>`.
Each stanza/handler pair is then put into the event queue.
.. note::
It is possible to skip the event queue and process an event immediately
by using ``direct=True`` when raising the event.
The code for ``BaseXMPP._handle_message`` follows this pattern, and
raises a ``'message'`` event::
self.event('message', msg)
The event call then places the message object back into the event queue
paired with an :term:`event handler`::
('event', 'message', msg_copy1, custom_event_handler_1)
('event', 'message', msg_copy2, custom_evetn_handler_2)
5. **Process Custom Events**
The stanza and :term:`event handler` are then pulled from the event
queue, and the handler is executed, passing the stanza as its only
argument. If the handler was registered as threaded, then a new thread
will be spawned for it.
.. note::
Events may be raised without needing :term:`stanza objects <stanza object>`.
For example, you could use ``self.event('custom', {'a': 'b'})``.
You don't even need any arguments: ``self.event('no_parameters')``.
However, every event handler MUST accept at least one argument.
Finally, after a long trek, our message is handed off to the user's
custom handler in order to do awesome stuff::
msg.reply()
msg['body'] = "Hey! This is awesome!"
msg.send()
.. index:: BaseXMPP, XMLStream
Raising XMPP Awareness: BaseXMPP
--------------------------------
While ``XMLStream`` attempts to shy away from anything too XMPP specific,
``BaseXMPP``'s sole purpose is to provide foundational support for sending
and receiving XMPP stanzas. This support includes registering the basic
message, presence, and iq stanzas, methods for creating and sending
stanzas, and default handlers for incoming messages and keeping track of
presence notifications.
The plugin system for adding new XEP support is also maintained by
``BaseXMPP``.
.. index:: ClientXMPP, BaseXMPP
ClientXMPP
----------
``ClientXMPP`` extends ``BaseXMPP`` with additional logic for connecting to
an XMPP server by performing DNS lookups. It also adds support for stream
features such as STARTTLS and SASL.
.. index:: ComponentXMPP, BaseXMPP
ComponentXMPP
-------------
``ComponentXMPP`` is only a thin layer on top of ``BaseXMPP`` that
implements the component handshake protocol.
.. index::
double: object; stanza
Stanza Objects: A Brief Look
----------------------------
.. seealso::
See :ref:`api-stanza-objects` for a more detailed overview.
Almost worthy of their own standalone library, :term:`stanza objects <stanza object>`
are wrappers for XML objects which expose dictionary like interfaces
for manipulating their XML content. For example, consider the XML:
.. code-block:: xml
<message />
A very plain element to start with, but we can create a :term:`stanza object`
using ``sleekxmpp.stanza.Message`` as so::
msg = Message(xml=ET.fromstring("<message />"))
The ``Message`` stanza class defines interfaces such as ``'body'`` and
``'to'``, so we can assign values to those interfaces to include new XML
content::
msg['body'] = "Following so far?"
msg['to'] = 'user@example.com'
Dumping the XML content of ``msg`` (using ``msg.xml``), we find:
.. code-block:: xml
<message to="user@example.com">
<body>Following so far?</body>
</message>
The process is similar for reading from interfaces and deleting interface
contents. A :term:`stanza object` behaves very similarly to a regular
``dict`` object: you may assign to keys, read from keys, and ``del`` keys.
Stanza interfaces come with built-in behaviours such as adding/removing
attribute and sub element values. However, a lot of the time more custom
logic is needed. This can be provided by defining methods of the form
``get_*``, ``set_*``, and ``del_*`` for any interface which requires custom
behaviour.
Stanza Plugins
~~~~~~~~~~~~~~
Since it is generally possible to embed one XML element inside another,
:term:`stanza objects <stanza object>` may be nested. Nested
:term:`stanza objects <stanza object>` are referred to as :term:`stanza plugins <stanza plugin>`
or :term:`substanzas <substanza>`.
A :term:`stanza plugin` exposes its own interfaces by adding a new
interface to its parent stanza. To demonstrate, consider these two stanza
class definitions using ``sleekxmpp.xmlstream.ElementBase``:
.. code-block:: python
class Parent(ElementBase):
name = "the-parent-xml-element-name"
namespace = "the-parent-namespace"
interfaces = set(('foo', 'bar'))
class Child(ElementBase):
name = "the-child-xml-element-name"
namespace = "the-child-namespace"
plugin_attrib = 'child'
interfaces = set(('baz',))
If we register the ``Child`` stanza as a plugin of the ``Parent`` stanza as
so, using ``sleekxmpp.xmlstream.register_stanza_plugin``::
register_stanza_plugin(Parent, Child)
Then we can access content in the child stanza through the parent.
Note that the interface used to access the child stanza is the same as
``Child.plugin_attrib``::
parent = Parent()
parent['foo'] = 'a'
parent['child']['baz'] = 'b'
The above code would produce:
.. code-block:: xml
<the-parent-xml-element xmlns="the-parent-namespace" foo="a">
<the-child-xml-element xmlsn="the-child-namespace" baz="b" />
</the-parent-xml-element>
It is also possible to allow a :term:`substanza` to appear multiple times
by using ``iterable=True`` in the ``register_stanza_plugin`` call. All
iterable :term:`substanzas <substanza>` can be accessed using a standard
``substanzas`` interface.

220
docs/conf.py Normal file
View File

@@ -0,0 +1,220 @@
# -*- coding: utf-8 -*-
#
# SleekXMPP documentation build configuration file, created by
# sphinx-quickstart on Tue Aug 9 22:27:06 2011.
#
# This file is execfile()d with the current directory set to its containing dir.
#
# Note that not all possible configuration values are present in this
# autogenerated file.
#
# All configuration values have a default; values that are commented out
# serve to show the default.
import sys, os
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#sys.path.insert(0, os.path.abspath('.'))
# -- General configuration -----------------------------------------------------
# If your documentation needs a minimal Sphinx version, state it here.
#needs_sphinx = '1.0'
# Add any Sphinx extension module names here, as strings. They can be extensions
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
extensions = ['sphinx.ext.autodoc', 'sphinx.ext.viewcode']
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
# The suffix of source filenames.
source_suffix = '.rst'
# The encoding of source files.
#source_encoding = 'utf-8-sig'
# The master toctree document.
master_doc = 'index'
# General information about the project.
project = u'SleekXMPP'
copyright = u'2011, Nathan Fritz, Lance Stout'
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# built documents.
#
# The short X.Y version.
version = '1.0'
# The full version, including alpha/beta/rc tags.
release = '1.0RC1'
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
#language = None
# There are two options for replacing |today|: either, you set today to some
# non-false value, then it is used:
#today = ''
# Else, today_fmt is used as the format for a strftime call.
#today_fmt = '%B %d, %Y'
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
exclude_patterns = ['_build']
# The reST default role (used for this markup: `text`) to use for all documents.
#default_role = None
# If true, '()' will be appended to :func: etc. cross-reference text.
#add_function_parentheses = True
# If true, the current module name will be prepended to all description
# unit titles (such as .. function::).
#add_module_names = True
# If true, sectionauthor and moduleauthor directives will be shown in the
# output. They are ignored by default.
#show_authors = False
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'default'
# A list of ignored prefixes for module index sorting.
#modindex_common_prefix = []
# -- Options for HTML output ---------------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
html_theme = 'haiku'
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
# documentation.
#html_theme_options = {'headingcolor': '#CFCFCF', 'linkcolor': '#4A7389'}
# 00ADEE
# Add any paths that contain custom themes here, relative to this directory.
#html_theme_path = []
# The name for this set of Sphinx documents. If None, it defaults to
# "<project> v<release> documentation".
html_title = 'SleekXMPP'
# A shorter title for the navigation bar. Default is the same as html_title.
html_short_title = '%s Documentation' % release
# The name of an image file (relative to this directory) to place at the top
# of the sidebar.
#html_logo = None
# The name of an image file (within the static path) to use as favicon of the
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
# pixels large.
#html_favicon = None
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
# using the given strftime format.
#html_last_updated_fmt = '%b %d, %Y'
# If true, SmartyPants will be used to convert quotes and dashes to
# typographically correct entities.
#html_use_smartypants = True
# Custom sidebar templates, maps document names to template names.
#html_sidebars = {}
# Additional templates that should be rendered to pages, maps page names to
# template names.
html_additional_pages = {
}
# If false, no module index is generated.
#html_domain_indices = True
# If false, no index is generated.
#html_use_index = True
# If true, the index is split into individual pages for each letter.
#html_split_index = False
# If true, links to the reST sources are added to the pages.
#html_show_sourcelink = True
# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
#html_show_sphinx = True
# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
#html_show_copyright = True
# If true, an OpenSearch description file will be output, and all pages will
# contain a <link> tag referring to it. The value of this option must be the
# base URL from which the finished HTML is served.
#html_use_opensearch = ''
# This is the file name suffix for HTML files (e.g. ".xhtml").
#html_file_suffix = None
# Output file base name for HTML help builder.
htmlhelp_basename = 'SleekXMPPdoc'
# -- Options for LaTeX output --------------------------------------------------
# The paper size ('letter' or 'a4').
#latex_paper_size = 'letter'
# The font size ('10pt', '11pt' or '12pt').
#latex_font_size = '10pt'
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title, author, documentclass [howto/manual]).
latex_documents = [
('index', 'SleekXMPP.tex', u'SleekXMPP Documentation',
u'Nathan Fritz, Lance Stout', 'manual'),
]
# The name of an image file (relative to this directory) to place at the top of
# the title page.
#latex_logo = None
# For "manual" documents, if this is true, then toplevel headings are parts,
# not chapters.
#latex_use_parts = False
# If true, show page references after internal links.
#latex_show_pagerefs = False
# If true, show URL addresses after external links.
#latex_show_urls = False
# Additional stuff for the LaTeX preamble.
#latex_preamble = ''
# Documents to append as an appendix to all manuals.
#latex_appendices = []
# If false, no module index is generated.
#latex_domain_indices = True
# -- Options for manual page output --------------------------------------------
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
('index', 'sleekxmpp', u'SleekXMPP Documentation',
[u'Nathan Fritz, Lance Stout'], 1)
]

677
docs/create_plugin.rst Normal file
View File

@@ -0,0 +1,677 @@
Creating a SleekXMPP Plugin
===========================
One of the goals of SleekXMPP is to provide support for every draft or final
XMPP extension (`XEP <http://xmpp.org/extensions/>`_). To do this, SleekXMPP has a
plugin mechanism for adding the functionalities required by each XEP. But even
though plugins were made to quickly implement and prototype the official XMPP
extensions, there is no reason you can't create your own plugin to implement
your own custom XMPP-based protocol.
This guide will help walk you through the steps to
implement a rudimentary version of `XEP-0077 In-band
Registration <http://xmpp.org/extensions/xep-0077.html>`_. In-band registration
was implemented in example 14-6 (page 223) of `XMPP: The Definitive
Guide <http://oreilly.com/catalog/9780596521271>`_ because there was no SleekXMPP
plugin for XEP-0077 at the time of writing. We will partially fix that issue
here by turning the example implementation from *XMPP: The Definitive Guide*
into a plugin. Again, note that this will not a complete implementation, and a
different, more robust, official plugin for XEP-0077 may be added to SleekXMPP
in the future.
.. note::
The example plugin created in this guide is for the server side of the
registration process only. It will **NOT** be able to register new accounts
on an XMPP server.
First Steps
-----------
Every plugin inherits from the class :mod:`base_plugin <sleekxmpp.plugins.base.base_plugin>`,
and must include a ``plugin_init`` method. While the
plugins distributed with SleekXMPP must be placed in the plugins directory
``sleekxmpp/plugins`` to be loaded, custom plugins may be loaded from any
module. To do so, use the following form when registering the plugin:
.. code-block:: python
self.register_plugin('myplugin', module=mod_containing_my_plugin)
The plugin name must be the same as the plugin's class name.
Now, we can open our favorite text editors and create ``xep_0077.py`` in
``SleekXMPP/sleekxmpp/plugins``. We want to do some basic house-keeping and
declare the name and description of the XEP we are implementing. If you
are creating your own custom plugin, you don't need to include the ``xep``
attribute.
.. code-block:: python
"""
Creating a SleekXMPP Plugin
This is a minimal implementation of XEP-0077 to serve
as a tutorial for creating SleekXMPP plugins.
"""
from sleekxmpp.plugins.base import base_plugin
class xep_0077(base_plugin):
"""
XEP-0077 In-Band Registration
"""
def plugin_init(self):
self.description = "In-Band Registration"
self.xep = "0077"
Now that we have a basic plugin, we need to edit
``sleekxmpp/plugins/__init__.py`` to include our new plugin by adding
``'xep_0077'`` to the ``__all__`` declaration.
Interacting with Other Plugins
------------------------------
In-band registration is a feature that should be advertised through `Service
Discovery <http://xmpp.org/extensions/xep-0030.html>`_. To do that, we tell the
``xep_0030`` plugin to add the ``"jabber:iq:register"`` feature. We put this
call in a method named ``post_init`` which will be called once the plugin has
been loaded; by doing so we advertise that we can do registrations only after we
finish activating the plugin.
The ``post_init`` method needs to call ``base_plugin.post_init(self)``
which will mark that ``post_init`` has been called for the plugin. Once the
SleekXMPP object begins processing, ``post_init`` will be called on any plugins
that have not already run ``post_init``. This allows you to register plugins and
their dependencies without needing to worry about the order in which you do so.
**Note:** by adding this call we have introduced a dependency on the XEP-0030
plugin. Be sure to register ``'xep_0030'`` as well as ``'xep_0077'``. SleekXMPP
does not automatically load plugin dependencies for you.
.. code-block:: python
def post_init(self):
base_plugin.post_init(self)
self.xmpp['xep_0030'].add_feature("jabber:iq:register")
Creating Custom Stanza Objects
------------------------------
Now, the IQ stanzas needed to implement our version of XEP-0077 are not very
complex, and we could just interact with the XML objects directly just like
in the *XMPP: The Definitive Guide* example. However, creating custom stanza
objects is good practice.
We will create a new ``Registration`` stanza. Following the *XMPP: The
Definitive Guide* example, we will add support for a username and password
field. We also need two flags: ``registered`` and ``remove``. The ``registered``
flag is sent when an already registered user attempts to register, along with
their registration data. The ``remove`` flag is a request to unregister a user's
account.
Adding additional `fields specified in
XEP-0077 <http://xmpp.org/extensions/xep-0077.html#registrar-formtypes-register>`_
will not be difficult and is left as an exercise for the reader.
Our ``Registration`` class needs to start with a few descriptions of its
behaviour:
* ``namespace``
The namespace our stanza object lives in. In this case,
``"jabber:iq:register"``.
* ``name``
The name of the root XML element. In this case, the ``query`` element.
* ``plugin_attrib``
The name to access this type of stanza. In particular, given a
registration stanza, the ``Registration`` object can be found using:
``iq_object['register']``.
* ``interfaces``
A list of dictionary-like keys that can be used with the stanza object.
When using ``"key"``, if there exists a method of the form ``getKey``,
``setKey``, or``delKey`` (depending on context) then the result of calling
that method will be returned. Otherwise, the value of the attribute ``key``
of the main stanza element is returned if one exists.
**Note:** The accessor methods currently use title case, and not camel case.
Thus if you need to access an item named ``"methodName"`` you will need to
use ``getMethodname``. This naming convention might change to full camel
case in a future version of SleekXMPP.
* ``sub_interfaces``
A subset of ``interfaces``, but these keys map to the text of any
subelements that are direct children of the main stanza element. Thus,
referencing ``iq_object['register']['username']`` will either execute
``getUsername`` or return the value in the ``username`` element of the
query.
If you need to access an element, say ``elem``, that is not a direct child
of the main stanza element, you will need to add ``getElem``, ``setElem``,
and ``delElem``. See the note above about naming conventions.
.. code-block:: python
from sleekxmpp.xmlstream import ElementBase, ET, JID, register_stanza_plugin
from sleekxmpp import Iq
class Registration(ElementBase):
namespace = 'jabber:iq:register'
name = 'query'
plugin_attrib = 'register'
interfaces = set(('username', 'password', 'registered', 'remove'))
sub_interfaces = interfaces
def getRegistered(self):
present = self.xml.find('{%s}registered' % self.namespace)
return present is not None
def getRemove(self):
present = self.xml.find('{%s}remove' % self.namespace)
return present is not None
def setRegistered(self, registered):
if registered:
self.addField('registered')
else:
del self['registered']
def setRemove(self, remove):
if remove:
self.addField('remove')
else:
del self['remove']
def addField(self, name):
itemXML = ET.Element('{%s}%s' % (self.namespace, name))
self.xml.append(itemXML)
Setting a ``sub_interface`` attribute to ``""`` will remove that subelement.
Since we want to include empty registration fields in our form, we need the
``addField`` method to add the empty elements.
Since the ``registered`` and ``remove`` elements are just flags, we need to add
custom logic to enforce the binary behavior.
Extracting Stanzas from the XML Stream
--------------------------------------
Now that we have a custom stanza object, we need to be able to detect when we
receive one. To do this, we register a stream handler that will pattern match
stanzas off of the XML stream against our stanza object's element name and
namespace. To do so, we need to create a ``Callback`` object which contains
an XML fragment that can identify our stanza type. We can add this handler
registration to our ``plugin_init`` method.
Also, we need to associate our ``Registration`` class with IQ stanzas;
that requires the use of the ``register_stanza_plugin`` function (in
``sleekxmpp.xmlstream.stanzabase``) which takes the class of a parent stanza
type followed by the substanza type. In our case, the parent stanza is an IQ
stanza, and the substanza is our registration query.
The ``__handleRegistration`` method referenced in the callback will be our
handler function to process registration requests.
.. code-block:: python
def plugin_init(self):
self.description = "In-Band Registration"
self.xep = "0077"
self.xmpp.registerHandler(
Callback('In-Band Registration',
MatchXPath('{%s}iq/{jabber:iq:register}query' % self.xmpp.default_ns),
self.__handleRegistration))
register_stanza_plugin(Iq, Registration)
Handling Incoming Stanzas and Triggering Events
-----------------------------------------------
There are six situations that we need to handle to finish our implementation of
XEP-0077.
**Registration Form Request from a New User:**
.. code-block:: xml
<iq type="result">
<query xmlns="jabber:iq:register">
<username />
<password />
</query>
</iq>
**Registration Form Request from an Existing User:**
.. code-block:: xml
<iq type="result">
<query xmlns="jabber:iq:register">
<registered />
<username>Foo</username>
<password>hunter2</password>
</query>
</iq>
**Unregister Account:**
.. code-block:: xml
<iq type="result">
<query xmlns="jabber:iq:register" />
</iq>
**Incomplete Registration:**
.. code-block:: xml
<iq type="error">
<query xmlns="jabber:iq:register">
<username>Foo</username>
</query>
<error code="406" type="modify">
<not-acceptable xmlns="urn:ietf:params:xml:ns:xmpp-stanzas" />
</error>
</iq>
**Conflicting Registrations:**
.. code-block:: xml
<iq type="error">
<query xmlns="jabber:iq:register">
<username>Foo</username>
<password>hunter2</password>
</query>
<error code="409" type="cancel">
<conflict xmlns="urn:ietf:params:xml:ns:xmpp-stanzas" />
</error>
</iq>
**Successful Registration:**
.. code-block:: xml
<iq type="result">
<query xmlns="jabber:iq:register" />
</iq>
Cases 1 and 2: Registration Requests
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Responding to registration requests depends on if the requesting user already
has an account. If there is an account, the response should include the
``registered`` flag and the user's current registration information. Otherwise,
we just send the fields for our registration form.
We will handle both cases by creating a ``sendRegistrationForm`` method that
will create either an empty of full form depending on if we provide it with
user data. Since we need to know which form fields to include (especially if we
add support for the other fields specified in XEP-0077), we will also create a
method ``setForm`` which will take the names of the fields we wish to include.
.. code-block:: python
def plugin_init(self):
self.description = "In-Band Registration"
self.xep = "0077"
self.form_fields = ('username', 'password')
... remainder of plugin_init
...
def __handleRegistration(self, iq):
if iq['type'] == 'get':
# Registration form requested
userData = self.backend[iq['from'].bare]
self.sendRegistrationForm(iq, userData)
def setForm(self, *fields):
self.form_fields = fields
def sendRegistrationForm(self, iq, userData=None):
reg = iq['register']
if userData is None:
userData = {}
else:
reg['registered'] = True
for field in self.form_fields:
data = userData.get(field, '')
if data:
# Add field with existing data
reg[field] = data
else:
# Add a blank field
reg.addField(field)
iq.reply().setPayload(reg.xml)
iq.send()
Note how we are able to access our ``Registration`` stanza object with
``iq['register']``.
A User Backend
++++++++++++++
You might have noticed the reference to ``self.backend``, which is an object
that abstracts away storing and retrieving user information. Since it is not
much more than a dictionary, we will leave the implementation details to the
final, full source code example.
Case 3: Unregister an Account
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The next simplest case to consider is responding to a request to remove
an account. If we receive a ``remove`` flag, we instruct the backend to
remove the user's account. Since your application may need to know about
when users are registered or unregistered, we trigger an event using
``self.xmpp.event('unregister_user', iq)``. See the component examples below for
how to respond to that event.
.. code-block:: python
def __handleRegistration(self, iq):
if iq['type'] == 'get':
# Registration form requested
userData = self.backend[iq['from'].bare]
self.sendRegistrationForm(iq, userData)
elif iq['type'] == 'set':
# Remove an account
if iq['register']['remove']:
self.backend.unregister(iq['from'].bare)
self.xmpp.event('unregistered_user', iq)
iq.reply().send()
return
Case 4: Incomplete Registration
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
For the next case we need to check the user's registration to ensure it has all
of the fields we wanted. The simple option that we will use is to loop over the
field names and check each one; however, this means that all fields we send to
the user are required. Adding optional fields is left to the reader.
Since we have received an incomplete form, we need to send an error message back
to the user. We have to send a few different types of errors, so we will also
create a ``_sendError`` method that will add the appropriate ``error`` element
to the IQ reply.
.. code-block:: python
def __handleRegistration(self, iq):
if iq['type'] == 'get':
# Registration form requested
userData = self.backend[iq['from'].bare]
self.sendRegistrationForm(iq, userData)
elif iq['type'] == 'set':
if iq['register']['remove']:
# Remove an account
self.backend.unregister(iq['from'].bare)
self.xmpp.event('unregistered_user', iq)
iq.reply().send()
return
for field in self.form_fields:
if not iq['register'][field]:
# Incomplete Registration
self._sendError(iq, '406', 'modify', 'not-acceptable'
"Please fill in all fields.")
return
...
def _sendError(self, iq, code, error_type, name, text=''):
iq.reply().setPayload(iq['register'].xml)
iq.error()
iq['error']['code'] = code
iq['error']['type'] = error_type
iq['error']['condition'] = name
iq['error']['text'] = text
iq.send()
Cases 5 and 6: Conflicting and Successful Registration
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
We are down to the final decision on if we have a successful registration. We
send the user's data to the backend with the ``self.backend.register`` method.
If it returns ``True``, then registration has been successful. Otherwise,
there has been a conflict with usernames and registration has failed. Like
with unregistering an account, we trigger an event indicating that a user has
been registered by using ``self.xmpp.event('registered_user', iq)``. See the
component examples below for how to respond to this event.
.. code-block:: python
def __handleRegistration(self, iq):
if iq['type'] == 'get':
# Registration form requested
userData = self.backend[iq['from'].bare]
self.sendRegistrationForm(iq, userData)
elif iq['type'] == 'set':
if iq['register']['remove']:
# Remove an account
self.backend.unregister(iq['from'].bare)
self.xmpp.event('unregistered_user', iq)
iq.reply().send()
return
for field in self.form_fields:
if not iq['register'][field]:
# Incomplete Registration
self._sendError(iq, '406', 'modify', 'not-acceptable',
"Please fill in all fields.")
return
if self.backend.register(iq['from'].bare, iq['register']):
# Successful registration
self.xmpp.event('registered_user', iq)
iq.reply().setPayload(iq['register'].xml)
iq.send()
else:
# Conflicting registration
self._sendError(iq, '409', 'cancel', 'conflict',
"That username is already taken.")
Example Component Using the XEP-0077 Plugin
-------------------------------------------
Alright, the moment we've been working towards - actually using our plugin to
simplify our other applications. Here is a basic component that simply manages
user registrations and sends the user a welcoming message when they register,
and a farewell message when they delete their account.
Note that we have to register the ``'xep_0030'`` plugin first,
and that we specified the form fields we wish to use with
``self.xmpp.plugin['xep_0077'].setForm('username', 'password')``.
.. code-block:: python
import sleekxmpp.componentxmpp
class Example(sleekxmpp.componentxmpp.ComponentXMPP):
def __init__(self, jid, password):
sleekxmpp.componentxmpp.ComponentXMPP.__init__(self, jid, password, 'localhost', 8888)
self.registerPlugin('xep_0030')
self.registerPlugin('xep_0077')
self.plugin['xep_0077'].setForm('username', 'password')
self.add_event_handler("registered_user", self.reg)
self.add_event_handler("unregistered_user", self.unreg)
def reg(self, iq):
msg = "Welcome! %s" % iq['register']['username']
self.sendMessage(iq['from'], msg, mfrom=self.fulljid)
def unreg(self, iq):
msg = "Bye! %s" % iq['register']['username']
self.sendMessage(iq['from'], msg, mfrom=self.fulljid)
**Congratulations!** We now have a basic, functioning implementation of
XEP-0077.
Complete Source Code for XEP-0077 Plugin
----------------------------------------
Here is a copy of a more complete implementation of the plugin we created, but
with some additional registration fields implemented.
.. code-block:: python
"""
Creating a SleekXMPP Plugin
This is a minimal implementation of XEP-0077 to serve
as a tutorial for creating SleekXMPP plugins.
"""
from sleekxmpp.plugins.base import base_plugin
from sleekxmpp.xmlstream.handler.callback import Callback
from sleekxmpp.xmlstream.matcher.xpath import MatchXPath
from sleekxmpp.xmlstream import ElementBase, ET, JID, register_stanza_plugin
from sleekxmpp import Iq
import copy
class Registration(ElementBase):
namespace = 'jabber:iq:register'
name = 'query'
plugin_attrib = 'register'
interfaces = set(('username', 'password', 'email', 'nick', 'name',
'first', 'last', 'address', 'city', 'state', 'zip',
'phone', 'url', 'date', 'misc', 'text', 'key',
'registered', 'remove', 'instructions'))
sub_interfaces = interfaces
def getRegistered(self):
present = self.xml.find('{%s}registered' % self.namespace)
return present is not None
def getRemove(self):
present = self.xml.find('{%s}remove' % self.namespace)
return present is not None
def setRegistered(self, registered):
if registered:
self.addField('registered')
else:
del self['registered']
def setRemove(self, remove):
if remove:
self.addField('remove')
else:
del self['remove']
def addField(self, name):
itemXML = ET.Element('{%s}%s' % (self.namespace, name))
self.xml.append(itemXML)
class UserStore(object):
def __init__(self):
self.users = {}
def __getitem__(self, jid):
return self.users.get(jid, None)
def register(self, jid, registration):
username = registration['username']
def filter_usernames(user):
return user != jid and self.users[user]['username'] == username
conflicts = filter(filter_usernames, self.users.keys())
if conflicts:
return False
self.users[jid] = registration
return True
def unregister(self, jid):
del self.users[jid]
class xep_0077(base_plugin):
"""
XEP-0077 In-Band Registration
"""
def plugin_init(self):
self.description = "In-Band Registration"
self.xep = "0077"
self.form_fields = ('username', 'password')
self.form_instructions = ""
self.backend = UserStore()
self.xmpp.registerHandler(
Callback('In-Band Registration',
MatchXPath('{%s}iq/{jabber:iq:register}query' % self.xmpp.default_ns),
self.__handleRegistration))
register_stanza_plugin(Iq, Registration)
def post_init(self):
base_plugin.post_init(self)
self.xmpp['xep_0030'].add_feature("jabber:iq:register")
def __handleRegistration(self, iq):
if iq['type'] == 'get':
# Registration form requested
userData = self.backend[iq['from'].bare]
self.sendRegistrationForm(iq, userData)
elif iq['type'] == 'set':
if iq['register']['remove']:
# Remove an account
self.backend.unregister(iq['from'].bare)
self.xmpp.event('unregistered_user', iq)
iq.reply().send()
return
for field in self.form_fields:
if not iq['register'][field]:
# Incomplete Registration
self._sendError(iq, '406', 'modify', 'not-acceptable',
"Please fill in all fields.")
return
if self.backend.register(iq['from'].bare, iq['register']):
# Successful registration
self.xmpp.event('registered_user', iq)
iq.reply().setPayload(iq['register'].xml)
iq.send()
else:
# Conflicting registration
self._sendError(iq, '409', 'cancel', 'conflict',
"That username is already taken.")
def setForm(self, *fields):
self.form_fields = fields
def setInstructions(self, instructions):
self.form_instructions = instructions
def sendRegistrationForm(self, iq, userData=None):
reg = iq['register']
if userData is None:
userData = {}
else:
reg['registered'] = True
if self.form_instructions:
reg['instructions'] = self.form_instructions
for field in self.form_fields:
data = userData.get(field, '')
if data:
# Add field with existing data
reg[field] = data
else:
# Add a blank field
reg.addField(field)
iq.reply().setPayload(reg.xml)
iq.send()
def _sendError(self, iq, code, error_type, name, text=''):
iq.reply().setPayload(iq['register'].xml)
iq.error()
iq['error']['code'] = code
iq['error']['type'] = error_type
iq['error']['condition'] = name
iq['error']['text'] = text
iq.send()

271
docs/event_index.rst Normal file
View File

@@ -0,0 +1,271 @@
Event Index
===========
.. glossary::
:sorted:
connected
- **Data:** ``{}``
- **Source:** :py:class:`~sleekxmpp.clientxmpp.ClientXMPP`
Signal that a connection has been made with the XMPP server, but a session
has not yet been established.
changed_status
- **Data:** :py:class:`~sleekxmpp.Presence`
- **Source:** :py:class:`~sleekxmpp.BaseXMPP`
Triggered when a presence stanza is received from a JID with a show type
different than the last presence stanza from the same JID.
changed_subscription
- **Data:** :py:class:`~sleekxmpp.Presence`
- **Source:** :py:class:`~sleekxmpp.BaseXMPP`
Triggered whenever a presence stanza with a type of ``subscribe``,
``subscribed``, ``unsubscribe``, or ``unsubscribed`` is received.
Note that if the values ``xmpp.auto_authorize`` and ``xmpp.auto_subscribe``
are set to ``True`` or ``False``, and not ``None``, then SleekXMPP will
either accept or reject all subscription requests before your event handlers
are called. Set these values to ``None`` if you wish to make more complex
subscription decisions.
chatstate_active
- **Data:**
- **Source:**
chatstate_composing
- **Data:**
- **Source:**
chatstate_gone
- **Data:**
- **Source:**
chatstate_inactive
- **Data:**
- **Source:**
chatstate_paused
- **Data:**
- **Source:**
disco_info
- **Data:** :py:class:`~sleekxmpp.plugins.xep_0030.stanza.DiscoInfo`
- **Source:** :py:class:`~sleekxmpp.plugins.xep_0030.disco.xep_0030`
Triggered whenever a ``disco#info`` result stanza is received.
disco_items
- **Data:** :py:class:`~sleekxmpp.plugins.xep_0030.stanza.DiscoItems`
- **Source:** :py:class:`~sleekxmpp.plugins.xep_0030.disco.xep_0030`
Triggered whenever a ``disco#items`` result stanza is received.
disconnected
- **Data:** ``{}``
- **Source:** :py:class:`~sleekxmpp.ClientXMPP`
Signal that the connection with the XMPP server has been lost.
entity_time
- **Data:**
- **Source:**
failed_auth
- **Data:** ``{}``
- **Source:** :py:class:`~sleekxmpp.ClientXMPP`, :py:class:`~sleekxmpp.plugins.xep_0078.xep_0078`
Signal that the server has rejected the provided login credentials.
gmail_notify
- **Data:** ``{}``
- **Source:** :py:class:`~sleekxmpp.plugins.gmail_notify.gmail_notify`
Signal that there are unread emails for the Gmail account associated with the current XMPP account.
gmail_messages
- **Data:** :py:class:`~sleekxmpp.Iq`
- **Source:** :py:class:`~sleekxmpp.plugins.gmail_notify.gmail_notify`
Signal that there are unread emails for the Gmail account associated with the current XMPP account.
got_online
- **Data:** :py:class:`~sleekxmpp.Presence`
- **Source:** :py:class:`~sleekxmpp.BaseXMPP`
If a presence stanza is received from a JID which was previously marked as
offline, and the presence has a show type of '``chat``', '``dnd``', '``away``',
or '``xa``', then this event is triggered as well.
got_offline
- **Data:** :py:class:`~sleekxmpp.Presence`
- **Source:** :py:class:`~sleekxmpp.BaseXMPP`
Signal that an unavailable presence stanza has been received from a JID.
groupchat_invite
- **Data:**
- **Source:**
groupchat_direct_invite
- **Data:** :py:class:`~sleekxmpp.Message`
- **Source:** :py:class:`~sleekxmpp.plugins.xep_0249.direct`
groupchat_message
- **Data:** :py:class:`~sleekxmpp.Message`
- **Source:** :py:class:`~sleekxmpp.plugins.xep_0045.xep_0045`
Triggered whenever a message is received from a multi-user chat room.
groupchat_presence
- **Data:** :py:class:`~sleekxmpp.Presence`
- **Source:** :py:class:`~sleekxmpp.plugins.xep_0045.xep_0045`
Triggered whenever a presence stanza is received from a user in a multi-user chat room.
groupchat_subject
- **Data:** :py:class:`~sleekxmpp.Message`
- **Source:** :py:class:`~sleekxmpp.plugins.xep_0045.xep_0045`
Triggered whenever the subject of a multi-user chat room is changed, or announced when joining a room.
killed
- **Data:**
- **Source:**
last_activity
- **Data:**
- **Source:**
message
- **Data:** :py:class:`~sleekxmpp.Message`
- **Source:** :py:class:`BaseXMPP <sleekxmpp.BaseXMPP>`
Makes the contents of message stanzas available whenever one is received. Be
sure to check the message type in order to handle error messages.
message_form
- **Data:** :py:class:`~sleekxmpp.plugins.xep_0004.Form`
- **Source:** :py:class:`~sleekxmpp.plugins.xep_0004.xep_0004`
Currently the same as :term:`message_xform`.
message_xform
- **Data:** :py:class:`~sleekxmpp.plugins.xep_0004.Form`
- **Source:** :py:class:`~sleekxmpp.plugins.xep_0004.xep_0004`
Triggered whenever a data form is received inside a message.
mucc::[room]::got_offline
- **Data:**
- **Source:**
muc::[room]::got_online
- **Data:**
- **Source:**
muc::[room]::message
- **Data:**
- **Source:**
muc::[room]::presence
- **Data:**
- **Source:**
presence_available
- **Data:** :py:class:`~sleekxmpp.Presence`
- **Source:** :py:class:`~sleekxmpp.BaseXMPP`
A presence stanza with a type of '``available``' is received.
presence_error
- **Data:** :py:class:`~sleekxmpp.Presence`
- **Source:** :py:class:`~sleekxmpp.BaseXMPP`
A presence stanza with a type of '``error``' is received.
presence_form
- **Data:** :py:class:`~sleekxmpp.plugins.xep_0004.Form`
- **Source:** :py:class:`~sleekxmpp.plugins.xep_0004.xep_0004`
This event is present in the XEP-0004 plugin code, but is currently not used.
presence_probe
- **Data:** :py:class:`~sleekxmpp.Presence`
- **Source:** :py:class:`~sleekxmpp.BaseXMPP`
A presence stanza with a type of '``probe``' is received.
presence_subscribe
- **Data:** :py:class:`~sleekxmpp.Presence`
- **Source:** :py:class:`~sleekxmpp.BaseXMPP`
A presence stanza with a type of '``subscribe``' is received.
presence_subscribed
- **Data:** :py:class:`~sleekxmpp.Presence`
- **Source:** :py:class:`~sleekxmpp.BaseXMPP`
A presence stanza with a type of '``subscribed``' is received.
presence_unavailable
- **Data:** :py:class:`~sleekxmpp.Presence`
- **Source:** :py:class:`~sleekxmpp.BaseXMPP`
A presence stanza with a type of '``unavailable``' is received.
presence_unsubscribe
- **Data:** :py:class:`~sleekxmpp.Presence`
- **Source:** :py:class:`~sleekxmpp.BaseXMPP`
A presence stanza with a type of '``unsubscribe``' is received.
presence_unsubscribed
- **Data:** :py:class:`~sleekxmpp.Presence`
- **Source:** :py:class:`~sleekxmpp.BaseXMPP`
A presence stanza with a type of '``unsubscribed``' is received.
roster_update
- **Data:** :py:class:`~sleekxmpp.stanza.Roster`
- **Source:** :py:class:`~sleekxmpp.ClientXMPP`
An IQ result containing roster entries is received.
sent_presence
- **Data:** ``{}``
- **Source:** :py:class:`BaseXMPP <sleekxmpp.BaseXMPP>`
Signal that an initial presence stanza has been written to the XML stream.
session_end
- **Data:** ``{}``
- **Source:** :py:class:`ClientXMPP <sleekxmpp.ClientXMPP>`,
:py:class:`ComponentXMPP <sleekxmpp.ComponentXMPP>`
:py:class:`XEP-0078 <sleekxmpp.plugins.xep_0078>`
Signal that a connection to the XMPP server has been lost and the current
stream session has ended. Currently equivalent to :term:`disconnected`, but
future implementation of `XEP-0198: Stream Management <http://xmpp.org/extensions/xep-0198.html>`_
will distinguish the two events.
Plugins that maintain session-based state should clear themselves when
this event is fired.
session_start
- **Data:** ``{}``
- **Source:** :py:class:`ClientXMPP <sleekxmpp.ClientXMPP>`,
:py:class:`ComponentXMPP <sleekxmpp.ComponentXMPP>`
:py:class:`XEP-0078 <sleekxmpp.plugins.xep_0078>`
Signal that a connection to the XMPP server has been made and a session has been established.
socket_error
- **Data:** ``Socket`` exception object
- **Source:** :py:class:`~sleekxmpp.xmlstream.XMLstream`
stream_error
- **Data:** :py:class:`~sleekxmpp.stanza.StreamError`
- **Source:** :py:class:`~sleekxmpp.BaseXMPP`

2
docs/features.rst Normal file
View File

@@ -0,0 +1,2 @@
How to Use Stream Features
==========================

View File

@@ -0,0 +1,2 @@
Create and Run a Server Component
=================================

View File

@@ -0,0 +1,390 @@
.. _echobot:
===============================
SleekXMPP Quickstart - Echo Bot
===============================
.. note::
If you have any issues working through this quickstart guide
or the other tutorials here, please either send a message to the
`mailing list <http://groups.google.com/group/sleekxmpp-discussion>`_
or join the chat room at `sleek@conference.jabber.org
<xmpp:sleek@conference.jabber.org?join>`_.
If you have not yet installed SleekXMPP, do so now by either checking out a version
from `Github <http://github.com/fritzy/SleekXMPP>`_, or installing it using ``pip``
or ``easy_install``.
.. code-block:: sh
pip install sleekxmpp # Or: easy_install sleekxmpp
As a basic starting project, we will create an echo bot which will reply to any
messages sent to it. We will also go through adding some basic command line configuration
for enabling or disabling debug log outputs and setting the username and password
for the bot.
For the command line options processing, we will use the built-in ``optparse``
module and the ``getpass`` module for reading in passwords.
TL;DR Just Give Me the Code
---------------------------
As you wish: :ref:`the completed example <echobot_complete>`.
Overview
--------
To get started, here is a brief outline of the structure that the final project will have:
.. code-block:: python
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import sys
import logging
import getpass
from optparse import OptionParser
import sleekxmpp
'''Here we will create out echo bot class'''
if __name__ == '__main__':
'''Here we will configure and read command line options'''
'''Here we will instantiate our echo bot'''
'''Finally, we connect the bot and start listening for messages'''
Default Encoding
----------------
XMPP requires support for UTF-8 and so SleekXMPP must use UTF-8 as well. In
Python3 this is simple because Unicode is the default string type. For Python2.6+
the situation is not as easy because standard strings are simply byte arrays and
use ASCII. We can get Python to use UTF-8 as the default encoding by including:
.. code-block:: python
if sys.version_info < (3, 0):
reload(sys)
sys.setdefaultencoding('utf8')
.. warning::
Until we are able to ensure that SleekXMPP will always use Unicode in Python2.6+, this
may cause issues embedding SleekXMPP into other applications which assume ASCII encoding.
Creating the EchoBot Class
--------------------------
There are three main types of entities within XMPP — servers, components, and
clients. Since our echo bot will only be responding to a few people, and won't need
to remember thousands of users, we will use a client connection. A client connection
is the same type that you use with your standard IM client such as Pidgin or Psi.
SleekXMPP comes with a :class:`ClientXMPP <sleekxmpp.clientxmpp.ClientXMPP>` class
which we can extend to add our message echoing feature. :class:`ClientXMPP <sleekxmpp.clientxmpp.ClientXMPP>`
requires the parameters ``jid`` and ``password``, so we will let our ``EchoBot`` class accept those
as well.
.. code-block:: python
class EchoBot(sleekxmpp.ClientXMPP):
def __init__(self, jid, password):
super(EchoBot, self).__init__(jid, password)
Handling Session Start
~~~~~~~~~~~~~~~~~~~~~~
The XMPP spec requires clients to broadcast its presence and retrieve its roster (buddy list) once
it connects and establishes a session with the XMPP server. Until these two tasks are completed,
some servers may not deliver or send messages or presence notifications to the client. So we now
need to be sure that we retrieve our roster and send an initial presence once the session has
started. To do that, we will register an event handler for the :term:`session_start` event.
.. code-block:: python
def __init__(self, jid, password):
super(EchoBot, self).__init__(jid, password)
self.add_event_handler('session_start', self.start)
Since we want the method ``self.start`` to execute when the :term:`session_start` event is triggered,
we also need to define the ``self.start`` handler.
.. code-block:: python
def start(self, event):
self.send_presence()
self.get_roster()
.. warning::
Not sending an initial presence and retrieving the roster when using a client instance can
prevent your program from receiving presence notifications or messages depending on the
XMPP server you have chosen.
Our event handler, like every event handler, accepts a single parameter which typically is the stanza
that was received that caused the event. In this case, ``event`` will just be an empty dictionary since
there is no associated data.
Our first task of sending an initial presence is done using :meth:`send_presence <sleekxmpp.basexmpp.BaseXMPP.send_presence>`.
Calling :meth:`send_presence <sleekxmpp.basexmpp.BaseXMPP.send_presence>` without any arguments will send the simplest
stanza allowed in XMPP:
.. code-block:: xml
<presence />
The second requirement is fulfilled using :meth:`get_roster <sleekxmpp.clientxmpp.ClientXMPP.get_roster>`, which
will send an IQ stanza requesting the roster to the server and then wait for the response. You may be wondering
what :meth:`get_roster <sleekxmpp.clientxmpp.ClientXMPP.get_roster>` returns since we are not saving any return
value. The roster data is saved by an internal handler to ``self.roster``, and in the case of a :class:`ClientXMPP
<sleekxmpp.clientxmpp.ClientXMPP>` instance to ``self.client_roster``. (The difference between ``self.roster`` and
``self.client_roster`` is that ``self.roster`` supports storing roster information for multiple JIDs, which is useful
for components, whereas ``self.client_roster`` stores roster data for just the client's JID.)
It is possible for a timeout to occur while waiting for the server to respond, which can happen if the
network is excessively slow or the server is no longer responding. In that case, an :class:`IQTimeout
<sleekxmpp.exceptions.IQTimeout>` is raised. Similarly, an :class:`IQError <sleekxmpp.exceptions.IQError>` exception can
be raised if the request contained bad data or requested the roster for the wrong user. In either case, you can wrap the
``get_roster()`` call in a ``try``/``except`` block to retry the roster retrieval process.
The XMPP stanzas from the roster retrieval process could look like this:
.. code-block:: xml
<iq type="get">
<query xmlns="jabber:iq:roster" />
</iq>
<iq type="result" to="echobot@example.com" from="example.com">
<query xmlns="jabber:iq:roster">
<item jid="friend@example.com" subscription="both" />
</query>
</iq>
Responding to Messages
~~~~~~~~~~~~~~~~~~~~~~
Now that an ``EchoBot`` instance handles :term:`session_start`, we can begin receiving and
responding to messages. Now we can register a handler for the :term:`message` event that is raised
whenever a messsage is received.
.. code-block:: python
def __init__(self, jid, password):
super(EchoBot, self).__init__(jid, password)
self.add_event_handler('session_start', self.start)
self.add_event_handler('message', self.message)
The :term:`message` event is fired whenever a ``<message />`` stanza is received, including for
group chat messages, errors, etc. Properly responding to messages thus requires checking the
``'type'`` interface of the message :term:`stanza object`. For responding to only messages
addressed to our bot (and not from a chat room), we check that the type is either ``normal``
or ``chat``. (Other potential types are ``error``, ``headline``, and ``groupchat``.)
.. code-block:: python
def message(self, msg):
if msg['type'] in ('normal', 'chat'):
msg.reply("Thanks for sending:\n%s" % msg['body']).send()
Let's take a closer look at the ``.reply()`` method used above. For message stanzas,
``.reply()`` accepts the parameter ``body`` (also as the first positional argument),
which is then used as the value of the ``<body />`` element of the message.
Setting the appropriate ``to`` JID is also handled by ``.reply()``.
Another way to have sent the reply message would be to use :meth:`send_message <sleekxmpp.basexmpp.BaseXMPP.send_message>`,
which is a convenience method for generating and sending a message based on the values passed to it. If we were to use
this method, the above code would look as so:
.. code-block:: python
def message(self, msg):
if msg['type'] in ('normal', 'chat'):
self.send_message(mto=msg['from'],
mbody='Thanks for sending:\n%s' % msg['body'])
Whichever method you choose to use, the results in action will look like this:
.. code-block:: xml
<message to="echobot@example.com" from="someuser@example.net" type="chat">
<body>Hej!</body>
</message>
<message to="someuser@example.net" type="chat">
<body>Thanks for sending:
Hej!</body>
</message>
.. note::
XMPP does not require stanzas sent by a client to include a ``from`` attribute, and
leaves that responsibility to the XMPP server. However, if a sent stanza does
include a ``from`` attribute, it must match the full JID of the client or some
servers will reject it. SleekXMPP thus leaves out the ``from`` attribute when replying
using a client connection.
Command Line Arguments and Logging
----------------------------------
While this isn't part of SleekXMPP itself, we do want our echo bot program to be able
to accept a JID and password from the command line instead of hard coding them. We will
use the ``optparse`` module for this, though there are several alternative methods, including
the newer ``argparse`` module.
We want to accept three parameters: the JID for the echo bot, its password, and a flag for
displaying the debugging logs. We also want these to be optional parameters, since passing
a password directly through the command line can be a security risk.
.. code-block:: python
if __name__ == '__main__':
optp = OptionParser()
optp.add_option('-d', '--debug', help='set logging to DEBUG',
action='store_const', dest='loglevel',
const=logging.DEBUG, default=logging.INFO)
optp.add_option("-j", "--jid", dest="jid",
help="JID to use")
optp.add_option("-p", "--password", dest="password",
help="password to use")
opts, args = optp.parse_args()
if opts.jid is None:
opts.jid = raw_input("Username: ")
if opts.password is None:
opts.password = getpass.getpass("Password: ")
Since we included a flag for enabling debugging logs, we need to configure the
``logging`` module to behave accordingly.
.. code-block:: python
if __name__ == '__main__':
# .. option parsing from above ..
logging.basicConfig(level=opts.loglevel,
format='%(levelname)-8s %(message)s')
Connecting to the Server and Processing
---------------------------------------
There are three steps remaining until our echo bot is complete:
1. We need to instantiate the bot.
2. The bot needs to connect to an XMPP server.
3. We have to instruct the bot to start running and processing messages.
Creating the bot is straightforward, but we can also perform some configuration
at this stage. For example, let's say we want our bot to support `service discovery
<http://xmpp.org/extensions/xep-0030.html>`_ and `pings <http://xmpp.org/extensions/xep-0199.html>`_:
.. code-block:: python
if __name__ == '__main__':
# .. option parsing and logging steps from above
xmpp = EchoBot(opts.jid, opts.password)
xmpp.register_plugin('xep_0030') # Service Discovery
xmpp.register_plugin('xep_0199') # Ping
If the ``EchoBot`` class had a hard dependency on a plugin, we could register that plugin in
the ``EchoBot.__init__`` method instead.
.. note::
If you are using the OpenFire server, you will need to include an additional
configuration step. OpenFire supports a different version of SSL than what
most servers and SleekXMPP support.
.. code-block:: python
import ssl
xmpp.ssl_version = ssl.PROTOCOL_SSLv3
Now we're ready to connect and begin echoing messages. If you have the package
``dnspython`` installed, then the :meth:`sleekxmpp.clientxmpp.ClientXMPP` method
will perform a DNS query to find the appropriate server to connect to for the
given JID. If you do not have ``dnspython``, then SleekXMPP will attempt to
connect to the hostname used by the JID, unless an address tuple is supplied
to :meth:`sleekxmpp.clientxmpp.ClientXMPP`.
.. code-block:: python
if __name__ == '__main__':
# .. option parsing & echo bot configuration
if xmpp.connect():
xmpp.process(block=True)
else:
print('Unable to connect')
.. note::
For Google Talk users withouth ``dnspython`` installed, the above code
should look like:
.. code-block:: python
if __name__ == '__main__':
# .. option parsing & echo bot configuration
if xmpp.connect(('talk.google.com', 5222)):
xmpp.process(block=True)
else:
print('Unable to connect')
To begin responding to messages, you'll see we called :meth:`sleekxmpp.basexmpp.BaseXMPP.process`
which will start the event handling, send queue, and XML reader threads. It will also call
the :meth:`sleekxmpp.plugins.base.base_plugin.post_init` method on all registered plugins. By
passing ``block=True`` to :meth:`sleekxmpp.basexmpp.BaseXMPP.process` we are running the
main processing loop in the main thread of execution. The :meth:`sleekxmpp.basexmpp.BaseXMPP.process`
call will not return until after SleekXMPP disconnects. If you need to run the client in the background
for another program, use ``block=False`` to spawn the processing loop in its own thread.
.. note::
Before 1.0, controlling the blocking behaviour of :meth:`sleekxmpp.basexmpp.BaseXMPP.process` was
done via the ``threaded`` argument. This arrangement was a source of confusion because some users
interpreted that as controlling whether or not SleekXMPP used threads at all, instead of how
the processing loop itself was spawned.
The statements ``xmpp.process(threaded=False)`` and ``xmpp.process(block=True)`` are equivalent.
.. _echobot_complete:
The Final Product
-----------------
Here then is what the final result should look like after working through the guide above. The code
can also be found in the SleekXMPP `examples directory <http://github.com/fritzy/SleekXMPP/tree/master/examples>`_.
.. compound::
You can run the code using:
.. code-block:: sh
python echobot.py -d -j echobot@example.com
which will prompt for the password and then begin echoing messages. To test, open
your regular IM client and start a chat with the echo bot. Messages you send to it should
be mirrored back to you. Be careful if you are using the same JID for the echo bot that
you also have logged in with another IM client. Messages could be routed to your IM client instead
of the bot.
.. include:: ../../examples/echo_client.py
:literal:

View File

@@ -0,0 +1,2 @@
Send/Receive IQ Stanzas
=======================

View File

@@ -0,0 +1,2 @@
Mulit-User Chat (MUC) Bot
=========================

View File

@@ -0,0 +1,2 @@
Manage Presence Subscriptions
=============================

View File

@@ -0,0 +1,2 @@
Enable HTTP Proxy Support
=========================

View File

@@ -0,0 +1,2 @@
Send a Message Every 5 Minutes
==============================

View File

@@ -0,0 +1,94 @@
Sign in, Send a Message, and Disconnect
=======================================
.. note::
If you have any issues working through this quickstart guide
or the other tutorials here, please either send a message to the
`mailing list <http://groups.google.com/group/sleekxmpp-discussion>`_
or join the chat room at `sleek@conference.jabber.org
<xmpp:sleek@conference.jabber.org?join>`_.
A common use case for SleekXMPP is to send one-off messages from
time to time. For example, one use case could be sending out a notice when
a shell script finishes a task.
We will create our one-shot bot based on the pattern explained in :ref:`echobot`. To
start, we create a client class based on :class:`ClientXMPP <sleekxmpp.clientxmpp.ClientXMPP>` and
register a handler for the :term:`session_start` event. We will also accept parameters
for the JID that will receive our message, and the string content of the message.
.. code-block:: python
import sleekxmpp
class SendMsgBot(sleekxmpp.ClientXMPP):
def __init__(self, jid, password, recipient, msg):
super(SendMsgBot, self).__init__(jid, password)
self.recipient = recipient
self.msg = msg
self.add_event_handler('session_start', self.start)
def start(self, event):
self.send_presence()
self.get_roster()
Note that as in :ref:`echobot`, we need to include send an initial presence and request
the roster. Next, we want to send our message, and to do that we will use :meth:`send_message <sleekxmpp.basexmpp.BaseXMPP.send_message>`.
.. code-block:: python
def start(self, event):
self.send_presence()
self.get_roster()
self.send_message(mto=self.recipient, mbody=self.msg)
Finally, we need to disconnect the client using :meth:`disconnect <sleekxmpp.xmlstream.XMLStream.disconnect>`.
Now, sent stanzas are placed in a queue to pass them to the send thread. If we were to call
:meth:`disconnect <sleekxmpp.xmlstream.XMLStream.disconnect>` without any parameters, then it is possible
for the client to disconnect before the send queue is processed and the message is actually
sent on the wire. To ensure that our message is processed, we use
:meth:`disconnect(wait=True) <sleekxmpp.xmlstream.XMLStream.disconnect>`.
.. code-block:: python
def start(self, event):
self.send_presence()
self.get_roster()
self.send_message(mto=self.recipient, mbody=self.msg)
self.disconnect(wait=True)
.. warning::
If you happen to be adding stanzas to the send queue faster than the send thread
can process them, then :meth:`disconnect(wait=True) <sleekxmpp.xmlstream.XMLStream.disconnect>`
will block and not disconnect.
Final Product
-------------
.. compound::
The final step is to create a small runner script for initialising our ``SendMsgBot`` class and adding some
basic configuration options. By following the basic boilerplate pattern in :ref:`echobot`, we arrive
at the code below. To experiment with this example, you can use:
.. code-block:: sh
python send_client.py -d -j oneshot@example.com -t someone@example.net -m "This is a message"
which will prompt for the password and then log in, send your message, and then disconnect. To test, open
your regular IM client with the account you wish to send messages to. When you run the ``send_client.py``
example and instruct it to send your IM client account a message, you should receive the message you
gave. If the two JIDs you use also have a mutual presence subscription (they're on each other's buddy lists)
then you will also see the ``SendMsgBot`` client come online and then go offline.
.. include:: ../../examples/send_client.py
:literal:

35
docs/glossary.rst Normal file
View File

@@ -0,0 +1,35 @@
.. _glossary:
Glossary
========
.. glossary::
:sorted:
stream handler
A callback function that accepts stanza objects pulled directly
from the XML stream. A stream handler is encapsulated in a
object that includes a :term:`Matcher` object, and which provides
additional semantics. For example, the ``Waiter`` handler wrapper
blocks thread execution until a matching stanza is received.
event handler
A callback function that responds to events raised by
``XMLStream.event``. An event handler may be marked as
threaded, allowing it to execute outside of the main processing
loop.
stanza object
Informally may refer both to classes which extend ``ElementBase``
or ``StanzaBase``, and to objects of such classes.
A stanza object is a wrapper for an XML object which exposes ``dict``
like interfaces which may be assigned to, read from, or deleted.
stanza plugin
A :term:`stanza object` which has been registered as a potential child
of another stanza object. The plugin stanza may accessed through the
parent stanza using the plugin's ``plugin_attrib`` as an interface.
substanza
See :term:`stanza plugin`

201
docs/guide_xep_0030.rst Normal file
View File

@@ -0,0 +1,201 @@
XEP-0030: Working with Service Discovery
========================================
XMPP networks can be composed of many individual clients, components,
and servers. Determining the JIDs for these entities and the various
features they may support is the role of `XEP-0030, Service
Discovery <http://xmpp.org/extensions/xep-0030.html>`_, or "disco" for short.
Every XMPP entity may possess what are called nodes. A node is just a name for
some aspect of an XMPP entity. For example, if an XMPP entity provides `Ad-Hoc
Commands <http://xmpp.org/extensions/xep-0050.html>`_, then it will have a node
named ``http://jabber.org/protocol/commands`` which will contain information
about the commands provided. Other agents using these ad-hoc commands will
interact with the information provided by this node. Note that the node name is
just an identifier; there is no inherent meaning.
Working with service discovery is about creating and querying these nodes.
According to XEP-0030, a node may contain three types of information:
identities, features, and items. (Further, extensible, information types are
defined in `XEP-0128 <http://xmpp.org/extensions/xep-0128.html>`_, but they are
not yet implemented by SleekXMPP.) SleekXMPP provides methods to configure each
of these node attributes.
Configuring Service Discovery
-----------------------------
The design focus for the XEP-0030 plug-in is handling info and items requests
in a dynamic fashion, allowing for complex policy decisions of who may receive
information and how much, or use alternate backend storage mechanisms for all
of the disco data. To do this, each action that the XEP-0030 plug-in performs
is handed off to what is called a "node handler," which is just a callback
function. These handlers are arranged in a hierarchy that allows for a single
handler to manage an entire domain of JIDs (say for a component), while allowing
other handler functions to override that global behaviour for certain JIDs, or
even further limited to only certain JID and node combinations.
The Dynamic Handler Hierarchy
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
* ``global``: (JID is None, node is None)
Handlers assigned at this level for an action (such as ``add_feature``) provide a global default
behaviour when the action is performed.
* ``jid``: (JID assigned, node is None)
At this level, handlers provide a default behaviour for actions affecting any node owned by the
JID in question. This level is most useful for component connections; there is effectively no
difference between this and the global level when using a client connection.
* ``node``: (JID assigned, node assigned)
A handler for this level is responsible for carrying out an action for only one node, and is the
most specific handler type available. These types of handlers will be most useful for "special"
nodes that require special processing different than others provided by the JID, such as using
access control lists, or consolidating data from other nodes.
Default Static Handlers
~~~~~~~~~~~~~~~~~~~~~~~
The XEP-0030 plug-in provides a default set of handlers that work using in-memory
disco stanzas. Each handler simply performs the appropriate lookup or storage
operation using these stanzas without doing any complex operations such as
checking an ACL, etc.
You may find it necessary at some point to revert a particular node or JID to
using the default, static handlers. To do so, use the method ``make_static()``.
You may also elect to only convert a given set of actions instead.
Creating a Node Handler
~~~~~~~~~~~~~~~~~~~~~~~
Every node handler receives three arguments: the JID, the node, and a data
parameter that will contain the relevant information for carrying out the
handler's action, typically a dictionary.
The JID will always have a value, defaulting to ``xmpp.boundjid.full`` for
components or ``xmpp.boundjid.bare`` for clients. The node value may be None or
a string.
Only handlers for the actions ``get_info`` and ``get_items`` need to have return
values. For these actions, DiscoInfo or DiscoItems stanzas are exepected as
output. It is also acceptable for handlers for these actions to generate an
XMPPError exception when necessary.
Example Node Handler:
+++++++++++++++++++++
Here is one of the built-in default handlers as an example:
.. code-block:: python
def add_identity(self, jid, node, data):
"""
Add a new identity to the 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.
"""
self.add_node(jid, node)
self.nodes[(jid, node)]['info'].add_identity(
data.get('category', ''),
data.get('itype', ''),
data.get('name', None),
data.get('lang', None))
Adding Identities, Features, and Items
--------------------------------------
In order to maintain some backwards compatibility, the methods ``add_identity``,
``add_feature``, and ``add_item`` do not follow the method signature pattern of
the other API methods (i.e. jid, node, then other options), but rather retain
the parameter orders from previous plug-in versions.
Adding an Identity
~~~~~~~~~~~~~~~~~~
Adding an identity may be done using either the older positional notation, or
with keyword parameters. The example below uses the keyword arguments, but in
the same order as expected using positional arguments.
.. code-block:: python
xmpp['xep_0030'].add_identity(category='client',
itype='bot',
name='Sleek',
node='foo',
jid=xmpp.boundjid.full,
lang='no')
The JID and node values determine which handler will be used to perform the
``add_identity`` action.
The ``lang`` parameter allows for adding localized versions of identities using
the ``xml:lang`` attribute.
Adding a Feature
~~~~~~~~~~~~~~~~
The position ordering for ``add_feature()`` is to include the feature, then
specify the node and then the JID. The JID and node values determine which
handler will be used to perform the ``add_feature`` action.
.. code-block:: python
xmpp['xep_0030'].add_feature(feature='jabber:x:data',
node='foo',
jid=xmpp.boundjid.full)
Adding an Item
~~~~~~~~~~~~~~
The parameters to ``add_item()`` are potentially confusing due to the fact that
adding an item requires two JID and node combinations: the JID and node of the
item itself, and the JID and node that will own the item.
.. code-block:: python
xmpp['xep_0030'].add_item(jid='myitemjid@example.com',
name='An Item!',
node='owner_node',
subnode='item_node',
ijid=xmpp.boundjid.full)
.. note::
In this case, the owning JID and node are provided with the
parameters ``ijid`` and ``node``.
Peforming Disco Queries
-----------------------
The methods ``get_info()`` and ``get_items()`` are used to query remote JIDs
and their nodes for disco information. Since these methods are wrappers for
sending Iq stanzas, they also accept all of the parameters of the ``Iq.send()``
method. The ``get_items()`` method may also accept the boolean parameter
``iterator``, which when set to ``True`` will return an iterator object using
the `XEP-0059 <http://xmpp.org/extensions/xep-0059.html>`_ plug-in.
.. code-block:: python
info = self['xep_0030'].get_info(jid='foo@example.com',
node='bar',
ifrom='baz@mycomponent.example.com',
block=True,
timeout=30)
items = self['xep_0030'].get_info(jid='foo@example.com',
node='bar',
iterator=True)
For more examples on how to use basic disco queries, check the ``disco_browser.py``
example in the ``examples`` directory.
Local Queries
~~~~~~~~~~~~~
In some cases, it may be necessary to query the contents of a node owned by the
client itself, or one of a component's many JIDs. The same method is used as for
normal queries, with two differences. First, the parameter ``local=True`` must
be used. Second, the return value will be a DiscoInfo or DiscoItems stanza, not
a full Iq stanza.
.. code-block:: python
info = self['xep_0030'].get_info(node='foo', local=True)
items = self['xep_0030'].get_items(jid='somejid@mycomponent.example.com',
node='bar',
local=True)

View File

@@ -0,0 +1,2 @@
Using Stream Handlers and Matchers
==================================

155
docs/index.rst Normal file
View File

@@ -0,0 +1,155 @@
SleekXMPP
#########
.. sidebar:: Get the Code
.. code-block:: sh
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.
**Stable Releases**
- `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>`_
**Develop Releases**
- `Latest Develop Version <http://github.com/fritzy/SleekXMPP/zipball/develop>`_
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>`_
SleekXMPP is an :ref:`MIT licensed <license>` 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.
Getting Started (with Examples)
-------------------------------
.. toctree::
:maxdepth: 1
getting_started/echobot
getting_started/sendlogout
getting_started/component
getting_started/presence
getting_started/muc
getting_started/proxy
getting_started/scheduler
getting_started/iq
Tutorials, FAQs, and How To Guides
----------------------------------
.. toctree::
:maxdepth: 1
xeps
xmpp_tdg
create_plugin
features
sasl
handlersmatchers
Plugin Guides
~~~~~~~~~~~~~
.. toctree::
:maxdepth: 1
guide_xep_0030
SleekXMPP Architecture and Design
---------------------------------
.. toctree::
:maxdepth: 3
architecture
plugin_arch
API Reference
-------------
.. toctree::
:maxdepth: 2
event_index
api/clientxmpp
api/basexmpp
api/xmlstream
Additional Info
---------------
.. toctree::
:hidden:
glossary
license
* :ref:`license`
* :ref:`glossary`
* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`
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 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>`_)

5
docs/license.rst Normal file
View File

@@ -0,0 +1,5 @@
.. _license:
License (MIT)
=============
.. include:: ../LICENSE

170
docs/make.bat Normal file
View File

@@ -0,0 +1,170 @@
@ECHO OFF
REM Command file for Sphinx documentation
if "%SPHINXBUILD%" == "" (
set SPHINXBUILD=sphinx-build
)
set BUILDDIR=_build
set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% .
if NOT "%PAPER%" == "" (
set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS%
)
if "%1" == "" goto help
if "%1" == "help" (
:help
echo.Please use `make ^<target^>` where ^<target^> is one of
echo. html to make standalone HTML files
echo. dirhtml to make HTML files named index.html in directories
echo. singlehtml to make a single large HTML file
echo. pickle to make pickle files
echo. json to make JSON files
echo. htmlhelp to make HTML files and a HTML help project
echo. qthelp to make HTML files and a qthelp project
echo. devhelp to make HTML files and a Devhelp project
echo. epub to make an epub
echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter
echo. text to make text files
echo. man to make manual pages
echo. changes to make an overview over all changed/added/deprecated items
echo. linkcheck to check all external links for integrity
echo. doctest to run all doctests embedded in the documentation if enabled
goto end
)
if "%1" == "clean" (
for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i
del /q /s %BUILDDIR%\*
goto end
)
if "%1" == "html" (
%SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The HTML pages are in %BUILDDIR%/html.
goto end
)
if "%1" == "dirhtml" (
%SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml.
goto end
)
if "%1" == "singlehtml" (
%SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml.
goto end
)
if "%1" == "pickle" (
%SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle
if errorlevel 1 exit /b 1
echo.
echo.Build finished; now you can process the pickle files.
goto end
)
if "%1" == "json" (
%SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json
if errorlevel 1 exit /b 1
echo.
echo.Build finished; now you can process the JSON files.
goto end
)
if "%1" == "htmlhelp" (
%SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp
if errorlevel 1 exit /b 1
echo.
echo.Build finished; now you can run HTML Help Workshop with the ^
.hhp project file in %BUILDDIR%/htmlhelp.
goto end
)
if "%1" == "qthelp" (
%SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp
if errorlevel 1 exit /b 1
echo.
echo.Build finished; now you can run "qcollectiongenerator" with the ^
.qhcp project file in %BUILDDIR%/qthelp, like this:
echo.^> qcollectiongenerator %BUILDDIR%\qthelp\SleekXMPP.qhcp
echo.To view the help file:
echo.^> assistant -collectionFile %BUILDDIR%\qthelp\SleekXMPP.ghc
goto end
)
if "%1" == "devhelp" (
%SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp
if errorlevel 1 exit /b 1
echo.
echo.Build finished.
goto end
)
if "%1" == "epub" (
%SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The epub file is in %BUILDDIR%/epub.
goto end
)
if "%1" == "latex" (
%SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
if errorlevel 1 exit /b 1
echo.
echo.Build finished; the LaTeX files are in %BUILDDIR%/latex.
goto end
)
if "%1" == "text" (
%SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The text files are in %BUILDDIR%/text.
goto end
)
if "%1" == "man" (
%SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The manual pages are in %BUILDDIR%/man.
goto end
)
if "%1" == "changes" (
%SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes
if errorlevel 1 exit /b 1
echo.
echo.The overview file is in %BUILDDIR%/changes.
goto end
)
if "%1" == "linkcheck" (
%SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck
if errorlevel 1 exit /b 1
echo.
echo.Link check complete; look for any errors in the above output ^
or in %BUILDDIR%/linkcheck/output.txt.
goto end
)
if "%1" == "doctest" (
%SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest
if errorlevel 1 exit /b 1
echo.
echo.Testing of doctests in the sources finished, look at the ^
results in %BUILDDIR%/doctest/output.txt.
goto end
)
:end

2
docs/plugin_arch.rst Normal file
View File

@@ -0,0 +1,2 @@
Plugin Architecture
===================

2
docs/sasl.rst Normal file
View File

@@ -0,0 +1,2 @@
How SASL Authentication Works
=============================

2
docs/xeps.rst Normal file
View File

@@ -0,0 +1,2 @@
Supported XEPS
==============

249
docs/xmpp_tdg.rst Normal file
View File

@@ -0,0 +1,249 @@
Following *XMPP: The Definitive Guide*
======================================
SleekXMPP was featured in the first edition of the O'Reilly book
`XMPP: The Definitive Guide <http://oreilly.com/catalog/9780596521271/>`_
by Peter Saint-Andre, Kevin Smith, and Remko Tronçon. The original source code
for the book's examples can be found at http://github.com/remko/xmpp-tdg. An
updated version of the source code, maintained to stay current with the latest
SleekXMPP release, is available at http://github.com/legastero/xmpp-tdg.
However, since publication, SleekXMPP has advanced from version 0.2.1 to version
1.0 and there have been several major API changes. The most notable is the
introduction of :term:`stanza objects <stanza object>` which have simplified and
standardized interactions with the XMPP XML stream.
What follows is a walk-through of *The Definitive Guide* highlighting the
changes needed to make the code examples work with version 1.0 of SleekXMPP.
These changes have been kept to a minimum to preserve the correlation with
the book's explanations, so be aware that some code may not use current best
practices.
Example 2-2. (Page 26)
----------------------
**Implementation of a basic bot that echoes all incoming messages back to its sender.**
The echo bot example requires a change to the ``handleIncomingMessage`` method
to reflect the use of the ``Message`` :term:`stanza object`. The
``"jid"`` field of the message object should now be ``"from"`` to match the
``from`` attribute of the actual XML message stanza. Likewise, ``"message"``
changes to ``"body"`` to match the ``body`` element of the message stanza.
Updated Code
~~~~~~~~~~~~
.. code-block:: python
def handleIncomingMessage(self, message):
self.xmpp.sendMessage(message["from"], message["body"])
`View full source <http://github.com/legastero/xmpp-tdg/blob/master/code/EchoBot/EchoBot.py>`_ |
`View original code <http://github.com/remko/xmpp-tdg/blob/master/code/EchoBot/EchoBot.py>`_
Example 14-1. (Page 215)
------------------------
**CheshiR IM bot implementation.**
The main event handling method in the Bot class is meant to process both message
events and presence update events. With the new changes in SleekXMPP 1.0,
extracting a CheshiR status "message" from both types of stanzas
requires accessing different attributes. In the case of a message stanza, the
``"body"`` attribute would contain the CheshiR message. For a presence event,
the information is stored in the ``"status"`` attribute. To handle both cases,
we can test the type of the given event object and look up the proper attribute
based on the type.
Like in the EchoBot example, the expression ``event["jid"]`` needs to change
to ``event["from"]`` in order to get a JID object for the stanza's sender.
Because other functions in CheshiR assume that the JID is a string, the ``jid``
attribute is used to access the string version of the JID. A check is also added
in case ``user`` is ``None``, but the check could (and probably should) be
placed in ``addMessageFromUser``.
Another change is needed in ``handleMessageAddedToBackend`` where
an HTML-IM response is created. The HTML content should be enclosed in a single
element, such as a ``<p>`` tag.
Updated Code
~~~~~~~~~~~~
.. code-block:: python
def handleIncomingXMPPEvent(self, event):
msgLocations = {sleekxmpp.stanza.presence.Presence: "status",
sleekxmpp.stanza.message.Message: "body"}
message = event[msgLocations[type(event)]]
user = self.backend.getUserFromJID(event["from"].jid)
if user is not None:
self.backend.addMessageFromUser(message, user)
def handleMessageAddedToBackend(self, message) :
body = message.user + ": " + message.text
htmlBody = "<p><a href='%(uri)s'>%(user)s</a>: %(message)s</p>" % {
"uri": self.url + "/" + message.user,
"user" : message.user, "message" : message.text }
for subscriberJID in self.backend.getSubscriberJIDs(message.user) :
self.xmpp.sendMessage(subscriberJID, body, mhtml=htmlBody)
`View full source <http://github.com/legastero/xmpp-tdg/blob/master/code/CheshiR/Bot.py>`_ |
`View original code <http://github.com/remko/xmpp-tdg/blob/master/code/CheshiR/Bot.py>`_
Example 14-3. (Page 217)
------------------------
**Configurable CheshiR IM bot implementation.**
.. note::
Since the CheshiR examples build on each other, see previous sections for
corrections to code that is not marked as new in the book example.
The main difference for the configurable IM bot is the handling for the
data form in ``handleConfigurationCommand``. The test for equality
with the string ``"1"`` is no longer required; SleekXMPP converts
boolean data form fields to the values ``True`` and ``False``
automatically.
For the method ``handleIncomingXMPPPresence``, the attribute
``"jid"`` is again converted to ``"from"`` to get a JID
object for the presence stanza's sender, and the ``jid`` attribute is
used to access the string version of that JID object. A check is also added in
case ``user`` is ``None``, but the check could (and probably
should) be placed in ``getShouldMonitorPresenceFromUser``.
Updated Code
~~~~~~~~~~~~
.. code-block:: python
def handleConfigurationCommand(self, form, sessionId):
values = form.getValues()
monitorPresence =values["monitorPresence"]
jid = self.xmpp.plugin["xep_0050"].sessions[sessionId]["jid"]
user = self.backend.getUserFromJID(jid)
self.backend.setShouldMonitorPresenceFromUser(user, monitorPresence)
def handleIncomingXMPPPresence(self, event):
user = self.backend.getUserFromJID(event["from"].jid)
if user is not None:
if self.backend.getShouldMonitorPresenceFromUser(user):
self.handleIncomingXMPPEvent(event)
`View full source <http://github.com/legastero/xmpp-tdg/blob/master/code/CheshiR/ConfigurableBot.py>`_ |
`View original code <http://github.com/remko/xmpp-tdg/blob/master/code/CheshiR/ConfigurableBot.py>`_
Example 14-4. (Page 220)
------------------------
**CheshiR IM server component implementation.**
.. note::
Since the CheshiR examples build on each other, see previous sections for
corrections to code that is not marked as new in the book example.
Like several previous examples, a needed change is to replace
``subscription["from"]`` with ``subscription["from"].jid`` because the
``BaseXMPP`` method ``makePresence`` requires the JID to be a string.
A correction needs to be made in ``handleXMPPPresenceProbe`` because a line was
left out of the original implementation; the variable ``user`` is undefined. The
JID of the user can be extracted from the presence stanza's ``from`` attribute.
Since this implementation of CheshiR uses an XMPP component, it must
include a ``from`` attribute in all messages that it sends. Adding the
``from`` attribute is done by including ``mfrom=self.xmpp.jid`` in calls to
``self.xmpp.sendMessage``.
Updated Code
~~~~~~~~~~~~
.. code-block:: python
def handleXMPPPresenceProbe(self, event) :
self.xmpp.sendPresence(pto = event["from"])
def handleXMPPPresenceSubscription(self, subscription) :
if subscription["type"] == "subscribe" :
userJID = subscription["from"].jid
self.xmpp.sendPresenceSubscription(pto=userJID, ptype="subscribed")
self.xmpp.sendPresence(pto = userJID)
self.xmpp.sendPresenceSubscription(pto=userJID, ptype="subscribe")
def handleMessageAddedToBackend(self, message) :
body = message.user + ": " + message.text
for subscriberJID in self.backend.getSubscriberJIDs(message.user) :
self.xmpp.sendMessage(subscriberJID, body, mfrom=self.xmpp.jid)
`View full source <http://github.com/legastero/xmpp-tdg/blob/master/code/CheshiR/SimpleComponent.py>`_ |
`View original code <http://github.com/remko/xmpp-tdg/blob/master/code/CheshiR/SimpleComponent.py>`_
Example 14-6. (Page 223)
------------------------
**CheshiR IM server component with in-band registration support.**
.. note::
Since the CheshiR examples build on each other, see previous sections for
corrections to code that is not marked as new in the book example.
After applying the changes from Example 14-4 above, the registrable component
implementation should work correctly.
.. tip::
To see how to implement in-band registration as a SleekXMPP plugin,
see the tutorial :ref:`tutorial-create-plugin`.
`View full source <http://github.com/legastero/xmpp-tdg/blob/master/code/CheshiR/RegistrableComponent.py>`_ |
`View original code <http://github.com/remko/xmpp-tdg/blob/master/code/CheshiR/RegistrableComponent.py>`_
Example 14-7. (Page 225)
------------------------
**Extended CheshiR IM server component implementation.**
.. note::
Since the CheshiR examples build on each other, see previous
sections for corrections to code that is not marked as new in the book
example.
While the final code example can look daunting with all of the changes
made, it requires very few modifications to work with the latest version of
SleekXMPP. Most differences are the result of CheshiR's backend functions
expecting JIDs to be strings so that they can be stripped to bare JIDs. To
resolve these, use the ``jid`` attribute of the JID objects. Also,
references to ``"message"`` and ``"jid"`` attributes need to
be changed to either ``"body"`` or ``"status"``, and either
``"from"`` or ``"to"`` depending on if the object is a message
or presence stanza and which of the JIDs from the stanza is needed.
Updated Code
~~~~~~~~~~~~
.. code-block:: python
def handleIncomingXMPPMessage(self, event) :
message = self.addRecipientToMessage(event["body"], event["to"].jid)
user = self.backend.getUserFromJID(event["from"].jid)
self.backend.addMessageFromUser(message, user)
def handleIncomingXMPPPresence(self, event) :
if event["to"].jid == self.componentDomain :
user = self.backend.getUserFromJID(event["from"].jid)
self.backend.addMessageFromUser(event["status"], user)
...
def handleXMPPPresenceSubscription(self, subscription) :
if subscription["type"] == "subscribe" :
userJID = subscription["from"].jid
user = self.backend.getUserFromJID(userJID)
contactJID = subscription["to"]
self.xmpp.sendPresenceSubscription(
pfrom=contactJID, pto=userJID, ptype="subscribed", pnick=user)
self.sendPresenceOfContactToUser(contactJID=contactJID, userJID=userJID)
if contactJID == self.componentDomain :
self.sendAllContactSubscriptionRequestsToUser(userJID)
`View full source <http://github.com/legastero/xmpp-tdg/blob/master/code/CheshiR/Component.py>`_ |
`View original code <http://github.com/remko/xmpp-tdg/blob/master/code/CheshiR/Component.py>`_

View File

@@ -24,6 +24,8 @@ import sleekxmpp
if sys.version_info < (3, 0):
reload(sys)
sys.setdefaultencoding('utf8')
else:
raw_input = input
class CommandBot(sleekxmpp.ClientXMPP):
@@ -79,6 +81,7 @@ class CommandBot(sleekxmpp.ClientXMPP):
here to persist across handler callbacks.
"""
form = self['xep_0004'].makeForm('form', 'Greeting')
form['instructions'] = 'Send a custom greeting to a JID'
form.addField(var='greeting',
ftype='text-single',
label='Your greeting')
@@ -123,8 +126,10 @@ class CommandBot(sleekxmpp.ClientXMPP):
form = payload
greeting = form['values']['greeting']
self.send_message(mto=session['from'],
mbody="%s, World!" % greeting)
mbody="%s, World!" % greeting,
mtype='chat')
# Having no return statement is the same as unsetting the 'payload'
# and 'next' session values and returning the session.
@@ -176,6 +181,7 @@ if __name__ == '__main__':
xmpp.register_plugin('xep_0030') # Service Discovery
xmpp.register_plugin('xep_0004') # Data Forms
xmpp.register_plugin('xep_0050') # Adhoc Commands
xmpp.register_plugin('xep_0199', {'keepalive': True, 'frequency':15})
# If you are working with an OpenFire server, you may need
# to adjust the SSL version used:

View File

@@ -24,6 +24,8 @@ import sleekxmpp
if sys.version_info < (3, 0):
reload(sys)
sys.setdefaultencoding('utf8')
else:
raw_input = input
class CommandUserBot(sleekxmpp.ClientXMPP):
@@ -136,6 +138,7 @@ class CommandUserBot(sleekxmpp.ClientXMPP):
# The session will automatically be cleared if no error
# handler is provided.
self['xep_0050'].terminate_command(session)
self.disconnect()
if __name__ == '__main__':
@@ -176,7 +179,7 @@ if __name__ == '__main__':
if opts.other is None:
opts.other = raw_input("JID Providing Commands: ")
if opts.greeting is None:
opts.other = raw_input("Greeting: ")
opts.greeting = raw_input("Greeting: ")
# Setup the CommandBot and register plugins. Note that while plugins may
# have interdependencies, the order in which you register them does

View File

@@ -27,6 +27,8 @@ from sleekxmpp.xmlstream.stanzabase import ET, registerStanzaPlugin
if sys.version_info < (3, 0):
reload(sys)
sys.setdefaultencoding('utf8')
else:
raw_input = input
class Config(ElementBase):

View File

@@ -25,6 +25,8 @@ import sleekxmpp
if sys.version_info < (3, 0):
reload(sys)
sys.setdefaultencoding('utf8')
else:
raw_input = input
class Disco(sleekxmpp.ClientXMPP):

View File

@@ -24,6 +24,8 @@ import sleekxmpp
if sys.version_info < (3, 0):
reload(sys)
sys.setdefaultencoding('utf8')
else:
raw_input = input
class EchoBot(sleekxmpp.ClientXMPP):
@@ -76,7 +78,8 @@ class EchoBot(sleekxmpp.ClientXMPP):
for stanza objects and the Message stanza to see
how it may be used.
"""
msg.reply("Thanks for sending\n%(body)s" % msg).send()
if msg['type'] in ('chat', 'normal'):
msg.reply("Thanks for sending\n%(body)s" % msg).send()
if __name__ == '__main__':

View File

@@ -23,6 +23,8 @@ import sleekxmpp
if sys.version_info < (3, 0):
reload(sys)
sys.setdefaultencoding('utf8')
else:
raw_input = input
class MUCBot(sleekxmpp.ClientXMPP):
@@ -59,7 +61,7 @@ class MUCBot(sleekxmpp.ClientXMPP):
# muc::room@server::got_online, or muc::room@server::got_offline.
self.add_event_handler("muc::%s::got_online" % self.room,
self.muc_online)
def start(self, event):
"""
@@ -76,15 +78,15 @@ class MUCBot(sleekxmpp.ClientXMPP):
"""
self.getRoster()
self.sendPresence()
self.plugin['xep_0045'].joinMUC(self.room,
self.nick,
self.plugin['xep_0045'].joinMUC(self.room,
self.nick,
# If a room password is needed, use:
# password=the_room_password,
wait=True)
def muc_message(self, msg):
"""
Process incoming message stanzas from any chat room. Be aware
Process incoming message stanzas from any chat room. Be aware
that if you also have any handlers for the 'message' event,
message stanzas may be processed by both handlers, so check
the 'type' attribute when using a 'message' event handler.
@@ -96,7 +98,7 @@ class MUCBot(sleekxmpp.ClientXMPP):
otherwise you will create an infinite loop responding
to your own messages.
This handler will reply to messages that mention
This handler will reply to messages that mention
the bot's nickname.
Arguments:
@@ -112,12 +114,12 @@ class MUCBot(sleekxmpp.ClientXMPP):
def muc_online(self, presence):
"""
Process a presence stanza from a chat room. In this case,
presences from users that have just come online are
presences from users that have just come online are
handled by sending a welcome message that includes
the user's nickname and role in the room.
Arguments:
presence -- The received presence stanza. See the
presence -- The received presence stanza. See the
documentation for the Presence stanza
to see how else it may be used.
"""

View File

@@ -24,6 +24,8 @@ import sleekxmpp
if sys.version_info < (3, 0):
reload(sys)
sys.setdefaultencoding('utf8')
else:
raw_input = input
class PingTest(sleekxmpp.ClientXMPP):

View File

@@ -24,6 +24,8 @@ import sleekxmpp
if sys.version_info < (3, 0):
reload(sys)
sys.setdefaultencoding('utf8')
else:
raw_input = input
class EchoBot(sleekxmpp.ClientXMPP):

173
examples/roster_browser.py Normal file
View File

@@ -0,0 +1,173 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
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 sys
import time
import logging
import getpass
import threading
from optparse import OptionParser
import sleekxmpp
from sleekxmpp.exceptions import IqError, IqTimeout
# Python versions before 3.0 do not use UTF-8 encoding
# by default. To ensure that Unicode is handled properly
# throughout SleekXMPP, we will set the default encoding
# ourselves to UTF-8.
if sys.version_info < (3, 0):
reload(sys)
sys.setdefaultencoding('utf8')
else:
raw_input = input
class RosterBrowser(sleekxmpp.ClientXMPP):
"""
A basic script for dumping a client's roster to
the command line.
"""
def __init__(self, jid, password):
sleekxmpp.ClientXMPP.__init__(self, jid, password)
# The session_start event will be triggered when
# the bot establishes its connection with the server
# and the XML streams are ready for use. We want to
# listen for this event so that we we can intialize
# our roster. We need threaded=True so that the
# session_start handler doesn't block event processing
# while we wait for presence stanzas to arrive.
self.add_event_handler("session_start", self.start, threaded=True)
self.add_event_handler("changed_status", self.wait_for_presences)
self.received = set()
self.presences_received = threading.Event()
def start(self, event):
"""
Process the session_start event.
Typical actions for the session_start event are
requesting the roster and broadcasting an intial
presence stanza.
Arguments:
event -- An empty dictionary. The session_start
event does not provide any additional
data.
"""
try:
self.get_roster()
except IqError as err:
print('Error: %' % err.iq['error']['condition'])
except IqTimeout:
print('Error: Request timed out')
self.send_presence()
print('Waiting for presence updates...\n')
self.presences_received.wait(5)
print('Roster for %s' % self.boundjid.bare)
groups = self.client_roster.groups()
for group in groups:
print('\n%s' % group)
print('-' * 72)
for jid in groups[group]:
sub = self.client_roster[jid]['subscription']
name = self.client_roster[jid]['name']
if self.client_roster[jid]['name']:
print(' %s (%s) [%s]' % (name, jid, sub))
else:
print(' %s [%s]' % (jid, sub))
connections = self.client_roster.presence(jid)
for res, pres in connections.items():
show = 'available'
if pres['show']:
show = pres['show']
print(' - %s (%s)' % (res, show))
if pres['status']:
print(' %s' % pres['status'])
self.disconnect()
def wait_for_presences(self, pres):
"""
Track how many roster entries have received presence updates.
"""
self.received.add(pres['from'].bare)
if len(self.received) >= len(self.client_roster.keys()):
self.presences_received.set()
else:
self.presences_received.clear()
if __name__ == '__main__':
# Setup the command line arguments.
optp = OptionParser()
optp.add_option('-q','--quiet', help='set logging to ERROR',
action='store_const',
dest='loglevel',
const=logging.ERROR,
default=logging.ERROR)
optp.add_option('-d','--debug', help='set logging to DEBUG',
action='store_const',
dest='loglevel',
const=logging.DEBUG,
default=logging.ERROR)
optp.add_option('-v','--verbose', help='set logging to COMM',
action='store_const',
dest='loglevel',
const=5,
default=logging.ERROR)
# JID and password options.
optp.add_option("-j", "--jid", dest="jid",
help="JID to use")
optp.add_option("-p", "--password", dest="password",
help="password to use")
opts,args = optp.parse_args()
# Setup logging.
logging.basicConfig(level=opts.loglevel,
format='%(levelname)-8s %(message)s')
if opts.jid is None:
opts.jid = raw_input("Username: ")
if opts.password is None:
opts.password = getpass.getpass("Password: ")
xmpp = RosterBrowser(opts.jid, opts.password)
# If you are working with an OpenFire server, you may need
# to adjust the SSL version used:
# xmpp.ssl_version = ssl.PROTOCOL_SSLv3
# If you want to verify the SSL certificates offered by a server:
# xmpp.ca_certs = "path/to/ca/cert"
# Connect to the XMPP server and start processing XMPP stanzas.
if xmpp.connect():
# If you do not have the pydns library installed, you will need
# to manually specify the name of the server if it does not match
# the one in the JID. For example, to use Google Talk you would
# need to use:
#
# if xmpp.connect(('talk.google.com', 5222)):
# ...
xmpp.process(threaded=False)
else:
print("Unable to connect.")

144
examples/send_client.py Executable file
View File

@@ -0,0 +1,144 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
SleekXMPP: The Sleek XMPP Library
Copyright (C) 2010 Nathanael C. Fritz
This file is part of SleekXMPP.
See the file LICENSE for copying permission.
"""
import sys
import logging
import time
import getpass
from optparse import OptionParser
import sleekxmpp
# Python versions before 3.0 do not use UTF-8 encoding
# by default. To ensure that Unicode is handled properly
# throughout SleekXMPP, we will set the default encoding
# ourselves to UTF-8.
if sys.version_info < (3, 0):
reload(sys)
sys.setdefaultencoding('utf8')
else:
raw_input = input
class SendMsgBot(sleekxmpp.ClientXMPP):
"""
A basic SleekXMPP bot that will log in, send a message,
and then log out.
"""
def __init__(self, jid, password, recipient, message):
sleekxmpp.ClientXMPP.__init__(self, jid, password)
# The message we wish to send, and the JID that
# will receive it.
self.recipient = recipient
self.msg = message
# The session_start event will be triggered when
# the bot establishes its connection with the server
# and the XML streams are ready for use. We want to
# listen for this event so that we we can intialize
# our roster.
self.add_event_handler("session_start", self.start)
def start(self, event):
"""
Process the session_start event.
Typical actions for the session_start event are
requesting the roster and broadcasting an intial
presence stanza.
Arguments:
event -- An empty dictionary. The session_start
event does not provide any additional
data.
"""
self.send_presence()
self.get_roster()
self.send_message(mto=self.recipient,
mbody=self.msg,
mtype='chat')
# Using wait=True ensures that the send queue will be
# emptied before ending the session.
self.disconnect(wait=True)
if __name__ == '__main__':
# Setup the command line arguments.
optp = OptionParser()
# Output verbosity options.
optp.add_option('-q', '--quiet', help='set logging to ERROR',
action='store_const', dest='loglevel',
const=logging.ERROR, default=logging.INFO)
optp.add_option('-d', '--debug', help='set logging to DEBUG',
action='store_const', dest='loglevel',
const=logging.DEBUG, default=logging.INFO)
optp.add_option('-v', '--verbose', help='set logging to COMM',
action='store_const', dest='loglevel',
const=5, default=logging.INFO)
# JID and password options.
optp.add_option("-j", "--jid", dest="jid",
help="JID to use")
optp.add_option("-p", "--password", dest="password",
help="password to use")
optp.add_option("-t", "--to", dest="to",
help="JID to send the message to")
optp.add_option("-m", "--message", dest="message",
help="message to send")
opts, args = optp.parse_args()
# Setup logging.
logging.basicConfig(level=opts.loglevel,
format='%(levelname)-8s %(message)s')
if opts.jid is None:
opts.jid = raw_input("Username: ")
if opts.password is None:
opts.password = getpass.getpass("Password: ")
if opts.to is None:
opts.to = raw_input("Send To: ")
if opts.message is None:
opts.message = raw_input("Message: ")
# Setup the EchoBot and register plugins. Note that while plugins may
# have interdependencies, the order in which you register them does
# not matter.
xmpp = SendMsgBot(opts.jid, opts.password, opts.to, opts.message)
xmpp.register_plugin('xep_0030') # Service Discovery
xmpp.register_plugin('xep_0199') # XMPP Ping
# If you are working with an OpenFire server, you may need
# to adjust the SSL version used:
# xmpp.ssl_version = ssl.PROTOCOL_SSLv3
# If you want to verify the SSL certificates offered by a server:
# xmpp.ca_certs = "path/to/ca/cert"
# Connect to the XMPP server and start processing XMPP stanzas.
if xmpp.connect():
# If you do not have the pydns library installed, you will need
# to manually specify the name of the server if it does not match
# the one in the JID. For example, to use Google Talk you would
# need to use:
#
# if xmpp.connect(('talk.google.com', 5222)):
# ...
xmpp.process(threaded=False)
print("Done")
else:
print("Unable to connect.")

191
setup.py
View File

@@ -1,94 +1,97 @@
#!/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 file,
# which you should have received as part of this distribution.
#
# from ez_setup import use_setuptools
from distutils.core import setup
import sys
import sleekxmpp
# 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 = sleekxmpp.__version__
DESCRIPTION = 'SleekXMPP is an elegant Python library for XMPP (aka Jabber, Google Talk, etc).'
with open('README') as readme:
LONG_DESCRIPTION = '\n'.join(readme)
CLASSIFIERS = [ 'Intended Audience :: Developers',
'License :: OSI Approved :: MIT License',
'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/xmlstream',
'sleekxmpp/xmlstream/matcher',
'sleekxmpp/xmlstream/handler',
'sleekxmpp/plugins',
'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_0085',
'sleekxmpp/plugins/xep_0086',
'sleekxmpp/plugins/xep_0092',
'sleekxmpp/plugins/xep_0128',
'sleekxmpp/plugins/xep_0199',
'sleekxmpp/plugins/xep_0202',
'sleekxmpp/plugins/xep_0203',
'sleekxmpp/plugins/xep_0224',
'sleekxmpp/plugins/xep_0249',
'sleekxmpp/features',
'sleekxmpp/features/feature_mechanisms',
'sleekxmpp/features/feature_mechanisms/stanza',
'sleekxmpp/features/feature_starttls',
'sleekxmpp/features/feature_bind',
'sleekxmpp/features/feature_session',
'sleekxmpp/thirdparty',
'sleekxmpp/thirdparty/suelta',
'sleekxmpp/thirdparty/suelta/mechanisms',
]
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 = [ 'tlslite', 'pythondns' ],
)
#!/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 file,
# which you should have received as part of this distribution.
#
# from ez_setup import use_setuptools
from distutils.core import setup
import sys
import sleekxmpp
# 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 = sleekxmpp.__version__
DESCRIPTION = 'SleekXMPP is an elegant Python library for XMPP (aka Jabber, Google Talk, etc).'
with open('README.rst') as readme:
LONG_DESCRIPTION = '\n'.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_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 = [ 'tlslite', 'pythondns' ],
)

View File

@@ -15,5 +15,5 @@ from sleekxmpp.xmlstream import XMLStream, RestartStream
from sleekxmpp.xmlstream.matcher import *
from sleekxmpp.xmlstream.stanzabase import StanzaBase, ET
__version__ = '1.0beta6.1'
__version_info__ = (1, 0, 0, 'beta6', 1)
__version__ = '1.0rc1'
__version_info__ = (1, 0, 0, 'rc1', 0)

View File

@@ -13,7 +13,8 @@ import copy
import logging
import sleekxmpp
from sleekxmpp import plugins
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
@@ -78,7 +79,7 @@ class BaseXMPP(XMLStream):
send_presence_subscribe -- Send a subscription request.
"""
def __init__(self, default_ns='jabber:client'):
def __init__(self, jid='', default_ns='jabber:client'):
"""
Adapt an XML stream for use with XMPP.
@@ -94,12 +95,16 @@ class BaseXMPP(XMLStream):
self.stream_ns = 'http://etherx.jabber.org/streams'
self.namespace_map[self.stream_ns] = 'stream'
self.boundjid = JID("")
self.boundjid = JID(jid)
self.plugin = {}
self.plugin_config = {}
self.plugin_whitelist = []
self.roster = {}
self.roster = roster.Roster(self)
self.roster.add(self.boundjid.bare)
self.client_roster = self.roster[self.boundjid.bare]
self.is_component = False
self.auto_authorize = True
self.auto_subscribe = True
@@ -122,10 +127,30 @@ class BaseXMPP(XMLStream):
MatchXPath("{%s}error" % self.stream_ns),
self._handle_stream_error))
self.add_event_handler('presence_subscribe',
self._handle_subscribe)
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)
@@ -138,6 +163,17 @@ class BaseXMPP(XMLStream):
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.
Overrides XMLStream.start_stream_handler.
Arguments:
xml -- The incoming stream's root element.
"""
self.stream_id = xml.get('id', '')
def process(self, *args, **kwargs):
"""
Overrides XMLStream.process.
@@ -198,6 +234,10 @@ class BaseXMPP(XMLStream):
# 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)
@@ -431,7 +471,7 @@ class BaseXMPP(XMLStream):
msubject -- Optional subject for the message.
mtype -- The message's type, such as 'chat' or 'groupchat'.
mhtml -- Optional HTML body content.
mfrom -- The sender of the message. If sending from a client,
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.
mnick -- Optional nickname of the sender.
@@ -446,7 +486,7 @@ class BaseXMPP(XMLStream):
return message
def make_presence(self, pshow=None, pstatus=None, ppriority=None,
pto=None, ptype=None, pfrom=None):
pto=None, ptype=None, pfrom=None, pnick=None):
"""
Create and initialize a new Presence stanza.
@@ -457,14 +497,16 @@ class BaseXMPP(XMLStream):
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.
"""
presence = self.Presence(stype=ptype, sfrom=pfrom, sto=pto)
if pshow is not None:
presence['type'] = pshow
if pfrom is None:
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,
@@ -472,13 +514,22 @@ class BaseXMPP(XMLStream):
"""
Create, initialize, and send a Message stanza.
Arguments:
mto -- The recipient of the message.
mbody -- The main contents of the message.
msubject -- Optional subject for the message.
mtype -- The message's type, such as 'chat' or 'groupchat'.
mhtml -- Optional HTML body content.
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.
mnick -- Optional nickname of the sender.
"""
self.makeMessage(mto, mbody, msubject, mtype,
mhtml, mfrom, mnick).send()
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):
pto=None, pfrom=None, ptype=None, pnick=None):
"""
Create, initialize, and send a Presence stanza.
@@ -489,13 +540,20 @@ class BaseXMPP(XMLStream):
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.makePresence(pshow, pstatus, ppriority, pto,
ptype=ptype, pfrom=pfrom).send()
# Unexpected errors may occur if
if not self.sentpresence:
self.event('sent_presence')
self.sentpresence = True
# 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):
@@ -598,7 +656,7 @@ class BaseXMPP(XMLStream):
def _handle_disconnected(self, event):
"""When disconnected, reset the roster"""
self.roster = {}
self.roster.reset()
def _handle_stream_error(self, error):
self.event('stream_error', error)
@@ -607,6 +665,65 @@ class BaseXMPP(XMLStream):
"""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 self.auto_authorize is True. They
will be rejected if self.auto_authorize is False. Setting
self.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 self.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.
@@ -624,97 +741,30 @@ class BaseXMPP(XMLStream):
not presence['type'] in presence.showtypes:
return
# Strip the information from the stanza.
jid = presence['from'].bare
resource = presence['from'].resource
show = presence['type']
status = presence['status']
priority = presence['priority']
was_offline = False
got_online = False
old_roster = self.roster.get(jid, {}).get(resource, {})
# Create a new roster entry if needed.
if not jid in self.roster:
self.roster[jid] = {'groups': [],
'name': '',
'subscription': 'none',
'presence': {},
'in_roster': False}
# Alias to simplify some references.
connections = self.roster[jid].get('presence', {})
# Determine if the user has just come online.
if not resource in connections:
if show == 'available' or show in presence.showtypes:
got_online = True
was_offline = True
connections[resource] = {}
if connections[resource].get('show', 'unavailable') == 'unavailable':
was_offline = True
# Update the roster's state for this JID's resource.
connections[resource] = {'show': show,
'status': status,
'priority': priority}
name = self.roster[jid].get('name', '')
# Remove unneeded state information after a resource
# disconnects. Determine if this was the last connection
# for the JID.
if show == 'unavailable':
log.debug("%s %s got offline" % (jid, resource))
del connections[resource]
if not connections and \
not self.roster[jid].get('in_roster', False):
del self.roster[jid]
if not was_offline:
self.event("got_offline", presence)
else:
return False
name = '(%s) ' % name if name else ''
# Presence state has changed.
self.event("changed_status", presence)
if got_online:
self.event("got_online", presence)
log.debug("STATUS: %s%s/%s[%s]: %s" % (name, jid, resource,
show, status))
def _handle_subscribe(self, presence):
def exception(self, exception):
"""
Automatically managage subscription requests.
Process any uncaught exceptions, notably IqError and
IqTimeout exceptions.
Subscription behavior is controlled by the settings
self.auto_authorize and self.auto_subscribe.
Overrides XMLStream.exception.
auto_auth auto_sub Result:
True True Create bi-directional subsriptions.
True False Create only directed subscriptions.
False * Decline all subscriptions.
None * Disable automatic handling and use
a custom handler.
Arguments:
exception -- An unhandled exception object.
"""
presence.reply()
presence['to'] = presence['to'].bare
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')
else:
log.exception(exception)
# We are using trinary logic, so conditions have to be
# more explicit than usual.
if self.auto_authorize == True:
presence['type'] = 'subscribed'
presence.send()
if self.auto_subscribe:
presence['type'] = 'subscribe'
presence.send()
elif self.auto_authorize == False:
presence['type'] = 'unsubscribed'
presence.send()
# Restore the old, lowercased name for backwards compatibility.
basexmpp = BaseXMPP

View File

@@ -27,11 +27,12 @@ from sleekxmpp.xmlstream.matcher import *
from sleekxmpp.xmlstream.handler import *
# Flag indicating if DNS SRV records are available for use.
SRV_SUPPORT = True
try:
import dns.resolver
except:
SRV_SUPPORT = False
except ImportError:
DNSPYTHON = False
else:
DNSPYTHON = True
log = logging.getLogger(__name__)
@@ -71,14 +72,14 @@ class ClientXMPP(BaseXMPP):
when calling register_plugins.
escape_quotes -- Deprecated.
"""
BaseXMPP.__init__(self, 'jabber:client')
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.srv_support = SRV_SUPPORT
self.default_port = 5222
self.stream_header = "<stream:stream to='%s' %s %s version='1.0'>" % (
self.boundjid.host,
@@ -133,55 +134,43 @@ class ClientXMPP(BaseXMPP):
connection. Defaults to True.
"""
self.session_started_event.clear()
if not address or len(address) < 2:
if not self.srv_support:
log.debug("Did not supply (address, port) to connect" + \
" to and no SRV support is installed" + \
" (http://www.dnspython.org)." + \
" Continuing to attempt connection, using" + \
" server hostname from JID.")
else:
log.debug("Since no address is supplied," + \
"attempting SRV lookup.")
try:
xmpp_srv = "_xmpp-client._tcp.%s" % self.boundjid.host
answers = dns.resolver.query(xmpp_srv, dns.rdatatype.SRV)
except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer):
log.debug("No appropriate SRV record found." + \
" Using JID server name.")
except (dns.exception.Timeout,):
log.debug("DNS resolution timed out.")
else:
# Pick a random server, weighted by priority.
addresses = {}
intmax = 0
topprio = 65535
for answer in answers:
topprio = min(topprio, answer.priority)
for answer in answers:
if answer.priority == topprio:
intmax += answer.weight
addresses[intmax] = (answer.target.to_text()[:-1],
answer.port)
#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
if not address:
# If all else fails, use the server from the JID.
address = (self.boundjid.host, 5222)
return XMLStream.connect(self, address[0], address[1],
use_tls=use_tls, reattempt=reattempt)
def get_dns_records(self, domain, port=None):
"""
Get the DNS records for a domain.
Overriddes XMLStream.get_dns_records to use SRV.
Arguments:
domain -- The domain in question.
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.
@@ -221,15 +210,8 @@ class ClientXMPP(BaseXMPP):
Will be executed when the roster is received.
Implies block=False.
"""
iq = self.Iq()
iq['type'] = 'set'
iq['roster']['items'] = {jid: {'name': name,
'subscription': subscription,
'groups': groups}}
response = iq.send(block, timeout, callback)
if response in [False, None] or not isinstance(response, Iq):
return response
return response['type'] == 'result'
return self.client_roster.updtae(jid, name, subscription, groups,
block, timeout, callback)
def del_roster_item(self, jid):
"""
@@ -239,7 +221,7 @@ class ClientXMPP(BaseXMPP):
Arguments:
jid -- The JID of the item to remove.
"""
return self.update_roster(jid, subscription='remove')
return self.client_roster.remove(jid)
def get_roster(self, block=True, timeout=None, callback=None):
"""
@@ -261,12 +243,7 @@ class ClientXMPP(BaseXMPP):
iq.enable('roster')
response = iq.send(block, timeout, callback)
if response == False:
self.event('roster_timeout')
if response in [False, None] or not isinstance(response, Iq):
return response
else:
if callback is None:
return self._handle_roster(response, request=True)
def _handle_connected(self, event=None):
@@ -310,13 +287,13 @@ class ClientXMPP(BaseXMPP):
"""
if iq['type'] == 'set' or (iq['type'] == 'result' and request):
for jid in iq['roster']['items']:
if not jid in self.roster:
self.roster[jid] = {'groups': [],
'name': '',
'subscription': 'none',
'presence': {},
'in_roster': True}
self.roster[jid].update(iq['roster']['items'][jid])
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)

View File

@@ -58,7 +58,7 @@ class ComponentXMPP(BaseXMPP):
default_ns = 'jabber:client'
else:
default_ns = 'jabber:component:accept'
BaseXMPP.__init__(self, default_ns)
BaseXMPP.__init__(self, jid, default_ns)
self.auto_authorize = None
self.stream_header = "<stream:stream %s %s to='%s'>" % (
@@ -68,8 +68,8 @@ class ComponentXMPP(BaseXMPP):
self.stream_footer = "</stream:stream>"
self.server_host = host
self.server_port = port
self.set_jid(jid)
self.secret = secret
self.plugin_config = plugin_config
self.plugin_whitelist = plugin_whitelist
self.is_component = True
@@ -78,6 +78,8 @@ class ComponentXMPP(BaseXMPP):
Callback('Handshake',
MatchXPath('{jabber:component:accept}handshake'),
self._handle_handshake))
self.add_event_handler('presence_probe',
self._handle_probe)
def connect(self):
"""
@@ -115,11 +117,13 @@ class ComponentXMPP(BaseXMPP):
Once the streams are established, attempt to handshake
with the server to be accepted as a component.
Overrides XMLStream.start_stream_handler.
Overrides BaseXMPP.start_stream_handler.
Arguments:
xml -- The incoming stream's root element.
"""
BaseXMPP.start_stream_handler(self, xml)
# Construct a hash of the stream ID and the component secret.
sid = xml.get('id', '')
pre_hash = '%s%s' % (sid, self.secret)
@@ -140,3 +144,8 @@ class ComponentXMPP(BaseXMPP):
"""
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)

View File

@@ -20,9 +20,9 @@ class XMPPError(Exception):
Meant for use in SleekXMPP plugins and applications using SleekXMPP.
"""
def __init__(self, condition='undefined-condition', text=None, etype=None,
extension=None, extension_ns=None, extension_args=None,
clear=True):
def __init__(self, condition='undefined-condition', text=None,
etype='cancel', extension=None, extension_ns=None,
extension_args=None, clear=True):
"""
Create a new XMPPError exception.
@@ -31,8 +31,10 @@ class XMPPError(Exception):
Arguments:
condition -- The XMPP defined error condition.
Defaults to 'undefined-condition'.
text -- Human readable text describing the error.
etype -- The XMPP error type, such as cancel or modify.
Defaults to 'cancel'.
extension -- Tag name of the extension's XML content.
extension_ns -- XML namespace of the extensions' XML content.
extension_args -- Content and attributes for the extension
@@ -52,3 +54,33 @@ class XMPPError(Exception):
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')
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'])
self.iq = iq

View File

@@ -29,6 +29,8 @@ class feature_mechanisms(base_plugin):
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
@@ -48,7 +50,8 @@ class feature_mechanisms(base_plugin):
username=self.xmpp.boundjid.user,
sec_query=suelta.sec_query_allow,
request_values=sasl_callback,
tls_active=tls_active)
tls_active=tls_active,
mech=self.use_mech)
register_stanza_plugin(StreamFeatures, stanza.Mechanisms)

View File

@@ -9,3 +9,5 @@ __all__ = ['xep_0004', 'xep_0009', 'xep_0012', 'xep_0030', 'xep_0033',
'xep_0045', 'xep_0050', 'xep_0060', 'xep_0066', 'xep_0082',
'xep_0085', 'xep_0086', 'xep_0092', 'xep_0128', 'xep_0199',
'xep_0203', 'xep_0224', 'xep_0249', 'gmail_notify']
# Don't automatically load xep_0078

View File

@@ -1,395 +0,0 @@
"""
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 copy
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
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
self.field = FieldAccessor(self)
def setup(self, xml=None):
if ElementBase.setup(self, xml): #if we had to generate xml
self['type'] = 'form'
def addField(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['label'] = label
field['desc'] = desc
field['required'] = required
field['value'] = value
if options is not None:
field['options'] = options
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 addItem(self, values):
itemXML = ET.Element('{%s}item' % self.namespace)
self.xml.append(itemXML)
reported_vars = self['reported'].keys()
for var in reported_vars:
fieldXML = ET.Element('{%s}field' % FormField.namespace)
itemXML.append(fieldXML)
field = FormField(xml=fieldXML)
field['var'] = var
field['value'] = values.get(var, None)
def addReported(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 delFields(self):
fieldsXML = self.xml.findall('{%s}field' % FormField.namespace)
for fieldXML in fieldsXML:
self.xml.remove(fieldXML)
def delInstructions(self):
instsXML = self.xml.findall('{%s}instructions')
for instXML in instsXML:
self.xml.remove(instXML)
def delItems(self):
itemsXML = self.xml.find('{%s}item' % self.namespace)
for itemXML in itemsXML:
self.xml.remove(itemXML)
def delReported(self):
reportedXML = self.xml.find('{%s}reported' % self.namespace)
if reportedXML is not None:
self.xml.remove(reportedXML)
def getFields(self, use_dict=False):
fields = {} if use_dict else []
fieldsXML = self.xml.findall('{%s}field' % FormField.namespace)
for fieldXML in fieldsXML:
field = FormField(xml=fieldXML)
if use_dict:
fields[field['var']] = field
else:
fields.append((field['var'], field))
return fields
def getInstructions(self):
instructions = ''
instsXML = self.xml.findall('{%s}instructions' % self.namespace)
return "\n".join([instXML.text for instXML in instsXML])
def getItems(self):
items = []
itemsXML = self.xml.findall('{%s}item' % self.namespace)
for itemXML in itemsXML:
item = {}
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 getReported(self):
fields = {}
fieldsXML = self.xml.findall('{%s}reported/{%s}field' % (self.namespace,
FormField.namespace))
for fieldXML in fieldsXML:
field = FormField(xml=fieldXML)
fields[field['var']] = field
return fields
def getValues(self):
values = {}
fields = self.getFields(use_dict=True)
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 setFields(self, fields, default=None):
del self['fields']
for field_data in fields:
var = field_data[0]
field = field_data[1]
field['var'] = var
self.addField(**field)
def setInstructions(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 setItems(self, items):
for item in items:
self.addItem(item)
def setReported(self, reported, default=None):
for var in reported:
field = reported[var]
field['var'] = var
self.addReported(var, **field)
def setValues(self, values):
fields = self.getFields(use_dict=True)
for field in values:
fields[field]['value'] = values[field]
def merge(self, other):
new = copy.copy(self)
if type(other) == dict:
new.setValues(other)
return new
nfields = new.getFields(use_dict=True)
ofields = other.getFields(use_dict=True)
nfields.update(ofields)
new.setFields([(x, nfields[x]) for x in nfields])
return new
class FieldAccessor(object):
def __init__(self, form):
self.form = form
def __getitem__(self, key):
return self.form.getFields(use_dict=True)[key]
def __contains__(self, key):
return key in self.form.getFields(use_dict=True)
def has_key(self, key):
return key in self.form.getFields(use_dict=True)
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',))
field_types = set(('boolean', 'fixed', 'hidden', 'jid-multi', 'jid-single', 'list-multi',
'list-single', 'text-multi', 'text-private', 'text-single'))
multi_value_types = set(('hidden', 'jid-multi', 'list-multi', 'text-multi'))
multi_line_types = set(('hidden', 'text-multi'))
option_types = set(('list-multi', 'list-single'))
true_values = set((True, '1', 'true'))
def addOption(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 delOptions(self):
optsXML = self.xml.findall('{%s}option' % self.namespace)
for optXML in optsXML:
self.xml.remove(optXML)
def delRequired(self):
reqXML = self.xml.find('{%s}required' % self.namespace)
if reqXML is not None:
self.xml.remove(reqXML)
def delValue(self):
valsXML = self.xml.findall('{%s}value' % self.namespace)
for valXML in valsXML:
self.xml.remove(valXML)
def getAnswer(self):
return self.getValue()
def getOptions(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 getRequired(self):
reqXML = self.xml.find('{%s}required' % self.namespace)
return reqXML is not None
def getValue(self):
valsXML = self.xml.findall('{%s}value' % self.namespace)
if len(valsXML) == 0:
return None
elif self['type'] == 'boolean':
return valsXML[0].text in self.true_values
elif self['type'] in self.multi_value_types:
values = []
for valXML in valsXML:
if valXML.text is None:
valXML.text = ''
values.append(valXML.text)
if self['type'] == 'text-multi':
values = "\n".join(values)
return values
else:
return valsXML[0].text
def setAnswer(self, answer):
self.setValue(answer)
def setFalse(self):
self.setValue(False)
def setOptions(self, options):
for value in options:
if isinstance(value, dict):
self.addOption(**value)
else:
self.addOption(value=value)
def setRequired(self, required):
exists = self.getRequired()
if not exists and required:
self.xml.append(ET.Element('{%s}required' % self.namespace))
elif exists and not required:
self.delRequired()
def setTrue(self):
self.setValue(True)
def setValue(self, value):
self.delValue()
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 self['type'] in self.multi_line_types and isinstance(value, str):
value = value.split('\n')
if not isinstance(value, list):
value = [value]
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',))
class xep_0004(base.base_plugin):
"""
XEP-0004: Data Forms
"""
def plugin_init(self):
self.xep = '0004'
self.description = 'Data Forms'
self.xmpp.registerHandler(
Callback('Data Form',
MatchXPath('{%s}message/{%s}x' % (self.xmpp.default_ns,
Form.namespace)),
self.handle_form))
registerStanzaPlugin(FormField, FieldOption)
registerStanzaPlugin(Form, FormField)
registerStanzaPlugin(Message, Form)
def makeForm(self, ftype='form', title='', instructions=''):
f = Form()
f['type'] = ftype
f['title'] = title
f['instructions'] = instructions
return f
def post_init(self):
base.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 buildForm(self, xml):
return Form(xml=xml)

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,178 @@
"""
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):
valsXML = self.xml.findall('{%s}value' % self.namespace)
if len(valsXML) == 0:
return None
elif self._type == 'boolean':
return valsXML[0].text in self.true_values
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':
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

View File

@@ -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:
fieldXML = ET.Element('{%s}field' % FormField.namespace)
itemXML.append(fieldXML)
field = FormField(xml=fieldXML)
field['var'] = var
field['value'] = values.get(var, None)
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 = {}
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 = {}
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 = {}
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

View File

@@ -112,7 +112,4 @@ class xep_0012(base.base_plugin):
iq.attrib['from'] = self.xmpp.boundjid.full
id = iq.get('id')
result = iq.send()
if result and result is not None and result.get('type', 'error') != 'error':
return result['last_activity']['seconds']
else:
return False
return result['last_activity']['seconds']

View File

@@ -188,8 +188,12 @@ class xep_0045(base.base_plugin):
iq['from'] = ifrom
query = ET.Element('{http://jabber.org/protocol/muc#owner}query')
iq.append(query)
result = iq.send()
if result['type'] == 'error':
# 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
@@ -209,8 +213,12 @@ class xep_0045(base.base_plugin):
form = form.getXML('submit')
query.append(form)
iq.append(query)
result = iq.send()
if result['type'] == 'error':
# For now, swallow errors to preserve existing API
try:
result = iq.send()
except IqError:
return False
except IqTimeout:
return False
return True
@@ -254,8 +262,12 @@ class xep_0045(base.base_plugin):
destroy.append(xreason)
query.append(destroy)
iq.append(query)
r = iq.send()
if r is False or r['type'] == 'error':
# For now, swallow errors to preserve existing API
try:
r = iq.send()
except IqError:
return False
except IqTimeout:
return False
return True
@@ -271,9 +283,13 @@ class xep_0045(base.base_plugin):
query.append(item)
iq = self.xmpp.makeIqSet(query)
iq['to'] = room
result = iq.send()
if result is False or result['type'] != 'result':
raise ValueError
# 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=''):
@@ -303,8 +319,12 @@ class xep_0045(base.base_plugin):
iq = self.xmpp.makeIqGet('http://jabber.org/protocol/muc#owner')
iq['to'] = room
iq['from'] = ifrom
result = iq.send()
if result is None or result['type'] != 'result':
# 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:

View File

@@ -10,6 +10,7 @@ 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
@@ -91,16 +92,6 @@ class xep_0050(base_plugin):
StanzaPath('iq@type=set/command'),
self._handle_command))
self.xmpp.register_handler(
Callback("Ad-Hoc Result",
StanzaPath('iq@type=result/command'),
self._handle_command_result))
self.xmpp.register_handler(
Callback("Ad-Hoc Error",
StanzaPath('iq@type=error/command'),
self._handle_command_result))
register_stanza_plugin(Iq, stanza.Command)
self.xmpp.add_event_handler('command_execute',
@@ -408,7 +399,7 @@ class xep_0050(base_plugin):
**kwargs)
def send_command(self, jid, node, ifrom=None, action='execute',
payload=None, sessionid=None, **kwargs):
payload=None, sessionid=None, flow=False, **kwargs):
"""
Create and send a command stanza, without using the provided
workflow management APIs.
@@ -422,6 +413,10 @@ class xep_0050(base_plugin):
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.
@@ -431,7 +426,7 @@ class xep_0050(base_plugin):
sleekxmpp.xmlstream.RESPONSE_TIMEOUT
callback -- Optional reference to a stream handler
function. Will be executed when a reply
stanza is received.
stanza is received if flow=False.
"""
iq = self.xmpp.Iq()
iq['type'] = 'set'
@@ -447,13 +442,24 @@ class xep_0050(base_plugin):
payload = [payload]
for item in payload:
iq['command'].append(item)
return iq.send(**kwargs)
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):
def start_command(self, jid, node, session, ifrom=None, block=False):
"""
Initiate executing a command provided by a remote agent.
The workflow provided is always non-blocking.
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.
@@ -465,11 +471,14 @@ class xep_0050(base_plugin):
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
@@ -481,7 +490,14 @@ class xep_0050(base_plugin):
sessionid = 'client:pending_' + iq['id']
session['id'] = sessionid
self.sessions[sessionid] = session
iq.send(block=False)
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):
"""
@@ -499,7 +515,9 @@ class xep_0050(base_plugin):
ifrom=session.get('from', None),
action='next',
payload=session.get('payload', None),
sessionid=session['id'])
sessionid=session['id'],
flow=True,
block=session['block'])
def cancel_command(self, session):
"""
@@ -517,7 +535,9 @@ class xep_0050(base_plugin):
ifrom=session.get('from', None),
action='cancel',
payload=session.get('payload', None),
sessionid=session['id'])
sessionid=session['id'],
flow=True,
block=session['block'])
def complete_command(self, session):
"""
@@ -535,7 +555,9 @@ class xep_0050(base_plugin):
ifrom=session.get('from', None),
action='complete',
payload=session.get('payload', None),
sessionid=session['id'])
sessionid=session['id'],
flow=True,
block=session['block'])
def terminate_command(self, session):
"""

View File

@@ -11,303 +11,118 @@ log = logging.getLogger(__name__)
class xep_0060(base.base_plugin):
"""
XEP-0060 Publish Subscribe
"""
"""
XEP-0060 Publish Subscribe
"""
def plugin_init(self):
self.xep = '0060'
self.description = '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 create_node(self, jid, node, config=None, ntype=None):
iq = IQ(sto=jid, stype='set', sfrom=self.xmpp.jid)
iq['pubsub']['create']['node'] = node
if ntype is None:
ntype = 'leaf'
if config is not None:
if 'FORM_TYPE' in submitform.field:
config.field['FORM_TYPE'].setValue('http://jabber.org/protocol/pubsub#node_config')
else:
config.addField('FORM_TYPE', 'hidden', value='http://jabber.org/protocol/pubsub#node_config')
if 'pubsub#node_type' in submitform.field:
config.field['pubsub#node_type'].setValue(ntype)
else:
config.addField('pubsub#node_type', value=ntype)
iq['pubsub']['configure']['form'] = config
return iq.send()
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 subscribe(self, jid, node, bare=True, subscribee=None):
iq = IQ(sto=jid, sfrom=self.xmpp.jid, stype='set')
iq['pubsub']['subscribe']['node'] = node
if subscribee is None:
if bare:
iq['pubsub']['subscribe']['jid'] = self.xmpp.jid.bare
else:
iq['pubsub']['subscribe']['jid'] = self.xmpp.jid.full
else:
iq['pubsub']['subscribe']['jid'] = subscribee
return iq.send()
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 unsubscribe(self, jid, node, subid=None, bare=True, subscribee=None):
iq = IQ(sto=jid, sfrom=self.xmpp.jid, stype='set')
iq['pubsub']['unsubscribe']['node'] = node
if subscribee is None:
if bare:
iq['pubsub']['unsubscribe']['jid'] = self.xmpp.jid.bare
else:
iq['pubsub']['unsubscribe']['jid'] = self.xmpp.jid.full
else:
iq['pubsub']['unsubscribe']['jid'] = subscribee
if subid is not None:
iq['pubsub']['unsubscribe']['subid'] = subid
return iq.send()
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 get_node_config(self, jid, node=None): # if no node, then grab default
iq = IQ(sto=jid, sfrom=self.xmpp.jid, stype='get')
if node is None:
iq['pubsub_owner']['default']
else:
iq['pubsub_owner']['configure']['node'] = node
return iq.send()
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 get_node_subscriptions(self, jid, node):
iq = IQ(sto=jid, sfrom=self.xmpp.jid, stype='get')
iq['pubsub_owner']['subscriptions']['node'] = node
return iq.send()
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 get_node_affiliations(self, jid, node):
iq = IQ(sto=jid, sfrom=self.xmpp.jid, stype='get')
iq['pubsub_owner']['affiliations']['node'] = node
return iq.send()
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 delete_node(self, jid, node):
iq = IQ(sto=jid, sfrom=self.xmpp.jid, stype='get')
iq['pubsub_owner']['delete']['node'] = node
return iq.send()
def set_node_config(self, jid, node, config):
iq = IQ(sto=jid, sfrom=self.xmpp.jid, stype='set')
iq['pubsub_owner']['configure']['node'] = node
iq['pubsub_owner']['configure']['config'] = config
return iq.send()
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 publish(self, jid, node, items=[]):
iq = IQ(sto=jid, sfrom=self.xmpp.jid, stype='set')
iq['pubsub']['publish']['node'] = node
for id, payload in items:
item = stanza.pubsub.Item()
if id is not None:
item['id'] = id
item['payload'] = payload
iq['pubsub']['publish'].append(item)
return iq.send()
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 retract(self, jid, node, item):
iq = IQ(sto=jid, sfrom=self.xmpp.jid, stype='set')
iq['pubsub']['retract']['node'] = node
item = stanza.pubsub.Item()
item['id'] = item
iq['pubsub']['retract'].append(item)
return iq.send()
def addItem(self, jid, node, items=[]):
return self.setItem(jid, node, items)
def get_nodes(self, jid):
return self.xmpp.plugin['xep_0030'].get_items(jid)
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, '')
def getItems(self, jid, node):
return self.xmpp.plugin['xep_0030'].get_items(jid, node)
def modify_affiliation(self, jid, node, affiliation, user_jid=None):
iq = IQ(sto=jid, sfrom=self.xmpp.jid, stype='set')
iq['pubsub_owner']['affiliations']
aff = stanza.pubsub.Affiliation()
aff['node'] = node
if user_jid is not None:
aff['jid'] = user_jid
aff['affiliation'] = affiliation
iq['pubsub_owner']['affiliations'].append(aff)
return iq.send()

View File

@@ -9,236 +9,236 @@ from sleekxmpp.plugins.xep_0060.stanza.base import OptionalSetting
class Pubsub(ElementBase):
namespace = 'http://jabber.org/protocol/pubsub'
name = 'pubsub'
plugin_attrib = 'pubsub'
interfaces = set(tuple())
plugin_attrib_map = {}
plugin_tag_map = {}
namespace = 'http://jabber.org/protocol/pubsub'
name = 'pubsub'
plugin_attrib = 'pubsub'
interfaces = set(tuple())
plugin_attrib_map = {}
plugin_tag_map = {}
registerStanzaPlugin(Iq, Pubsub)
class Affiliation(ElementBase):
namespace = 'http://jabber.org/protocol/pubsub'
name = 'affiliation'
plugin_attrib = name
interfaces = set(('node', 'affiliation'))
plugin_attrib_map = {}
plugin_tag_map = {}
namespace = 'http://jabber.org/protocol/pubsub'
name = 'affiliation'
plugin_attrib = name
interfaces = set(('node', 'affiliation', 'jid'))
plugin_attrib_map = {}
plugin_tag_map = {}
def setJid(self, value):
self._setAttr('jid', str(value))
def getJid(self):
return JID(self._getAttr('jid'))
class Affiliations(ElementBase):
namespace = 'http://jabber.org/protocol/pubsub'
name = 'affiliations'
plugin_attrib = 'affiliations'
interfaces = set(tuple())
plugin_attrib_map = {}
plugin_tag_map = {}
subitem = (Affiliation,)
def append(self, affiliation):
if not isinstance(affiliation, Affiliation):
raise TypeError
self.xml.append(affiliation.xml)
return self.iterables.append(affiliation)
namespace = 'http://jabber.org/protocol/pubsub'
name = 'affiliations'
plugin_attrib = 'affiliations'
interfaces = set(tuple())
plugin_attrib_map = {}
plugin_tag_map = {}
subitem = (Affiliation,)
registerStanzaPlugin(Pubsub, Affiliations)
class Subscription(ElementBase):
namespace = 'http://jabber.org/protocol/pubsub'
name = 'subscription'
plugin_attrib = name
interfaces = set(('jid', 'node', 'subscription', 'subid'))
plugin_attrib_map = {}
plugin_tag_map = {}
namespace = 'http://jabber.org/protocol/pubsub'
name = 'subscription'
plugin_attrib = name
interfaces = set(('jid', 'node', 'subscription', 'subid'))
plugin_attrib_map = {}
plugin_tag_map = {}
def setjid(self, value):
self._setattr('jid', str(value))
def setjid(self, value):
self._setattr('jid', str(value))
def getjid(self):
return jid(self._getattr('jid'))
def getjid(self):
return jid(self._getattr('jid'))
registerStanzaPlugin(Pubsub, Subscription)
class Subscriptions(ElementBase):
namespace = 'http://jabber.org/protocol/pubsub'
name = 'subscriptions'
plugin_attrib = 'subscriptions'
interfaces = set(tuple())
plugin_attrib_map = {}
plugin_tag_map = {}
subitem = (Subscription,)
namespace = 'http://jabber.org/protocol/pubsub'
name = 'subscriptions'
plugin_attrib = 'subscriptions'
interfaces = set(tuple())
plugin_attrib_map = {}
plugin_tag_map = {}
subitem = (Subscription,)
registerStanzaPlugin(Pubsub, Subscriptions)
class SubscribeOptions(ElementBase, OptionalSetting):
namespace = 'http://jabber.org/protocol/pubsub'
name = 'subscribe-options'
plugin_attrib = 'suboptions'
plugin_attrib_map = {}
plugin_tag_map = {}
interfaces = set(('required',))
namespace = 'http://jabber.org/protocol/pubsub'
name = 'subscribe-options'
plugin_attrib = 'suboptions'
plugin_attrib_map = {}
plugin_tag_map = {}
interfaces = set(('required',))
registerStanzaPlugin(Subscription, SubscribeOptions)
class Item(ElementBase):
namespace = 'http://jabber.org/protocol/pubsub'
name = 'item'
plugin_attrib = name
interfaces = set(('id', 'payload'))
plugin_attrib_map = {}
plugin_tag_map = {}
namespace = 'http://jabber.org/protocol/pubsub'
name = 'item'
plugin_attrib = name
interfaces = set(('id', 'payload'))
plugin_attrib_map = {}
plugin_tag_map = {}
def setPayload(self, value):
self.xml.append(value)
def setPayload(self, value):
self.xml.append(value)
def getPayload(self):
childs = self.xml.getchildren()
if len(childs) > 0:
return childs[0]
def getPayload(self):
childs = self.xml.getchildren()
if len(childs) > 0:
return childs[0]
def delPayload(self):
for child in self.xml.getchildren():
self.xml.remove(child)
def delPayload(self):
for child in self.xml.getchildren():
self.xml.remove(child)
class Items(ElementBase):
namespace = 'http://jabber.org/protocol/pubsub'
name = 'items'
plugin_attrib = 'items'
interfaces = set(('node',))
plugin_attrib_map = {}
plugin_tag_map = {}
subitem = (Item,)
namespace = 'http://jabber.org/protocol/pubsub'
name = 'items'
plugin_attrib = 'items'
interfaces = set(('node',))
plugin_attrib_map = {}
plugin_tag_map = {}
subitem = (Item,)
registerStanzaPlugin(Pubsub, Items)
class Create(ElementBase):
namespace = 'http://jabber.org/protocol/pubsub'
name = 'create'
plugin_attrib = name
interfaces = set(('node',))
plugin_attrib_map = {}
plugin_tag_map = {}
namespace = 'http://jabber.org/protocol/pubsub'
name = 'create'
plugin_attrib = name
interfaces = set(('node',))
plugin_attrib_map = {}
plugin_tag_map = {}
registerStanzaPlugin(Pubsub, Create)
#class Default(ElementBase):
# namespace = 'http://jabber.org/protocol/pubsub'
# name = 'default'
# plugin_attrib = name
# interfaces = set(('node', 'type'))
# plugin_attrib_map = {}
# plugin_tag_map = {}
# namespace = 'http://jabber.org/protocol/pubsub'
# name = 'default'
# plugin_attrib = name
# interfaces = set(('node', 'type'))
# plugin_attrib_map = {}
# plugin_tag_map = {}
#
# def getType(self):
# t = self._getAttr('type')
# if not t: t == 'leaf'
# return t
# def getType(self):
# t = self._getAttr('type')
# if not t: t == 'leaf'
# return t
#
#registerStanzaPlugin(Pubsub, Default)
class Publish(Items):
namespace = 'http://jabber.org/protocol/pubsub'
name = 'publish'
plugin_attrib = name
interfaces = set(('node',))
plugin_attrib_map = {}
plugin_tag_map = {}
subitem = (Item,)
namespace = 'http://jabber.org/protocol/pubsub'
name = 'publish'
plugin_attrib = name
interfaces = set(('node',))
plugin_attrib_map = {}
plugin_tag_map = {}
subitem = (Item,)
registerStanzaPlugin(Pubsub, Publish)
class Retract(Items):
namespace = 'http://jabber.org/protocol/pubsub'
name = 'retract'
plugin_attrib = name
interfaces = set(('node', 'notify'))
plugin_attrib_map = {}
plugin_tag_map = {}
namespace = 'http://jabber.org/protocol/pubsub'
name = 'retract'
plugin_attrib = name
interfaces = set(('node', 'notify'))
plugin_attrib_map = {}
plugin_tag_map = {}
registerStanzaPlugin(Pubsub, Retract)
class Unsubscribe(ElementBase):
namespace = 'http://jabber.org/protocol/pubsub'
name = 'unsubscribe'
plugin_attrib = name
interfaces = set(('node', 'jid'))
plugin_attrib_map = {}
plugin_tag_map = {}
namespace = 'http://jabber.org/protocol/pubsub'
name = 'unsubscribe'
plugin_attrib = name
interfaces = set(('node', 'jid', 'subid'))
plugin_attrib_map = {}
plugin_tag_map = {}
def setJid(self, value):
self._setAttr('jid', str(value))
def setJid(self, value):
self._setAttr('jid', str(value))
def getJid(self):
return JID(self._getAttr('jid'))
def getJid(self):
return JID(self._getAttr('jid'))
registerStanzaPlugin(Pubsub, Unsubscribe)
class Subscribe(ElementBase):
namespace = 'http://jabber.org/protocol/pubsub'
name = 'subscribe'
plugin_attrib = name
interfaces = set(('node', 'jid'))
plugin_attrib_map = {}
plugin_tag_map = {}
namespace = 'http://jabber.org/protocol/pubsub'
name = 'subscribe'
plugin_attrib = name
interfaces = set(('node', 'jid'))
plugin_attrib_map = {}
plugin_tag_map = {}
def setJid(self, value):
self._setAttr('jid', str(value))
def setJid(self, value):
self._setAttr('jid', str(value))
def getJid(self):
return JID(self._getAttr('jid'))
def getJid(self):
return JID(self._getAttr('jid'))
registerStanzaPlugin(Pubsub, Subscribe)
class Configure(ElementBase):
namespace = 'http://jabber.org/protocol/pubsub'
name = 'configure'
plugin_attrib = name
interfaces = set(('node', 'type'))
plugin_attrib_map = {}
plugin_tag_map = {}
namespace = 'http://jabber.org/protocol/pubsub'
name = 'configure'
plugin_attrib = name
interfaces = set(('node', 'type'))
plugin_attrib_map = {}
plugin_tag_map = {}
def getType(self):
t = self._getAttr('type')
if not t: t == 'leaf'
return t
def getType(self):
t = self._getAttr('type')
if not t: t == 'leaf'
return t
registerStanzaPlugin(Pubsub, Configure)
registerStanzaPlugin(Configure, xep_0004.Form)
class Options(ElementBase):
namespace = 'http://jabber.org/protocol/pubsub'
name = 'options'
plugin_attrib = 'options'
interfaces = set(('jid', 'node', 'options'))
plugin_attrib_map = {}
plugin_tag_map = {}
namespace = 'http://jabber.org/protocol/pubsub'
name = 'options'
plugin_attrib = 'options'
interfaces = set(('jid', 'node', 'options'))
plugin_attrib_map = {}
plugin_tag_map = {}
def __init__(self, *args, **kwargs):
ElementBase.__init__(self, *args, **kwargs)
def __init__(self, *args, **kwargs):
ElementBase.__init__(self, *args, **kwargs)
def getOptions(self):
config = self.xml.find('{jabber:x:data}x')
form = xep_0004.Form()
if config is not None:
form.fromXML(config)
return form
def getOptions(self):
config = self.xml.find('{jabber:x:data}x')
form = xep_0004.Form()
if config is not None:
form.fromXML(config)
return form
def setOptions(self, value):
self.xml.append(value.getXML())
return self
def setOptions(self, value):
self.xml.append(value.getXML())
return self
def delOptions(self):
config = self.xml.find('{jabber:x:data}x')
self.xml.remove(config)
def delOptions(self):
config = self.xml.find('{jabber:x:data}x')
self.xml.remove(config)
def setJid(self, value):
self._setAttr('jid', str(value))
def setJid(self, value):
self._setAttr('jid', str(value))
def getJid(self):
return JID(self._getAttr('jid'))
def getJid(self):
return JID(self._getAttr('jid'))
registerStanzaPlugin(Pubsub, Options)
registerStanzaPlugin(Subscribe, Options)

View File

@@ -9,144 +9,147 @@ from sleekxmpp.plugins.xep_0060.stanza.base import OptionalSetting
from sleekxmpp.plugins.xep_0060.stanza.pubsub import Affiliations, Affiliation, Configure, Subscriptions
class PubsubOwner(ElementBase):
namespace = 'http://jabber.org/protocol/pubsub#owner'
name = 'pubsub'
plugin_attrib = 'pubsub_owner'
interfaces = set(tuple())
plugin_attrib_map = {}
plugin_tag_map = {}
namespace = 'http://jabber.org/protocol/pubsub#owner'
name = 'pubsub'
plugin_attrib = 'pubsub_owner'
interfaces = set(tuple())
plugin_attrib_map = {}
plugin_tag_map = {}
registerStanzaPlugin(Iq, PubsubOwner)
class DefaultConfig(ElementBase):
namespace = 'http://jabber.org/protocol/pubsub#owner'
name = 'default'
plugin_attrib = 'default'
interfaces = set(('node', 'type', 'config'))
plugin_attrib_map = {}
plugin_tag_map = {}
namespace = 'http://jabber.org/protocol/pubsub#owner'
name = 'default'
plugin_attrib = 'default'
interfaces = set(('node', 'type', 'config'))
plugin_attrib_map = {}
plugin_tag_map = {}
def __init__(self, *args, **kwargs):
ElementBase.__init__(self, *args, **kwargs)
def __init__(self, *args, **kwargs):
ElementBase.__init__(self, *args, **kwargs)
def getType(self):
t = self._getAttr('type')
if not t: t = 'leaf'
return t
def getType(self):
t = self._getAttr('type')
if not t: t = 'leaf'
return t
def getConfig(self):
return self['form']
def getConfig(self):
return self['form']
def setConfig(self, value):
self['form'].setStanzaValues(value.getStanzaValues())
return self
def setConfig(self, value):
self['form'].setStanzaValues(value.getStanzaValues())
return self
registerStanzaPlugin(PubsubOwner, DefaultConfig)
registerStanzaPlugin(DefaultConfig, xep_0004.Form)
class OwnerAffiliations(Affiliations):
namespace = 'http://jabber.org/protocol/pubsub#owner'
interfaces = set(('node'))
plugin_attrib_map = {}
plugin_tag_map = {}
namespace = 'http://jabber.org/protocol/pubsub#owner'
interfaces = set(('node'))
plugin_attrib_map = {}
plugin_tag_map = {}
def append(self, affiliation):
if not isinstance(affiliation, OwnerAffiliation):
raise TypeError
self.xml.append(affiliation.xml)
return self.affiliations.append(affiliation)
def append(self, affiliation):
if not isinstance(affiliation, OwnerAffiliation):
raise TypeError
self.xml.append(affiliation.xml)
return self.affiliations.append(affiliation)
registerStanzaPlugin(PubsubOwner, OwnerAffiliations)
class OwnerAffiliation(Affiliation):
namespace = 'http://jabber.org/protocol/pubsub#owner'
interfaces = set(('affiliation', 'jid'))
plugin_attrib_map = {}
plugin_tag_map = {}
namespace = 'http://jabber.org/protocol/pubsub#owner'
interfaces = set(('affiliation', 'jid'))
plugin_attrib_map = {}
plugin_tag_map = {}
class OwnerConfigure(Configure):
namespace = 'http://jabber.org/protocol/pubsub#owner'
interfaces = set(('node', 'config'))
plugin_attrib_map = {}
plugin_tag_map = {}
name = 'configure'
plugin_attrib = 'configure'
namespace = 'http://jabber.org/protocol/pubsub#owner'
interfaces = set(('node', 'config'))
plugin_attrib_map = {}
plugin_tag_map = {}
def getConfig(self):
return self['form']
def setConfig(self, value):
self['form'].setStanzaValues(value.getStanzaValues())
return self
registerStanzaPlugin(PubsubOwner, OwnerConfigure)
class OwnerDefault(OwnerConfigure):
namespace = 'http://jabber.org/protocol/pubsub#owner'
interfaces = set(('node', 'config'))
plugin_attrib_map = {}
plugin_tag_map = {}
namespace = 'http://jabber.org/protocol/pubsub#owner'
interfaces = set(('node', 'config'))
plugin_attrib_map = {}
plugin_tag_map = {}
def getConfig(self):
return self['form']
def setConfig(self, value):
self['form'].setStanzaValues(value.getStanzaValues())
return self
registerStanzaPlugin(PubsubOwner, OwnerDefault)
registerStanzaPlugin(OwnerDefault, xep_0004.Form)
class OwnerDelete(ElementBase, OptionalSetting):
namespace = 'http://jabber.org/protocol/pubsub#owner'
name = 'delete'
plugin_attrib = 'delete'
plugin_attrib_map = {}
plugin_tag_map = {}
interfaces = set(('node',))
namespace = 'http://jabber.org/protocol/pubsub#owner'
name = 'delete'
plugin_attrib = 'delete'
plugin_attrib_map = {}
plugin_tag_map = {}
interfaces = set(('node',))
registerStanzaPlugin(PubsubOwner, OwnerDelete)
class OwnerPurge(ElementBase, OptionalSetting):
namespace = 'http://jabber.org/protocol/pubsub#owner'
name = 'purge'
plugin_attrib = name
plugin_attrib_map = {}
plugin_tag_map = {}
namespace = 'http://jabber.org/protocol/pubsub#owner'
name = 'purge'
plugin_attrib = name
plugin_attrib_map = {}
plugin_tag_map = {}
registerStanzaPlugin(PubsubOwner, OwnerPurge)
class OwnerRedirect(ElementBase):
namespace = 'http://jabber.org/protocol/pubsub#owner'
name = 'redirect'
plugin_attrib = name
interfaces = set(('node', 'jid'))
plugin_attrib_map = {}
plugin_tag_map = {}
namespace = 'http://jabber.org/protocol/pubsub#owner'
name = 'redirect'
plugin_attrib = name
interfaces = set(('node', 'jid'))
plugin_attrib_map = {}
plugin_tag_map = {}
def setJid(self, value):
self._setAttr('jid', str(value))
def setJid(self, value):
self._setAttr('jid', str(value))
def getJid(self):
return JID(self._getAttr('jid'))
def getJid(self):
return JID(self._getAttr('jid'))
registerStanzaPlugin(OwnerDelete, OwnerRedirect)
class OwnerSubscriptions(Subscriptions):
namespace = 'http://jabber.org/protocol/pubsub#owner'
interfaces = set(('node',))
plugin_attrib_map = {}
plugin_tag_map = {}
namespace = 'http://jabber.org/protocol/pubsub#owner'
interfaces = set(('node',))
plugin_attrib_map = {}
plugin_tag_map = {}
def append(self, subscription):
if not isinstance(subscription, OwnerSubscription):
raise TypeError
self.xml.append(subscription.xml)
return self.subscriptions.append(subscription)
def append(self, subscription):
if not isinstance(subscription, OwnerSubscription):
raise TypeError
self.xml.append(subscription.xml)
return self.subscriptions.append(subscription)
registerStanzaPlugin(PubsubOwner, OwnerSubscriptions)
class OwnerSubscription(ElementBase):
namespace = 'http://jabber.org/protocol/pubsub#owner'
name = 'subscription'
plugin_attrib = name
interfaces = set(('jid', 'subscription'))
plugin_attrib_map = {}
plugin_tag_map = {}
namespace = 'http://jabber.org/protocol/pubsub#owner'
name = 'subscription'
plugin_attrib = name
interfaces = set(('jid', 'subscription'))
plugin_attrib_map = {}
plugin_tag_map = {}
def setJid(self, value):
self._setAttr('jid', str(value))
def setJid(self, value):
self._setAttr('jid', str(value))
def getJid(self):
return JID(self._getAttr('from'))
def getJid(self):
return JID(self._getAttr('from'))

View File

@@ -1,72 +0,0 @@
"""
SleekXMPP: The Sleek XMPP Library
Copyright (C) 2010 Nathanael C. Fritz
This file is part of SleekXMPP.
See the file LICENSE for copying permission.
"""
from __future__ import with_statement
from xml.etree import cElementTree as ET
import logging
import hashlib
from . import base
log = logging.getLogger(__name__)
class xep_0078(base.base_plugin):
"""
XEP-0078 NON-SASL Authentication
"""
def plugin_init(self):
self.description = "Non-SASL Authentication (broken)"
self.xep = "0078"
self.xmpp.add_event_handler("session_start", self.check_stream)
#disabling until I fix conflict with PLAIN
#self.xmpp.registerFeature("<auth xmlns='http://jabber.org/features/iq-auth'/>", self.auth)
self.streamid = ''
def check_stream(self, xml):
self.streamid = xml.attrib['id']
if xml.get('version', '0') != '1.0':
self.auth()
def auth(self, xml=None):
log.debug("Starting jabber:iq:auth Authentication")
auth_request = self.xmpp.makeIqGet()
auth_request_query = ET.Element('{jabber:iq:auth}query')
auth_request.attrib['to'] = self.xmpp.boundjid.host
username = ET.Element('username')
username.text = self.xmpp.username
auth_request_query.append(username)
auth_request.append(auth_request_query)
result = auth_request.send()
rquery = result.find('{jabber:iq:auth}query')
attempt = self.xmpp.makeIqSet()
query = ET.Element('{jabber:iq:auth}query')
resource = ET.Element('resource')
resource.text = self.xmpp.resource
query.append(username)
query.append(resource)
if rquery.find('{jabber:iq:auth}digest') is None:
log.warning("Authenticating via jabber:iq:auth Plain.")
password = ET.Element('password')
password.text = self.xmpp.password
query.append(password)
else:
log.debug("Authenticating via jabber:iq:auth Digest")
digest = ET.Element('digest')
digest.text = hashlib.sha1(b"%s%s" % (self.streamid, self.xmpp.password)).hexdigest()
query.append(digest)
attempt.append(query)
result = attempt.send()
if result.attrib['type'] == 'result':
with self.xmpp.lock:
self.xmpp.authenticated = True
self.xmpp.sessionstarted = True
self.xmpp.event("session_start")
else:
log.info("Authentication failed")
self.xmpp.disconnect()
self.xmpp.event("failed_auth")

View File

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

View File

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

View File

@@ -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 = {}

View File

@@ -11,6 +11,7 @@ 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
@@ -89,8 +90,13 @@ class xep_0199(base_plugin):
def scheduled_ping():
"""Send ping request to the server."""
log.debug("Pinging...")
resp = self.send_ping(self.xmpp.boundjid.host, self.timeout)
if resp is None or resp is False:
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()
@@ -108,7 +114,7 @@ class xep_0199(base_plugin):
iq -- The ping request.
"""
log.debug("Pinged by %s" % iq['from'])
iq.reply().enable('ping').send()
iq.reply().send()
def send_ping(self, jid, timeout=None, errorfalse=False,
ifrom=None, block=True, callback=None):
@@ -142,9 +148,14 @@ class xep_0199(base_plugin):
iq.enable('ping')
start_time = time.clock()
resp = iq.send(block=block,
timeout=timeout,
callback=callback)
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
@@ -152,9 +163,6 @@ class xep_0199(base_plugin):
if not block:
return None
if not resp or resp['type'] == 'error':
return False
log.debug("Pong: %s %f" % (jid, delay))
return delay

View File

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

478
sleekxmpp/roster/item.py Normal file
View File

@@ -0,0 +1,478 @@
"""
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.
"""
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] = {}
self.resources[resource].update(data)
def handle_unavailable(self, presence):
resource = presence['from'].resource
if not self.resources:
return
if resource in self.resources:
del self.resources[resource]
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 = {}

140
sleekxmpp/roster/multi.py Normal file
View File

@@ -0,0 +1,140 @@
"""
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 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)

287
sleekxmpp/roster/single.py Normal file
View File

@@ -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.
"""
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
state = {'name': name,
'groups': groups or [],
'from': afrom,
'to': ato,
'pending_in': pending_in,
'pending_out': pending_out,
'whitelisted': whitelisted,
'subscription': 'none'}
self._jids[jid] = RosterItem(self.xmpp, jid, self.jid,
state=state, db=self.db,
roster=self)
if save:
self._jids[jid].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'] = group
self[jid].save()
if not self.xmpp.is_component:
iq = self.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

View File

@@ -11,6 +11,7 @@ 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):
@@ -197,7 +198,12 @@ class Iq(RootStanza):
waitfor = Waiter('IqWait_%s' % self['id'], MatcherId(self['id']))
self.stream.register_handler(waitfor)
StanzaBase.send(self, now=now)
return waitfor.wait(timeout)
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)

View File

@@ -10,7 +10,7 @@ import logging
import traceback
import sys
from sleekxmpp.exceptions import XMPPError
from sleekxmpp.exceptions import XMPPError, IqError, IqTimeout
from sleekxmpp.stanza import Error
from sleekxmpp.xmlstream import ET, StanzaBase, register_stanza_plugin
@@ -43,23 +43,41 @@ class RootStanza(StanzaBase):
Arguments:
e -- Exception object
"""
if isinstance(e, XMPPError):
self.reply(clear=e.clear)
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['error']['type'] = e.etype
self.send()
else:
self.reply()
# 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' %

View File

@@ -89,6 +89,8 @@ class Roster(ElementBase):
item = {}
item['name'] = itemxml.get('name', '')
item['subscription'] = itemxml.get('subscription', '')
item['ask'] = itemxml.get('ask', '')
item['approved'] = itemxml.get('approved', '')
item['groups'] = []
groupsxml = itemxml.findall('{jabber:iq:roster}group')
if groupsxml is not None:

View File

@@ -19,6 +19,8 @@ class StreamFeatures(StanzaBase):
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)

View File

@@ -16,6 +16,7 @@ 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
@@ -150,6 +151,32 @@ class SleekTest(unittest.TestCase):
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

View File

@@ -36,9 +36,16 @@ from sleekxmpp.xmlstream.matcher import MatchXMLMask
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 = 10
RESPONSE_TIMEOUT = 30
# The number of threads to use to handle XML stream events. This is not the
# same as the number of custom event handling threads. HANDLER_THREADS must
@@ -49,8 +56,7 @@ HANDLER_THREADS = 1
SSL_SUPPORT = True
# Maximum time to delay between connection attempts is one hour.
RECONNECT_MAX_DELAY = 3600
RECONNECT_MAX_DELAY = 600
log = logging.getLogger(__name__)
@@ -92,6 +98,7 @@ class XMLStream(object):
events to be processed.
filesocket -- A filesocket created from the main connection socket.
Required for ElementTree.iterparse.
default_port -- Default port to connect to.
namespace_map -- Optional mapping of namespaces to namespace prefixes.
scheduler -- A scheduler object for triggering events
after a given period of time.
@@ -121,6 +128,7 @@ class XMLStream(object):
reconnect_max_delay -- Maximum time to delay between connection
attempts. Defaults to RECONNECT_MAX_DELAY,
which is one hour.
dns_answers -- List of dns answers not yet used to connect.
Methods:
add_event_handler -- Add a handler for a custom event.
@@ -177,6 +185,8 @@ class XMLStream(object):
self.state = StateMachine(('disconnected', 'connected'))
self.state._set_state('disconnected')
self.default_port = int(port)
self.default_domain = ''
self.address = (host, int(port))
self.filesocket = None
self.set_socket(socket)
@@ -193,6 +203,7 @@ class XMLStream(object):
self.proxy_config = {}
self.default_ns = ''
self.stream_ns = ''
self.stream_header = "<stream>"
self.stream_footer = "</stream>"
@@ -219,6 +230,7 @@ class XMLStream(object):
self.auto_reconnect = True
self.is_client = False
self.dns_answers = []
def use_signals(self, signals=None):
"""
@@ -303,6 +315,10 @@ class XMLStream(object):
"""
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]
self.is_client = True
# Respect previous SSL and TLS usage directives.
@@ -322,6 +338,9 @@ class XMLStream(object):
def _connect(self):
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.socket.settimeout(None)
@@ -461,6 +480,7 @@ class XMLStream(object):
# 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()
@@ -638,6 +658,69 @@ class XMLStream(object):
idx += 1
return False
def get_dns_records(self, domain, port=None):
"""
Get the DNS records for a domain.
Arguments:
domain -- The domain in question.
port -- If the results don't include a port, use this one.
"""
if port is None:
port = self.default_port
if DNSPYTHON:
try:
answers = dns.resolver.query(domain, dns.rdatatype.A)
except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer):
log.warning("No A records for %s" % domain)
except dns.exception.Timeout:
log.warning("DNS resolution timed out " + \
"for A record of %s" % domain)
answers = [((answer.address, port), 0, 0) for answer in answers]
return answers
else:
log.warning("dnspython is not installed -- " + \
"relying on OS A record resolution")
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.
Arguments:
domain -- The domain in question.
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):
"""
@@ -708,6 +791,8 @@ class XMLStream(object):
log.exception(error_msg % str(handler[0]))
if hasattr(data, 'exception'):
data.exception(e)
else:
self.exception(e)
else:
self.event_queue.put(('event', handler, copy.copy(data)))
@@ -1049,6 +1134,8 @@ class XMLStream(object):
log.exception(error_msg % str(func))
if hasattr(orig, 'exception'):
orig.exception(e)
else:
self.exception(e)
def _event_runner(self):
"""
@@ -1084,8 +1171,9 @@ class XMLStream(object):
try:
log.debug('Scheduled event: %s' % args)
handler(*args[0])
except:
except Exception as e:
log.exception('Error processing scheduled task')
self.exception(e)
elif etype == 'event':
func, threaded, disposable = handler
orig = copy.copy(args[0])
@@ -1103,6 +1191,8 @@ class XMLStream(object):
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

View File

@@ -49,7 +49,8 @@ class TestPresenceStanzas(SleekTest):
self.failUnless(happened == [],
"changed_status event triggered for extra unavailable presence")
self.failUnless(c.roster == {},
roster = c.roster['crap@wherever']
self.failUnless(roster['bill@chadmore.com'].resources == {},
"Roster updated for superfulous unavailable presence")
def testNickPlugin(self):

View File

@@ -48,10 +48,14 @@ class TestRosterStanzas(SleekTest):
'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'])

Some files were not shown because too many files have changed in this diff Show More