Compare commits
293 Commits
sleek-1.0-
...
sleek-1.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
79f1aa0e1b | ||
|
|
fb5a6a7d71 | ||
|
|
7d1c5f4a2b | ||
|
|
1b0fd76b45 | ||
|
|
46e93bea09 | ||
|
|
cbc6a0296b | ||
|
|
cc63bef179 | ||
|
|
cbcfa156c4 | ||
|
|
4a12e1059a | ||
|
|
f9cd051209 | ||
|
|
03bc38f7e3 | ||
|
|
4e23a4e08e | ||
|
|
8cafa8578f | ||
|
|
b74ea47650 | ||
|
|
2dc230a68b | ||
|
|
522f0dac16 | ||
|
|
cd5ae944ec | ||
|
|
42a86fe0d4 | ||
|
|
e928b9c434 | ||
|
|
fb55d9e9d1 | ||
|
|
74e7e5a291 | ||
|
|
6c58b8cc4b | ||
|
|
2b3d11a7a5 | ||
|
|
9950208d06 | ||
|
|
a67e16d1b7 | ||
|
|
c98a22e065 | ||
|
|
d496417deb | ||
|
|
116bb6e1b9 | ||
|
|
9c6dde5d22 | ||
|
|
fc8a13df5a | ||
|
|
85e9042db6 | ||
|
|
62e6d6fb4c | ||
|
|
16c72e8efd | ||
|
|
efe1b9f5a9 | ||
|
|
65dbddb6b6 | ||
|
|
2a67a31120 | ||
|
|
a720c3348b | ||
|
|
79ac60b6e8 | ||
|
|
e01c2d222a | ||
|
|
8922e2050a | ||
|
|
a85891c611 | ||
|
|
2586fdffda | ||
|
|
c9dc9ec11e | ||
|
|
b9332142c9 | ||
|
|
b7b53362e1 | ||
|
|
68cf66a5fe | ||
|
|
4eb7eeb40f | ||
|
|
a1d64fa215 | ||
|
|
5f44c0e678 | ||
|
|
b87c4d786d | ||
|
|
329b0df3f6 | ||
|
|
6906c15e8e | ||
|
|
ff5421cefc | ||
|
|
4498e992a2 | ||
|
|
2d610dfdc8 | ||
|
|
2b0a05ee32 | ||
|
|
bc2d0ee9a8 | ||
|
|
862a2a1440 | ||
|
|
fba60ffff1 | ||
|
|
d1a945a305 | ||
|
|
685b9ab102 | ||
|
|
24f27c0fe3 | ||
|
|
3019c82d8a | ||
|
|
f9d0b55ca3 | ||
|
|
b54cc97e4c | ||
|
|
e3b9d5abbf | ||
|
|
2332970cf2 | ||
|
|
48af3d3322 | ||
|
|
429c94d6a9 | ||
|
|
deb52ad350 | ||
|
|
6f3cc77bb5 | ||
|
|
1baf139ca4 | ||
|
|
7945b3e738 | ||
|
|
d4c1ff5309 | ||
|
|
22868c3924 | ||
|
|
2de1be188c | ||
|
|
9faecec2db | ||
|
|
5d7111fe3b | ||
|
|
0c86f8288d | ||
|
|
5a6a65fd9f | ||
|
|
43c4d23896 | ||
|
|
9f9e8db814 | ||
|
|
b8efcc7cf0 | ||
|
|
2f29d18e53 | ||
|
|
888e286a09 | ||
|
|
1a93a187f0 | ||
|
|
a8d5da5091 | ||
|
|
e2720fac9e | ||
|
|
4374729f20 | ||
|
|
87999333cb | ||
|
|
335dc2927b | ||
|
|
ccbef6b696 | ||
|
|
3e384d3cfe | ||
|
|
e33949c397 | ||
|
|
eccac859ad | ||
|
|
7dd586f2fd | ||
|
|
3607c5b792 | ||
|
|
e37adace62 | ||
|
|
d10f591bf4 | ||
|
|
262da78ca7 | ||
|
|
0b83edf439 | ||
|
|
cf7fcf496e | ||
|
|
1765271f84 | ||
|
|
0ec79f8dc3 | ||
|
|
6f72c05ebf | ||
|
|
20cacc84ba | ||
|
|
24a14a0284 | ||
|
|
982c2d9b83 | ||
|
|
efa4a9b330 | ||
|
|
39ec1cff19 | ||
|
|
24c5f8d374 | ||
|
|
d6b0158ddb | ||
|
|
7e5e9542e9 | ||
|
|
d7fc2aaa9c | ||
|
|
8471a485d1 | ||
|
|
462b375c8f | ||
|
|
afbd506cfc | ||
|
|
ec01e45ed1 | ||
|
|
993829b23f | ||
|
|
002257b820 | ||
|
|
0af35c2224 | ||
|
|
76bc0a2ba6 | ||
|
|
d2dc4824ee | ||
|
|
3f9ca0366b | ||
|
|
b68785e19e | ||
|
|
a1bbb719e1 | ||
|
|
46f23f7348 | ||
|
|
09252baa71 | ||
|
|
3623a7a16a | ||
|
|
cc504ab07c | ||
|
|
2500a0649b | ||
|
|
5ec4e4a026 | ||
|
|
c3df4dd052 | ||
|
|
730c3fada0 | ||
|
|
628978fc8c | ||
|
|
7fb9d68714 | ||
|
|
e0a1c477d0 | ||
|
|
b70565720f | ||
|
|
33ac0c9dd6 | ||
|
|
4699bdff60 | ||
|
|
354641a3ce | ||
|
|
58a43e40c7 | ||
|
|
6b7fde10d3 | ||
|
|
13fdab0139 | ||
|
|
2ce617b2ce | ||
|
|
63e0496c30 | ||
|
|
850e3bb99b | ||
|
|
2d90deb96a | ||
|
|
3fb3f63e51 | ||
|
|
d12949ff1c | ||
|
|
e3e985220e | ||
|
|
802dd8393d | ||
|
|
fe6bc31c60 | ||
|
|
2162d6042e | ||
|
|
b8a4ffece9 | ||
|
|
d929e0deb2 | ||
|
|
4c08c9c524 | ||
|
|
63b8444abe | ||
|
|
82546d776d | ||
|
|
84f9505a8d | ||
|
|
ede59ab40e | ||
|
|
2a80824076 | ||
|
|
f92f96325a | ||
|
|
b98555c512 | ||
|
|
e02a42a008 | ||
|
|
3e51126e18 | ||
|
|
a714fa82b2 | ||
|
|
e86e6eae81 | ||
|
|
592c25f352 | ||
|
|
015f662249 | ||
|
|
8d998d71a3 | ||
|
|
f75b6bf955 | ||
|
|
fb78bf0996 | ||
|
|
cd7cd30b4c | ||
|
|
4ea22ff69b | ||
|
|
3853898ab3 | ||
|
|
7d8aa4157b | ||
|
|
3fc20e10f5 | ||
|
|
004eabf809 | ||
|
|
62230fc970 | ||
|
|
961668d420 | ||
|
|
01061a0355 | ||
|
|
9fdd85d9f1 | ||
|
|
331db30f8f | ||
|
|
017d7ec62b | ||
|
|
76826b5495 | ||
|
|
de315ff6d8 | ||
|
|
c26b716164 | ||
|
|
dcaddb8042 | ||
|
|
5ef197e5fd | ||
|
|
52ed02bd06 | ||
|
|
bd427849fb | ||
|
|
127d7acb91 | ||
|
|
484efff156 | ||
|
|
8f1d0e7a79 | ||
|
|
88184ff955 | ||
|
|
bd8c110f00 | ||
|
|
0050c51124 | ||
|
|
9b7ed73f95 | ||
|
|
a189cb8333 | ||
|
|
0d4825d3ea | ||
|
|
156b3200e3 | ||
|
|
572becad44 | ||
|
|
75f23d1130 | ||
|
|
e83fae3a6f | ||
|
|
5be5b8c02b | ||
|
|
6c4cb2bf00 | ||
|
|
148a23579c | ||
|
|
ea95811c4c | ||
|
|
08cb5f42e7 | ||
|
|
9abf37bbd1 | ||
|
|
89cffd43f4 | ||
|
|
d7fe724145 | ||
|
|
ad978700fc | ||
|
|
20df6348a4 | ||
|
|
48fb7006f7 | ||
|
|
7057984831 | ||
|
|
847510c6b5 | ||
|
|
774e0f2022 | ||
|
|
d1e12cd46f | ||
|
|
adf6d49fd1 | ||
|
|
0826a44d4b | ||
|
|
ce145b04ac | ||
|
|
29d775e675 | ||
|
|
251a47db8c | ||
|
|
e4b3a191d6 | ||
|
|
393d702e77 | ||
|
|
a3597d6deb | ||
|
|
8fada4d015 | ||
|
|
6d59f55fd4 | ||
|
|
937dce8e65 | ||
|
|
5c1562f36b | ||
|
|
8eb59072b4 | ||
|
|
20d053807d | ||
|
|
8aa4396e44 | ||
|
|
788a5b73f9 | ||
|
|
5ed27bf5f6 | ||
|
|
62bdaab7c7 | ||
|
|
bb2bc64d15 | ||
|
|
baa1eaf73a | ||
|
|
4c7da3899e | ||
|
|
4d3593ac86 | ||
|
|
c49a8e9114 | ||
|
|
d3bd9cd31d | ||
|
|
e3b14bc5a9 | ||
|
|
8e46aa7054 | ||
|
|
8a22597180 | ||
|
|
e919906c8c | ||
|
|
46dc6eac88 | ||
|
|
b9bf30e095 | ||
|
|
b60c51ef13 | ||
|
|
6b05938573 | ||
|
|
77601f7262 | ||
|
|
d9be51b2ef | ||
|
|
393259c24b | ||
|
|
756c4c032f | ||
|
|
e1360ae049 | ||
|
|
b048f8d733 | ||
|
|
f65f88325b | ||
|
|
42c8f6ae87 | ||
|
|
e4f3b777f9 | ||
|
|
a278f79bdb | ||
|
|
606c369173 | ||
|
|
3c871920b1 | ||
|
|
de6170a13d | ||
|
|
65931bb384 | ||
|
|
a5d53b3349 | ||
|
|
4487a90623 | ||
|
|
05da8cc3d1 | ||
|
|
23e499998f | ||
|
|
c156a4f723 | ||
|
|
3657bf6636 | ||
|
|
adade2e5ec | ||
|
|
c16913c999 | ||
|
|
12b61365ad | ||
|
|
9214dc6610 | ||
|
|
58b95e4ae4 | ||
|
|
debf909359 | ||
|
|
0d4d84b2fa | ||
|
|
d2aca3e7e0 | ||
|
|
26aca2b789 | ||
|
|
5424ede413 | ||
|
|
20112f8e16 | ||
|
|
4260a754e5 | ||
|
|
ce30f72738 | ||
|
|
69d430dd75 | ||
|
|
673545c7e4 | ||
|
|
4f69a03bb1 | ||
|
|
c2c18acd6a | ||
|
|
12ead36f96 | ||
|
|
41a642e06c | ||
|
|
c6ed4b8a1d | ||
|
|
65aa6573df |
6
.gitignore
vendored
6
.gitignore
vendored
@@ -1,2 +1,8 @@
|
||||
*.pyc
|
||||
build/
|
||||
dist/
|
||||
MANIFEST
|
||||
docs/_build/
|
||||
*.swp
|
||||
.tox/
|
||||
.coverage
|
||||
|
||||
4
LICENSE
4
LICENSE
@@ -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.
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
6
MANIFEST.in
Normal file
6
MANIFEST.in
Normal file
@@ -0,0 +1,6 @@
|
||||
include README.rst
|
||||
include LICENSE
|
||||
include testall.py
|
||||
recursive-include docs Makefile *.bat *.py *.rst *.css *.ttf *.png
|
||||
recursive-include examples *.py
|
||||
recursive-include tests *.py
|
||||
50
README
50
README
@@ -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.
|
||||
182
README.rst
Normal file
182
README.rst
Normal file
@@ -0,0 +1,182 @@
|
||||
SleekXMPP
|
||||
#########
|
||||
|
||||
SleekXMPP is an MIT licensed XMPP library for Python 2.6/3.1+,
|
||||
and is featured in examples in
|
||||
`XMPP: The Definitive Guide <http://oreilly.com/catalog/9780596521271>`_
|
||||
by Kevin Smith, Remko Tronçon, and Peter Saint-Andre. If you've arrived
|
||||
here from reading the Definitive Guide, please see the notes on updating
|
||||
the examples to the latest version of SleekXMPP.
|
||||
|
||||
SleekXMPP's design goals and philosphy are:
|
||||
|
||||
**Low number of dependencies**
|
||||
Installing and using SleekXMPP should be as simple as possible, without
|
||||
having to deal with long dependency chains.
|
||||
|
||||
As part of reducing the number of dependencies, some third party
|
||||
modules are included with SleekXMPP in the ``thirdparty`` directory.
|
||||
Imports from this module first try to import an existing installed
|
||||
version before loading the packaged version, when possible.
|
||||
|
||||
**Every XEP as a plugin**
|
||||
Following Python's "batteries included" approach, the goal is to
|
||||
provide support for all currently active XEPs (final and draft). Since
|
||||
adding XEP support is done through easy to create plugins, the hope is
|
||||
to also provide a solid base for implementing and creating experimental
|
||||
XEPs.
|
||||
|
||||
**Rewarding to work with**
|
||||
As much as possible, SleekXMPP should allow things to "just work" using
|
||||
sensible defaults and appropriate abstractions. XML can be ugly to work
|
||||
with, but it doesn't have to be that way.
|
||||
|
||||
|
||||
Get the Code
|
||||
------------
|
||||
|
||||
Get the latest stable version from PyPI::
|
||||
|
||||
pip install sleekxmpp
|
||||
|
||||
The latest source code for SleekXMPP may be found on `Github
|
||||
<http://github.com/fritzy/SleekXMPP>`_. Releases can be found in the
|
||||
``master`` branch, while the latest development version is in the
|
||||
``develop`` branch.
|
||||
|
||||
**Latest Release**
|
||||
- `1.0 <http://github.com/fritzy/SleekXMPP/zipball/1.0>`_
|
||||
|
||||
**Develop Releases**
|
||||
- `Latest Develop Version <http://github.com/fritzy/SleekXMPP/zipball/develop>`_
|
||||
|
||||
**Older Stable Releases**
|
||||
- `1.0 RC3 <http://github.com/fritzy/SleekXMPP/zipball/1.0-RC3>`_
|
||||
- `1.0 RC2 <http://github.com/fritzy/SleekXMPP/zipball/1.0-RC2>`_
|
||||
- `1.0 RC1 <http://github.com/fritzy/SleekXMPP/zipball/1.0-RC1>`_
|
||||
- `1.0 Beta6.1 <http://github.com/fritzy/SleekXMPP/zipball/1.0-Beta6.1>`_
|
||||
- `1.0 Beta5 <http://github.com/fritzy/SleekXMPP/zipball/1.0-Beta5>`_
|
||||
- `1.0 Beta4 <http://github.com/fritzy/SleekXMPP/zipball/1.0-Beta4>`_
|
||||
- `1.0 Beta3 <http://github.com/fritzy/SleekXMPP/zipball/1.0-Beta3>`_
|
||||
- `1.0 Beta2 <http://github.com/fritzy/SleekXMPP/zipball/1.0-Beta2>`_
|
||||
- `1.0 Beta1 <http://github.com/fritzy/SleekXMPP/zipball/1.0-Beta1>`_
|
||||
|
||||
Installing DNSPython
|
||||
---------------------
|
||||
If you are using Python3 and wish to use dnspython, you will have to checkout and
|
||||
install the ``python3`` branch::
|
||||
|
||||
git clone http://github.com/rthalley/dnspython
|
||||
cd dnspython
|
||||
git checkout python3
|
||||
python3 setup.py install
|
||||
|
||||
Discussion
|
||||
----------
|
||||
A mailing list and XMPP chat room are available for discussing and getting
|
||||
help with SleekXMPP.
|
||||
|
||||
**Mailing List**
|
||||
`SleekXMPP Discussion on Google Groups <http://groups.google.com/group/sleekxmpp-discussion>`_
|
||||
|
||||
**Chat**
|
||||
`sleek@conference.jabber.org <xmpp:sleek@conference.jabber.org?join>`_
|
||||
|
||||
Documentation and Testing
|
||||
-------------------------
|
||||
Documentation can be found both inline in the code, and as a Sphinx project in ``/docs``.
|
||||
To generate the Sphinx documentation, follow the commands below. The HTML output will
|
||||
be in ``docs/_build/html``::
|
||||
|
||||
cd docs
|
||||
make html
|
||||
open _build/html/index.html
|
||||
|
||||
To run the test suite for SleekXMPP::
|
||||
|
||||
python testall.py
|
||||
|
||||
|
||||
The SleekXMPP Boilerplate
|
||||
-------------------------
|
||||
Projects using SleekXMPP tend to follow a basic pattern for setting up client/component
|
||||
connections and configuration. Here is the gist of the boilerplate needed for a SleekXMPP
|
||||
based project. See the documetation or examples directory for more detailed archetypes for
|
||||
SleekXMPP projects::
|
||||
|
||||
import logging
|
||||
|
||||
from sleekxmpp import ClientXMPP
|
||||
from sleekxmpp.exceptions import IqError, IqTimeout
|
||||
|
||||
|
||||
class EchoBot(ClientXMPP):
|
||||
|
||||
def __init__(self, jid, password):
|
||||
ClientXMPP.__init__(self, jid, password)
|
||||
|
||||
self.add_event_handler("session_start", self.session_start)
|
||||
self.add_event_handler("message", self.message)
|
||||
|
||||
self.register_plugin('xep_0030') # Service Discovery
|
||||
self.register_plugin('xep_0199') # XMPP Ping
|
||||
|
||||
# If you are working with an OpenFire server, you will
|
||||
# need to use a different SSL version:
|
||||
# import ssl
|
||||
# self.ssl_version = ssl.PROTOCOL_SSLv3
|
||||
|
||||
def session_start(self, event):
|
||||
self.send_presence()
|
||||
|
||||
# Most get_*/set_* methods from plugins use Iq stanzas, which
|
||||
# can generate IqError and IqTimeout exceptions
|
||||
try:
|
||||
self.get_roster()
|
||||
except IqError as err:
|
||||
logging.error('There was an error getting the roster')
|
||||
logging.error(err.iq['error']['condition'])
|
||||
self.disconnect()
|
||||
except IqTimeout:
|
||||
logging.error('Server is taking too long to respond')
|
||||
self.disconnect()
|
||||
|
||||
def message(self, msg):
|
||||
if msg['type'] in ('chat', 'normal'):
|
||||
msg.reply("Thanks for sending\n%(body)s" % msg).send()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# Ideally use optparse or argparse to get JID,
|
||||
# password, and log level.
|
||||
|
||||
logging.basicConfig(level=logging.DEBUG,
|
||||
format='%(levelname)-8s %(message)s')
|
||||
|
||||
xmpp = EchoBot('somejid@example.com', 'use_getpass')
|
||||
xmpp.connect()
|
||||
xmpp.process(block=True)
|
||||
|
||||
|
||||
Credits
|
||||
-------
|
||||
**Main Author:** Nathan Fritz
|
||||
`fritzy@netflint.net <xmpp:fritzy@netflint.net?message>`_,
|
||||
`@fritzy <http://twitter.com/fritzy>`_
|
||||
|
||||
Nathan is also the author of XMPPHP and `Seesmic-AS3-XMPP
|
||||
<http://code.google.com/p/seesmic-as3-xmpp/>`_, and a former member of
|
||||
the XMPP Council.
|
||||
|
||||
**Co-Author:** Lance Stout
|
||||
`lancestout@gmail.com <xmpp:lancestout@gmail.com?message>`_,
|
||||
`@lancestout <http://twitter.com/lancestout>`_
|
||||
|
||||
**Contributors:**
|
||||
- Brian Beggs (`macdiesel <http://github.com/macdiesel>`_)
|
||||
- Dann Martens (`dannmartens <http://github.com/dannmartens>`_)
|
||||
- Florent Le Coz (`louiz <http://github.com/louiz>`_)
|
||||
- Kevin Smith (`Kev <http://github.com/Kev>`_, http://kismith.co.uk)
|
||||
- Remko Tronçon (`remko <http://github.com/remko>`_, http://el-tramo.be)
|
||||
- Te-jé Rogers (`te-je <http://github.com/te-je>`_)
|
||||
- Thom Nichols (`tomstrummer <http://github.com/tomstrummer>`_)
|
||||
@@ -122,14 +122,14 @@ if __name__ == '__main__':
|
||||
logging.basicConfig(level=opts.loglevel, format='%(levelname)-8s %(message)s')
|
||||
|
||||
#load xml config
|
||||
logging.info("Loading config file: %s" % opts.configfile)
|
||||
logging.info("Loading config file: %s" , opts.configfile)
|
||||
config = configparser.RawConfigParser()
|
||||
config.read(opts.configfile)
|
||||
|
||||
#init
|
||||
logging.info("Account 1 is %s" % config.get('account1', 'jid'))
|
||||
logging.info("Account 1 is %s" , config.get('account1', 'jid'))
|
||||
xmpp1 = TestClient(config.get('account1','jid'), config.get('account1','pass'))
|
||||
logging.info("Account 2 is %s" % config.get('account2', 'jid'))
|
||||
logging.info("Account 2 is %s" , config.get('account2', 'jid'))
|
||||
xmpp2 = TestClient(config.get('account2','jid'), config.get('account2','pass'))
|
||||
|
||||
xmpp1.registerPlugin('xep_0004')
|
||||
|
||||
@@ -186,14 +186,14 @@ if __name__ == '__main__':
|
||||
logging.basicConfig(level=opts.loglevel, format='%(levelname)-8s %(message)s')
|
||||
|
||||
#load xml config
|
||||
logging.info("Loading config file: %s" % opts.configfile)
|
||||
logging.info("Loading config file: %s" , opts.configfile)
|
||||
config = configparser.RawConfigParser()
|
||||
config.read(opts.configfile)
|
||||
|
||||
#init
|
||||
logging.info("Account 1 is %s" % config.get('account1', 'jid'))
|
||||
logging.info("Account 1 is %s" , config.get('account1', 'jid'))
|
||||
xmpp1 = TestClient(config.get('account1','jid'), config.get('account1','pass'))
|
||||
logging.info("Account 2 is %s" % config.get('account2', 'jid'))
|
||||
logging.info("Account 2 is %s" , config.get('account2', 'jid'))
|
||||
xmpp2 = TestClient(config.get('account2','jid'), config.get('account2','pass'))
|
||||
|
||||
xmpp1.registerPlugin('xep_0004')
|
||||
|
||||
@@ -329,11 +329,11 @@ if __name__ == '__main__':
|
||||
logging.basicConfig(level=opts.loglevel, format='%(levelname)-8s %(message)s')
|
||||
|
||||
#load xml config
|
||||
logging.info("Loading config file: %s" % opts.configfile)
|
||||
logging.info("Loading config file: %s" , opts.configfile)
|
||||
config = ET.parse(os.path.expanduser(opts.configfile)).find('auth')
|
||||
|
||||
#init
|
||||
logging.info("Logging in as %s" % config.attrib['jid'])
|
||||
logging.info("Logging in as %s" , config.attrib['jid'])
|
||||
|
||||
|
||||
plugin_config = {}
|
||||
|
||||
130
docs/Makefile
Normal file
130
docs/Makefile
Normal 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
452
docs/_static/agogo.css
vendored
Normal 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
532
docs/_static/basic.css
vendored
Normal 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
256
docs/_static/default.css
vendored
Normal 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
BIN
docs/_static/fonts/Museo_Slab_500.otf
vendored
Normal file
Binary file not shown.
BIN
docs/_static/fonts/Museo_Slab_500italic.otf
vendored
Normal file
BIN
docs/_static/fonts/Museo_Slab_500italic.otf
vendored
Normal file
Binary file not shown.
BIN
docs/_static/fonts/OFLGoudyStMTT-Italic.ttf
vendored
Normal file
BIN
docs/_static/fonts/OFLGoudyStMTT-Italic.ttf
vendored
Normal file
Binary file not shown.
BIN
docs/_static/fonts/OFLGoudyStMTT.ttf
vendored
Normal file
BIN
docs/_static/fonts/OFLGoudyStMTT.ttf
vendored
Normal file
Binary file not shown.
BIN
docs/_static/fonts/YanoneKaffeesatz-Bold.ttf
vendored
Normal file
BIN
docs/_static/fonts/YanoneKaffeesatz-Bold.ttf
vendored
Normal file
Binary file not shown.
BIN
docs/_static/fonts/YanoneKaffeesatz-Light.ttf
vendored
Normal file
BIN
docs/_static/fonts/YanoneKaffeesatz-Light.ttf
vendored
Normal file
Binary file not shown.
BIN
docs/_static/fonts/YanoneKaffeesatz-Regular.ttf
vendored
Normal file
BIN
docs/_static/fonts/YanoneKaffeesatz-Regular.ttf
vendored
Normal file
Binary file not shown.
BIN
docs/_static/fonts/YanoneKaffeesatz-Thin.ttf
vendored
Normal file
BIN
docs/_static/fonts/YanoneKaffeesatz-Thin.ttf
vendored
Normal file
Binary file not shown.
406
docs/_static/haiku.css
vendored
Normal file
406
docs/_static/haiku.css
vendored
Normal 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
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
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
70
docs/_static/ir_black.css
vendored
Normal 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
245
docs/_static/nature.css
vendored
Normal 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
BIN
docs/_static/noise_dk.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
339
docs/_static/sphinxdoc.css
vendored
Normal file
339
docs/_static/sphinxdoc.css
vendored
Normal 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;
|
||||
}
|
||||
8
docs/api/basexmpp.rst
Normal file
8
docs/api/basexmpp.rst
Normal file
@@ -0,0 +1,8 @@
|
||||
========
|
||||
BaseXMPP
|
||||
========
|
||||
|
||||
.. module:: sleekxmpp.basexmpp
|
||||
|
||||
.. autoclass:: BaseXMPP
|
||||
:members:
|
||||
8
docs/api/clientxmpp.rst
Normal file
8
docs/api/clientxmpp.rst
Normal file
@@ -0,0 +1,8 @@
|
||||
==========
|
||||
ClientXMPP
|
||||
==========
|
||||
|
||||
.. module:: sleekxmpp.clientxmpp
|
||||
|
||||
.. autoclass:: ClientXMPP
|
||||
:members:
|
||||
8
docs/api/componentxmpp.rst
Normal file
8
docs/api/componentxmpp.rst
Normal file
@@ -0,0 +1,8 @@
|
||||
=============
|
||||
ComponentXMPP
|
||||
=============
|
||||
|
||||
.. module:: sleekxmpp.componentxmpp
|
||||
|
||||
.. autoclass:: ComponentXMPP
|
||||
:members:
|
||||
14
docs/api/exceptions.rst
Normal file
14
docs/api/exceptions.rst
Normal file
@@ -0,0 +1,14 @@
|
||||
Exceptions
|
||||
==========
|
||||
|
||||
.. module:: sleekxmpp.exceptions
|
||||
|
||||
|
||||
.. autoexception:: XMPPError
|
||||
:members:
|
||||
|
||||
.. autoexception:: IqError
|
||||
:members:
|
||||
|
||||
.. autoexception:: IqTimeout
|
||||
:members:
|
||||
12
docs/api/xmlstream/filesocket.rst
Normal file
12
docs/api/xmlstream/filesocket.rst
Normal file
@@ -0,0 +1,12 @@
|
||||
.. module:: sleekxmpp.xmlstream.filesocket
|
||||
|
||||
.. _filesocket:
|
||||
|
||||
Python 2.6 File Socket Shims
|
||||
============================
|
||||
|
||||
.. autoclass:: FileSocket
|
||||
:members:
|
||||
|
||||
.. autoclass:: Socket26
|
||||
:members:
|
||||
24
docs/api/xmlstream/handler.rst
Normal file
24
docs/api/xmlstream/handler.rst
Normal file
@@ -0,0 +1,24 @@
|
||||
Stanza Handlers
|
||||
===============
|
||||
|
||||
The Basic Handler
|
||||
-----------------
|
||||
.. module:: sleekxmpp.xmlstream.handler.base
|
||||
|
||||
.. autoclass:: BaseHandler
|
||||
:members:
|
||||
|
||||
Callback
|
||||
--------
|
||||
.. module:: sleekxmpp.xmlstream.handler.callback
|
||||
|
||||
.. autoclass:: Callback
|
||||
:members:
|
||||
|
||||
|
||||
Waiter
|
||||
------
|
||||
.. module:: sleekxmpp.xmlstream.handler.waiter
|
||||
|
||||
.. autoclass:: Waiter
|
||||
:members:
|
||||
7
docs/api/xmlstream/jid.rst
Normal file
7
docs/api/xmlstream/jid.rst
Normal file
@@ -0,0 +1,7 @@
|
||||
Jabber IDs (JID)
|
||||
=================
|
||||
|
||||
.. module:: sleekxmpp.xmlstream.jid
|
||||
|
||||
.. autoclass:: JID
|
||||
:members:
|
||||
41
docs/api/xmlstream/matcher.rst
Normal file
41
docs/api/xmlstream/matcher.rst
Normal file
@@ -0,0 +1,41 @@
|
||||
Stanza Matchers
|
||||
===============
|
||||
|
||||
The Basic Matcher
|
||||
-----------------
|
||||
.. module:: sleekxmpp.xmlstream.matcher.base
|
||||
|
||||
.. autoclass:: MatcherBase
|
||||
:members:
|
||||
|
||||
|
||||
ID Matching
|
||||
-----------
|
||||
.. module:: sleekxmpp.xmlstream.matcher.id
|
||||
|
||||
.. autoclass:: MatcherId
|
||||
:members:
|
||||
|
||||
|
||||
Stanza Path Matching
|
||||
--------------------
|
||||
.. module:: sleekxmpp.xmlstream.matcher.stanzapath
|
||||
|
||||
.. autoclass:: StanzaPath
|
||||
:members:
|
||||
|
||||
|
||||
XPath
|
||||
-----
|
||||
.. module:: sleekxmpp.xmlstream.matcher.xpath
|
||||
|
||||
.. autoclass:: MatchXPath
|
||||
:members:
|
||||
|
||||
|
||||
XMLMask
|
||||
-------
|
||||
.. module:: sleekxmpp.xmlstream.matcher.xmlmask
|
||||
|
||||
.. autoclass:: MatchXMLMask
|
||||
:members:
|
||||
11
docs/api/xmlstream/scheduler.rst
Normal file
11
docs/api/xmlstream/scheduler.rst
Normal file
@@ -0,0 +1,11 @@
|
||||
=========
|
||||
Scheduler
|
||||
=========
|
||||
|
||||
.. module:: sleekxmpp.xmlstream.scheduler
|
||||
|
||||
.. autoclass:: Task
|
||||
:members:
|
||||
|
||||
.. autoclass:: Scheduler
|
||||
:members:
|
||||
123
docs/api/xmlstream/stanzabase.rst
Normal file
123
docs/api/xmlstream/stanzabase.rst
Normal file
@@ -0,0 +1,123 @@
|
||||
.. _stanzabase:
|
||||
|
||||
==============
|
||||
Stanza Objects
|
||||
==============
|
||||
|
||||
.. module:: sleekxmpp.xmlstream.stanzabase
|
||||
|
||||
The :mod:`~sleekmxpp.xmlstream.stanzabase` module provides a wrapper for the
|
||||
standard :mod:`~xml.etree.ElementTree` module that makes working with XML
|
||||
less painful. Instead of having to manually move up and down an element
|
||||
tree and insert subelements and attributes, you can interact with an object
|
||||
that behaves like a normal dictionary or JSON object, which silently maps
|
||||
keys to XML attributes and elements behind the scenes.
|
||||
|
||||
Overview
|
||||
--------
|
||||
|
||||
The usefulness of this layer grows as the XML you have to work with
|
||||
becomes nested. The base unit here, :class:`ElementBase`, can map to a
|
||||
single XML element, or several depending on how advanced of a mapping
|
||||
is desired from interface keys to XML structures. For example, a single
|
||||
:class:`ElementBase` derived class could easily describe:
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<message to="user@example.com" from="friend@example.com">
|
||||
<body>Hi!</body>
|
||||
<x:extra>
|
||||
<x:item>Custom item 1</x:item>
|
||||
<x:item>Custom item 2</x:item>
|
||||
<x:item>Custom item 3</x:item>
|
||||
</x:extra>
|
||||
</message>
|
||||
|
||||
If that chunk of XML were put in the :class:`ElementBase` instance
|
||||
``msg``, we could extract the data from the XML using::
|
||||
|
||||
>>> msg['extra']
|
||||
['Custom item 1', 'Custom item 2', 'Custom item 3']
|
||||
|
||||
Provided we set up the handler for the ``'extra'`` interface to load the
|
||||
``<x:item>`` element content into a list.
|
||||
|
||||
The key concept is that given an XML structure that will be repeatedly
|
||||
used, we can define a set of :term:`interfaces` which when we read from,
|
||||
write to, or delete, will automatically manipulate the underlying XML
|
||||
as needed. In addition, some of these interfaces may in turn reference
|
||||
child objects which expose interfaces for particularly complex child
|
||||
elements of the original XML chunk.
|
||||
|
||||
.. seealso::
|
||||
:ref:`create-stanza-interfaces`.
|
||||
|
||||
Because the :mod:`~sleekxmpp.xmlstream.stanzabase` module was developed
|
||||
as part of an `XMPP <http://xmpp.org>`_ library, these chunks of XML are
|
||||
referred to as :term:`stanzas <stanza>`, and in SleekXMPP we refer to a
|
||||
subclass of :class:`ElementBase` which defines the interfaces needed for
|
||||
interacting with a given :term:`stanza` a :term:`stanza object`.
|
||||
|
||||
To make dealing with more complicated and nested :term:`stanzas <stanza>`
|
||||
or XML chunks easier, :term:`stanza objects <stanza object>` can be
|
||||
composed in two ways: as iterable child objects or as plugins. Iterable
|
||||
child stanzas, or :term:`substanzas`, are accessible through a special
|
||||
``'substanzas'`` interface. This option is useful for stanzas which
|
||||
may contain more than one of the same kind of element. When there is
|
||||
only one child element, the plugin method is more useful. For plugins,
|
||||
a parent stanza object delegates one of its XML child elements to the
|
||||
plugin stanza object. Here is an example:
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<iq type="result">
|
||||
<query xmlns="http://jabber.org/protocol/disco#info">
|
||||
<identity category="client" type="bot" name="SleekXMPP Bot" />
|
||||
</query>
|
||||
</iq>
|
||||
|
||||
We can can arrange this stanza into two objects: an outer, wrapper object for
|
||||
dealing with the ``<iq />`` element and its attributes, and a plugin object to
|
||||
control the ``<query />`` payload element. If we give the plugin object the
|
||||
name ``'disco_info'`` (using its :attr:`ElementBase.plugin_attrib` value), then
|
||||
we can access the plugin as so::
|
||||
|
||||
>>> iq['disco_info']
|
||||
'<query xmlns="http://jabber.org/protocol/disco#info">
|
||||
<identity category="client" type="bot" name="SleekXMPP Bot" />
|
||||
</query>'
|
||||
|
||||
We can then drill down through the plugin object's interfaces as desired::
|
||||
|
||||
>>> iq['disco_info']['identities']
|
||||
[('client', 'bot', 'SleekXMPP Bot')]
|
||||
|
||||
Plugins may also add new interfaces to the parent stanza object as if they
|
||||
had been defined by the parent directly, and can also override the behaviour
|
||||
of an interface defined by the parent.
|
||||
|
||||
.. seealso::
|
||||
|
||||
- :ref:`create-stanza-plugins`
|
||||
- :ref:`create-extension-plugins`
|
||||
- :ref:`override-parent-interfaces`
|
||||
|
||||
|
||||
Registering Stanza Plugins
|
||||
--------------------------
|
||||
|
||||
.. autofunction:: register_stanza_plugin
|
||||
|
||||
ElementBase
|
||||
-----------
|
||||
|
||||
.. autoclass:: ElementBase
|
||||
:members:
|
||||
:private-members:
|
||||
:special-members:
|
||||
|
||||
StanzaBase
|
||||
----------
|
||||
|
||||
.. autoclass:: StanzaBase
|
||||
:members:
|
||||
46
docs/api/xmlstream/tostring.rst
Normal file
46
docs/api/xmlstream/tostring.rst
Normal file
@@ -0,0 +1,46 @@
|
||||
.. module:: sleekxmpp.xmlstream.tostring
|
||||
|
||||
.. _tostring:
|
||||
|
||||
XML Serialization
|
||||
=================
|
||||
|
||||
Since the XML layer of SleekXMPP is based on :mod:`~xml.etree.ElementTree`,
|
||||
why not just use the built-in :func:`~xml.etree.ElementTree.tostring`
|
||||
method? The answer is that using that method produces ugly results when
|
||||
using namespaces. The :func:`tostring()` method used here intelligently
|
||||
hides namespaces when able and does not introduce excessive namespace
|
||||
prefixes::
|
||||
|
||||
>>> from sleekxmpp.xmlstream.tostring import tostring
|
||||
>>> from xml.etree import cElementTree as ET
|
||||
>>> xml = ET.fromstring('<foo xmlns="bar"><baz /></foo>')
|
||||
>>> ET.tostring(xml)
|
||||
'<ns0:foo xmlns:ns0="bar"><ns0:baz /></foo>'
|
||||
>>> tostring(xml)
|
||||
'<foo xmlns="bar"><baz /></foo>'
|
||||
|
||||
As a side effect of this namespace hiding, using :func:`tostring()` may
|
||||
produce unexpected results depending on how the :func:`tostring()` method
|
||||
is invoked. For example, when sending XML on the wire, the main XMPP
|
||||
stanzas with their namespace of ``jabber:client`` will not include the
|
||||
namespace because that is already declared by the stream header. But, if
|
||||
you create a :class:`~sleekxmpp.stanza.message.Message` instance and dump
|
||||
it to the terminal, the ``jabber:client`` namespace will appear.
|
||||
|
||||
.. autofunction:: tostring
|
||||
|
||||
Escaping Special Characters
|
||||
---------------------------
|
||||
|
||||
In order to prevent errors when sending arbitrary text as the textual
|
||||
content of an XML element, certain characters must be escaped. These
|
||||
are: ``&``, ``<``, ``>``, ``"``, and ``'``. The default escaping
|
||||
mechanism is to replace those characters with their equivalent escape
|
||||
entities: ``&``, ``<``, ``>``, ``'``, and ``"``.
|
||||
|
||||
In the future, the use of CDATA sections may be allowed to reduce the
|
||||
size of escaped text or for when other XMPP processing agents do not
|
||||
undertand these entities.
|
||||
|
||||
.. autofunction:: xml_escape
|
||||
10
docs/api/xmlstream/xmlstream.rst
Normal file
10
docs/api/xmlstream/xmlstream.rst
Normal file
@@ -0,0 +1,10 @@
|
||||
==========
|
||||
XML Stream
|
||||
==========
|
||||
|
||||
.. module:: sleekxmpp.xmlstream.xmlstream
|
||||
|
||||
.. autoexception:: RestartStream
|
||||
|
||||
.. autoclass:: XMLStream
|
||||
:members:
|
||||
177
docs/architecture.rst
Normal file
177
docs/architecture.rst
Normal file
@@ -0,0 +1,177 @@
|
||||
.. 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
|
||||
-------------------------
|
||||
:class:`~sleekxmpp.xmlstream.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
|
||||
~~~~~~~~~~~~~~~~
|
||||
:class:`~sleekxmpp.xmlstream.xmlstream.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 :class:`~sleekxmpp.stanza.Message`
|
||||
:term:`stanza object` because the namespaced element name
|
||||
``{jabber:client}message`` is associated with the class
|
||||
: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 :class:`~sleekxmpp.stanza.Message` object is thus paired with the message stanza handler
|
||||
:meth:`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 event
|
||||
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
|
||||
:class:`~sleekxmpp.stanza.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 :meth:`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 :class:`~sleekxmpp.xmlstream.xmlstream.XMLStream` attempts to shy away
|
||||
from anything too XMPP specific, :class:`~sleekxmpp.basexmpp.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
|
||||
:class:`~sleekxmpp.basexmpp.BaseXMPP`.
|
||||
|
||||
.. index:: ClientXMPP, BaseXMPP
|
||||
|
||||
ClientXMPP
|
||||
----------
|
||||
:class:`~sleekxmpp.clientxmpp.ClientXMPP` extends
|
||||
:class:`~sleekxmpp.clientxmpp.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
|
||||
-------------
|
||||
:class:`~sleekxmpp.componentxmpp.ComponentXMPP` is only a thin layer on top of
|
||||
:class:`~sleekxmpp.basexmpp.BaseXMPP` that implements the component handshake
|
||||
protocol.
|
||||
222
docs/conf.py
Normal file
222
docs/conf.py
Normal file
@@ -0,0 +1,222 @@
|
||||
# -*- 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', 'sphinx.ext.intersphinx']
|
||||
|
||||
# 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.0RC3'
|
||||
|
||||
# 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 = 'tango'
|
||||
|
||||
# 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 = 'nature'
|
||||
|
||||
# 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)
|
||||
]
|
||||
|
||||
intersphinx_mapping = {'python': ('http://docs.python.org/3.2', 'python-objects.inv')}
|
||||
679
docs/create_plugin.rst
Normal file
679
docs/create_plugin.rst
Normal file
@@ -0,0 +1,679 @@
|
||||
.. _create-plugin:
|
||||
|
||||
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
271
docs/event_index.rst
Normal 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
2
docs/features.rst
Normal file
@@ -0,0 +1,2 @@
|
||||
How to Use Stream Features
|
||||
==========================
|
||||
75
docs/getting_started/component.rst
Normal file
75
docs/getting_started/component.rst
Normal file
@@ -0,0 +1,75 @@
|
||||
.. _echocomponent:
|
||||
|
||||
=================================
|
||||
Create and Run a Server Component
|
||||
=================================
|
||||
|
||||
.. 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
|
||||
|
||||
|
||||
Many XMPP applications eventually graduate to requiring to run as a server
|
||||
component in order to meet scalability requirements. To demonstrate how to
|
||||
turn an XMPP client bot into a component, we'll turn the echobot example
|
||||
(:ref:`echobot`) into a component version.
|
||||
|
||||
The first difference is that we will add an additional import statement:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from sleekxmpp.componentxmpp import ComponentXMPP
|
||||
|
||||
Likewise, we will change the bot's class definition to match:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class EchoComponent(ComponentXMPP):
|
||||
|
||||
def __init__(self, jid, secret, server, port):
|
||||
ComponentXMPP.__init__(self, jid, secret, server, port)
|
||||
|
||||
A component instance requires two extra parameters compared to a client
|
||||
instance: ``server`` and ``port``. These specifiy the name and port of
|
||||
the XMPP server that will be accepting the component. For example, for
|
||||
a MUC component, the following could be used:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
muc = ComponentXMPP('muc.sleekxmpp.com', '******', 'sleekxmpp.com', 5555)
|
||||
|
||||
.. note::
|
||||
|
||||
The ``server`` value is **NOT** derived from the provided JID for the
|
||||
component, unlike with client connections.
|
||||
|
||||
One difference with the component version is that we do not have
|
||||
to handle the :term:`session_start` event if we don't wish to deal
|
||||
with presence.
|
||||
|
||||
The other, main difference with components is that the
|
||||
``'from'`` value for every stanza must be explicitly set, since
|
||||
components may send stanzas from multiple JIDs. To do so,
|
||||
the :meth:`~sleekxmpp.basexmpp.BaseXMPP.send_message()` and
|
||||
:meth:`~sleekxmpp.basexmpp.BaseXMPP.send_presence()` accept the parameters
|
||||
``mfrom`` and ``pfrom``, respectively. For any method that uses
|
||||
:class:`~sleekxmpp.stanza.iq.Iq` stanzas, ``ifrom`` may be used.
|
||||
|
||||
|
||||
Final Product
|
||||
-------------
|
||||
|
||||
.. include:: ../../examples/echo_component.py
|
||||
:literal:
|
||||
390
docs/getting_started/echobot.rst
Normal file
390
docs/getting_started/echobot.rst
Normal 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:
|
||||
182
docs/getting_started/iq.rst
Normal file
182
docs/getting_started/iq.rst
Normal file
@@ -0,0 +1,182 @@
|
||||
Send/Receive IQ Stanzas
|
||||
=======================
|
||||
|
||||
Unlike :class:`~sleekxmpp.stanza.message.Message` and
|
||||
:class:`~sleekxmpp.stanza.presence.Presence` stanzas which only use
|
||||
text data for basic usage, :class:`~sleekxmpp.stanza.iq.Iq` stanzas
|
||||
require using XML payloads, and generally entail creating a new
|
||||
SleekXMPP plugin to provide the necessary convenience methods to
|
||||
make working with them easier.
|
||||
|
||||
Basic Use
|
||||
---------
|
||||
|
||||
XMPP's use of :class:`~sleekxmpp.stanza.iq.Iq` stanzas is built around
|
||||
namespaced ``<query />`` elements. For clients, just sending the
|
||||
empty ``<query />`` element will suffice for retrieving information. For
|
||||
example, a very basic implementation of service discovery would just
|
||||
need to be able to send:
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<iq to="user@example.com" type="get" id="1">
|
||||
<query xmlns="http://jabber.org/protocol/disco#info" />
|
||||
</iq>
|
||||
|
||||
Creating Iq Stanzas
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
SleekXMPP provides built-in support for creating basic :class:`~sleekxmpp.stanza.iq.Iq`
|
||||
stanzas this way. The relevant methods are:
|
||||
|
||||
* :meth:`~sleekxmpp.basexmpp.BaseXMPP.make_iq`
|
||||
* :meth:`~sleekxmpp.basexmpp.BaseXMPP.make_iq_get`
|
||||
* :meth:`~sleekxmpp.basexmpp.BaseXMPP.make_iq_set`
|
||||
* :meth:`~sleekxmpp.basexmpp.BaseXMPP.make_iq_result`
|
||||
* :meth:`~sleekxmpp.basexmpp.BaseXMPP.make_iq_error`
|
||||
* :meth:`~sleekxmpp.basexmpp.BaseXMPP.make_iq_query`
|
||||
|
||||
These methods all follow the same pattern: create or modify an existing
|
||||
:class:`~sleekxmpp.stanza.iq.Iq` stanza, set the ``'type'`` value based
|
||||
on the method name, and finally add a ``<query />`` element with the given
|
||||
namespace. For example, to produce the query above, you would use:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
self.make_iq_get(queryxmlns='http://jabber.org/protocol/disco#info',
|
||||
ito='user@example.com')
|
||||
|
||||
|
||||
Sending Iq Stanzas
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Once an :class:`~sleekxmpp.stanza.iq.Iq` stanza is created, sending it
|
||||
over the wire is done using its :meth:`~sleekxmpp.stanza.iq.Iq.send()`
|
||||
method, like any other stanza object. However, there are a few extra
|
||||
options to control how to wait for the query's response.
|
||||
|
||||
These options are:
|
||||
|
||||
* ``block``: The default behaviour is that :meth:`~sleekxmpp.stanza.iq.Iq.send()`
|
||||
will block until a response is received and the response stanza will be the
|
||||
return value. Setting ``block`` to ``False`` will cause the call to return
|
||||
immediately. In which case, you will need to arrange some way to capture
|
||||
the response stanza if you need it.
|
||||
|
||||
* ``timeout``: When using the blocking behaviour, the call will eventually
|
||||
timeout with an error. The default timeout is 30 seconds, but this may
|
||||
be overidden two ways. To change the timeout globally, set:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
self.response_timeout = 10
|
||||
|
||||
To change the timeout for a single call, the ``timeout`` parameter works:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
iq.send(timeout=60)
|
||||
|
||||
* ``callback``: When not using a blocking call, using the ``callback``
|
||||
argument is a simple way to register a handler that will execute
|
||||
whenever a response is finally received. Using this method, there
|
||||
is no timeout limit. In case you need to remove the callback, the
|
||||
name of the newly created callback is returned.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
cb_name = iq.send(callback=self.a_callback)
|
||||
|
||||
# ... later if we need to cancel
|
||||
self.remove_handler(cb_name)
|
||||
|
||||
Properly working with :class:`~sleekxmpp.stanza.iq.Iq` stanzas requires
|
||||
handling the intended, normal flow, error responses, and timed out
|
||||
requests. To make this easier, two exceptions may be thrown by
|
||||
:meth:`~sleekxmpp.stanza.iq.Iq.send()`: :exc:`~sleekxmpp.exceptions.IqError`
|
||||
and :exc:`~sleekxmpp.exceptions.IqTimeout`. These exceptions only
|
||||
apply to the default, blocking calls.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
try:
|
||||
resp = iq.send()
|
||||
# ... do stuff with expected Iq result
|
||||
except IqError as e:
|
||||
err_resp = e.iq
|
||||
# ... handle error case
|
||||
except IqTimeout:
|
||||
# ... no response received in time
|
||||
pass
|
||||
|
||||
If you do not care to distinguish between errors and timeouts, then you
|
||||
can combine both cases with a generic :exc:`~sleekxmpp.exceptions.XMPPError`
|
||||
exception:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
try:
|
||||
resp = iq.send()
|
||||
except XMPPError:
|
||||
# ... Don't care about the response
|
||||
pass
|
||||
|
||||
Advanced Use
|
||||
------------
|
||||
|
||||
Going beyond the basics provided by SleekXMPP requires building at least a
|
||||
rudimentary SleekXMPP plugin to create a :term:`stanza object` for
|
||||
interfacting with the :class:`~sleekxmpp.stanza.iq.Iq` payload.
|
||||
|
||||
.. seealso::
|
||||
|
||||
* :ref:`create-plugin`
|
||||
* :ref:`work-with-stanzas`
|
||||
* :ref:`using-handlers-matchers`
|
||||
|
||||
|
||||
The typical way to respond to :class:`~sleekxmpp.stanza.iq.Iq` requests is
|
||||
to register stream handlers. As an example, suppose we create a stanza class
|
||||
named ``CustomXEP`` which uses the XML element ``<query xmlns="custom-xep" />``,
|
||||
and has a :attr:`~sleekxmpp.xmlstream.stanzabase.ElementBase.plugin_attrib` value
|
||||
of ``custom_xep``.
|
||||
|
||||
There are two types of incoming :class:`~sleekxmpp.stanza.iq.Iq` requests:
|
||||
``get`` and ``set``. You can register a handler that will accept both and then
|
||||
filter by type as needed, as so:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
self.register_handler(Callback(
|
||||
'CustomXEP Handler',
|
||||
StanzaPath('iq/custom_xep'),
|
||||
self._handle_custom_iq))
|
||||
|
||||
# ...
|
||||
|
||||
def _handle_custom_iq(self, iq):
|
||||
if iq['type'] == 'get':
|
||||
# ...
|
||||
pass
|
||||
elif iq['type'] == 'set':
|
||||
# ...
|
||||
pass
|
||||
else:
|
||||
# ... This will capture error responses too
|
||||
pass
|
||||
|
||||
If you want to filter out query types beforehand, you can adjust the matching
|
||||
filter by using ``@type=get`` or ``@type=set`` if you are using the recommended
|
||||
:class:`~sleekxmpp.xmlstream.matcher.stanzapath.StanzaPath` matcher.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
self.register_handler(Callback(
|
||||
'CustomXEP Handler',
|
||||
StanzaPath('iq@type=get/custom_xep'),
|
||||
self._handle_custom_iq_get))
|
||||
|
||||
# ...
|
||||
|
||||
def _handle_custom_iq_get(self, iq):
|
||||
assert(iq['type'] == 'get')
|
||||
2
docs/getting_started/muc.rst
Normal file
2
docs/getting_started/muc.rst
Normal file
@@ -0,0 +1,2 @@
|
||||
Mulit-User Chat (MUC) Bot
|
||||
=========================
|
||||
2
docs/getting_started/presence.rst
Normal file
2
docs/getting_started/presence.rst
Normal file
@@ -0,0 +1,2 @@
|
||||
Manage Presence Subscriptions
|
||||
=============================
|
||||
42
docs/getting_started/proxy.rst
Normal file
42
docs/getting_started/proxy.rst
Normal file
@@ -0,0 +1,42 @@
|
||||
.. _proxy:
|
||||
|
||||
=========================
|
||||
Enable HTTP Proxy Support
|
||||
=========================
|
||||
|
||||
.. 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>`_.
|
||||
|
||||
In some instances, you may wish to route XMPP traffic through
|
||||
an HTTP proxy, probably to get around restrictive firewalls.
|
||||
SleekXMPP provides support for basic HTTP proxying with DIGEST
|
||||
authentication.
|
||||
|
||||
Enabling proxy support is done in two steps. The first is to instruct SleekXMPP
|
||||
to use a proxy, and the second is to configure the proxy details:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
xmpp = ClientXMPP(...)
|
||||
xmpp.use_proxy = True
|
||||
xmpp.proxy_config = {
|
||||
'host': 'proxy.example.com',
|
||||
'port': 5555,
|
||||
'username': 'example_user',
|
||||
'password': '******'
|
||||
}
|
||||
|
||||
The ``'username'`` and ``'password'`` fields are optional if the proxy does not
|
||||
require authentication.
|
||||
|
||||
|
||||
The Final Product
|
||||
-----------------
|
||||
|
||||
.. include:: ../../examples/proxy_echo_client.py
|
||||
:literal:
|
||||
2
docs/getting_started/scheduler.rst
Normal file
2
docs/getting_started/scheduler.rst
Normal file
@@ -0,0 +1,2 @@
|
||||
Send a Message Every 5 Minutes
|
||||
==============================
|
||||
94
docs/getting_started/sendlogout.rst
Normal file
94
docs/getting_started/sendlogout.rst
Normal 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
35
docs/glossary.rst
Normal 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
201
docs/guide_xep_0030.rst
Normal 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)
|
||||
4
docs/handlersmatchers.rst
Normal file
4
docs/handlersmatchers.rst
Normal file
@@ -0,0 +1,4 @@
|
||||
.. _using-handlers-matchers:
|
||||
|
||||
Using Stream Handlers and Matchers
|
||||
==================================
|
||||
30
docs/howto/stanzas.rst
Normal file
30
docs/howto/stanzas.rst
Normal file
@@ -0,0 +1,30 @@
|
||||
.. _work-with-stanzas:
|
||||
|
||||
How to Work with Stanza Objects
|
||||
===============================
|
||||
|
||||
|
||||
.. _create-stanza-interfaces:
|
||||
|
||||
Defining Stanza Interfaces
|
||||
--------------------------
|
||||
|
||||
|
||||
.. _create-stanza-plugins:
|
||||
|
||||
Creating Stanza Plugins
|
||||
-----------------------
|
||||
|
||||
|
||||
|
||||
.. _create-extension-plugins:
|
||||
|
||||
Creating a Stanza Extension
|
||||
---------------------------
|
||||
|
||||
|
||||
|
||||
.. _override-parent-interfaces:
|
||||
|
||||
Overriding a Parent Stanza
|
||||
--------------------------
|
||||
179
docs/index.rst
Normal file
179
docs/index.rst
Normal file
@@ -0,0 +1,179 @@
|
||||
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.
|
||||
|
||||
**Latest Stable Release**
|
||||
- `1.0 RC3 <http://github.com/fritzy/SleekXMPP/zipball/1.0-RC3>`_
|
||||
|
||||
**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
|
||||
|
||||
faq
|
||||
xeps
|
||||
xmpp_tdg
|
||||
howto/stanzas
|
||||
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/componentxmpp
|
||||
api/basexmpp
|
||||
api/exceptions
|
||||
api/xmlstream/jid
|
||||
api/xmlstream/stanzabase
|
||||
api/xmlstream/handler
|
||||
api/xmlstream/matcher
|
||||
api/xmlstream/xmlstream
|
||||
api/xmlstream/scheduler
|
||||
api/xmlstream/tostring
|
||||
api/xmlstream/filesocket
|
||||
|
||||
Core Stanzas
|
||||
~~~~~~~~~~~~
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
api/stanza/rootstanza
|
||||
api/stanza/message
|
||||
api/stanza/presence
|
||||
api/stanza/iq
|
||||
api/stanza/error
|
||||
api/stanza/stream_error
|
||||
|
||||
Plugins
|
||||
~~~~~~~
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
|
||||
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
5
docs/license.rst
Normal file
@@ -0,0 +1,5 @@
|
||||
.. _license:
|
||||
|
||||
License (MIT)
|
||||
=============
|
||||
.. include:: ../LICENSE
|
||||
170
docs/make.bat
Normal file
170
docs/make.bat
Normal 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
2
docs/plugin_arch.rst
Normal file
@@ -0,0 +1,2 @@
|
||||
Plugin Architecture
|
||||
===================
|
||||
BIN
docs/python-objects.inv
Normal file
BIN
docs/python-objects.inv
Normal file
Binary file not shown.
2
docs/sasl.rst
Normal file
2
docs/sasl.rst
Normal file
@@ -0,0 +1,2 @@
|
||||
How SASL Authentication Works
|
||||
=============================
|
||||
50
docs/xeps.rst
Normal file
50
docs/xeps.rst
Normal file
@@ -0,0 +1,50 @@
|
||||
Supported XEPS
|
||||
==============
|
||||
|
||||
======= ============================= ================
|
||||
XEP Description Notes
|
||||
======= ============================= ================
|
||||
`0004`_ Data forms
|
||||
`0009`_ Jabber RPC
|
||||
`0012`_ Last Activity
|
||||
`0030`_ Service Discovery
|
||||
`0033`_ Extended Stanza Addressing
|
||||
`0045`_ Multi-User Chat (MUC) Client-side only
|
||||
`0050`_ Ad-hoc Commands
|
||||
`0059`_ Result Set Management
|
||||
`0060`_ Publish/Subscribe (PubSub) Client-side only
|
||||
`0066`_ Out-of-band Data
|
||||
`0078`_ Non-SASL Authentication
|
||||
`0082`_ XMPP Date and Time Profiles
|
||||
`0085`_ Chat-State Notifications
|
||||
`0086`_ Error Condition Mappings
|
||||
`0092`_ Software Version
|
||||
`0128`_ Service Discovery Extensions
|
||||
`0202`_ Entity Time
|
||||
`0203`_ Delayed Delivery
|
||||
`0224`_ Attention
|
||||
`0249`_ Direct MUC Invitations
|
||||
======= ============================= ================
|
||||
|
||||
|
||||
.. _0004: http://xmpp.org/extensions/xep-0004.html
|
||||
.. _0009: http://xmpp.org/extensions/xep-0009.html
|
||||
.. _0012: http://xmpp.org/extensions/xep-0012.html
|
||||
.. _0030: http://xmpp.org/extensions/xep-0030.html
|
||||
.. _0033: http://xmpp.org/extensions/xep-0033.html
|
||||
.. _0045: http://xmpp.org/extensions/xep-0045.html
|
||||
.. _0050: http://xmpp.org/extensions/xep-0050.html
|
||||
.. _0059: http://xmpp.org/extensions/xep-0059.html
|
||||
.. _0060: http://xmpp.org/extensions/xep-0060.html
|
||||
.. _0066: http://xmpp.org/extensions/xep-0066.html
|
||||
.. _0078: http://xmpp.org/extensions/xep-0078.html
|
||||
.. _0082: http://xmpp.org/extensions/xep-0082.html
|
||||
.. _0085: http://xmpp.org/extensions/xep-0085.html
|
||||
.. _0086: http://xmpp.org/extensions/xep-0086.html
|
||||
.. _0092: http://xmpp.org/extensions/xep-0092.html
|
||||
.. _0128: http://xmpp.org/extensions/xep-0128.html
|
||||
.. _0199: http://xmpp.org/extensions/xep-0199.html
|
||||
.. _0202: http://xmpp.org/extensions/xep-0202.html
|
||||
.. _0203: http://xmpp.org/extensions/xep-0203.html
|
||||
.. _0224: http://xmpp.org/extensions/xep-0224.html
|
||||
.. _0249: http://xmpp.org/extensions/xep-0249.html
|
||||
249
docs/xmpp_tdg.rst
Normal file
249
docs/xmpp_tdg.rst
Normal 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>`_
|
||||
@@ -11,7 +11,6 @@
|
||||
|
||||
import sys
|
||||
import logging
|
||||
import time
|
||||
import getpass
|
||||
from optparse import OptionParser
|
||||
|
||||
@@ -24,6 +23,8 @@ import sleekxmpp
|
||||
if sys.version_info < (3, 0):
|
||||
reload(sys)
|
||||
sys.setdefaultencoding('utf8')
|
||||
else:
|
||||
raw_input = input
|
||||
|
||||
|
||||
class CommandBot(sleekxmpp.ClientXMPP):
|
||||
@@ -39,7 +40,7 @@ class CommandBot(sleekxmpp.ClientXMPP):
|
||||
# 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
|
||||
# listen for this event so that we we can initialize
|
||||
# our roster.
|
||||
self.add_event_handler("session_start", self.start)
|
||||
|
||||
@@ -48,7 +49,7 @@ class CommandBot(sleekxmpp.ClientXMPP):
|
||||
Process the session_start event.
|
||||
|
||||
Typical actions for the session_start event are
|
||||
requesting the roster and broadcasting an intial
|
||||
requesting the roster and broadcasting an initial
|
||||
presence stanza.
|
||||
|
||||
Arguments:
|
||||
@@ -70,7 +71,7 @@ class CommandBot(sleekxmpp.ClientXMPP):
|
||||
|
||||
def _handle_command(self, iq, session):
|
||||
"""
|
||||
Respond to the intial request for a command.
|
||||
Respond to the initial request for a command.
|
||||
|
||||
Arguments:
|
||||
iq -- The iq stanza containing the command request.
|
||||
@@ -79,6 +80,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 +125,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 +180,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:
|
||||
@@ -186,14 +191,14 @@ if __name__ == '__main__':
|
||||
|
||||
# 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
|
||||
# If you do not have the dnspython 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)
|
||||
xmpp.process(block=True)
|
||||
print("Done")
|
||||
else:
|
||||
print("Unable to connect.")
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
|
||||
import sys
|
||||
import logging
|
||||
import time
|
||||
import getpass
|
||||
from optparse import OptionParser
|
||||
|
||||
@@ -24,6 +23,8 @@ import sleekxmpp
|
||||
if sys.version_info < (3, 0):
|
||||
reload(sys)
|
||||
sys.setdefaultencoding('utf8')
|
||||
else:
|
||||
raw_input = input
|
||||
|
||||
|
||||
class CommandUserBot(sleekxmpp.ClientXMPP):
|
||||
@@ -42,7 +43,7 @@ class CommandUserBot(sleekxmpp.ClientXMPP):
|
||||
# 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
|
||||
# listen for this event so that we we can initialize
|
||||
# our roster.
|
||||
self.add_event_handler("session_start", self.start)
|
||||
self.add_event_handler("message", self.message)
|
||||
@@ -52,7 +53,7 @@ class CommandUserBot(sleekxmpp.ClientXMPP):
|
||||
Process the session_start event.
|
||||
|
||||
Typical actions for the session_start event are
|
||||
requesting the roster and broadcasting an intial
|
||||
requesting the roster and broadcasting an initial
|
||||
presence stanza.
|
||||
|
||||
Arguments:
|
||||
@@ -136,6 +137,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 +178,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
|
||||
@@ -195,14 +197,14 @@ if __name__ == '__main__':
|
||||
|
||||
# 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
|
||||
# If you do not have the dnspython 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)
|
||||
xmpp.process(block=True)
|
||||
print("Done")
|
||||
else:
|
||||
print("Unable to connect.")
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
<config xmlns="sleekxmpp:config">
|
||||
<jid>component.localhost</jid>
|
||||
<secret>ssshh</secret>
|
||||
<server>localhost</server>
|
||||
<port>8888</port>
|
||||
|
||||
<query xmlns="jabber:iq:roster">
|
||||
<item jid="user@example.com" subscription="both" />
|
||||
</query>
|
||||
</config>
|
||||
@@ -1,190 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
SleekXMPP: The Sleek XMPP Library
|
||||
Copyright (C) 2010 Nathanael C. Fritz
|
||||
This file is part of SleekXMPP.
|
||||
|
||||
See the file LICENSE for copying permission.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import logging
|
||||
import time
|
||||
from optparse import OptionParser
|
||||
|
||||
import sleekxmpp
|
||||
from sleekxmpp.componentxmpp import ComponentXMPP
|
||||
from sleekxmpp.stanza.roster import Roster
|
||||
from sleekxmpp.xmlstream import ElementBase
|
||||
from sleekxmpp.xmlstream.stanzabase import ET, registerStanzaPlugin
|
||||
|
||||
# Python versions before 3.0 do not use UTF-8 encoding
|
||||
# by default. To ensure that Unicode is handled properly
|
||||
# throughout SleekXMPP, we will set the default encoding
|
||||
# ourselves to UTF-8.
|
||||
if sys.version_info < (3, 0):
|
||||
reload(sys)
|
||||
sys.setdefaultencoding('utf8')
|
||||
|
||||
|
||||
class Config(ElementBase):
|
||||
|
||||
"""
|
||||
In order to make loading and manipulating an XML config
|
||||
file easier, we will create a custom stanza object for
|
||||
our config XML file contents. See the documentation
|
||||
on stanza objects for more information on how to create
|
||||
and use stanza objects and stanza plugins.
|
||||
|
||||
We will reuse the IQ roster query stanza to store roster
|
||||
information since it already exists.
|
||||
|
||||
Example config XML:
|
||||
<config xmlns="sleekxmpp:config">
|
||||
<jid>component.localhost</jid>
|
||||
<secret>ssshh</secret>
|
||||
<server>localhost</server>
|
||||
<port>8888</port>
|
||||
|
||||
<query xmlns="jabber:iq:roster">
|
||||
<item jid="user@example.com" subscription="both" />
|
||||
</query>
|
||||
</config>
|
||||
"""
|
||||
|
||||
name = "config"
|
||||
namespace = "sleekxmpp:config"
|
||||
interfaces = set(('jid', 'secret', 'server', 'port'))
|
||||
sub_interfaces = interfaces
|
||||
|
||||
|
||||
registerStanzaPlugin(Config, Roster)
|
||||
|
||||
|
||||
class ConfigComponent(ComponentXMPP):
|
||||
|
||||
"""
|
||||
A simple SleekXMPP component that uses an external XML
|
||||
file to store its configuration data. To make testing
|
||||
that the component works, it will also echo messages sent
|
||||
to it.
|
||||
"""
|
||||
|
||||
def __init__(self, config):
|
||||
"""
|
||||
Create a ConfigComponent.
|
||||
|
||||
Arguments:
|
||||
config -- The XML contents of the config file.
|
||||
config_file -- The XML config file object itself.
|
||||
"""
|
||||
ComponentXMPP.__init__(self, config['jid'],
|
||||
config['secret'],
|
||||
config['server'],
|
||||
config['port'])
|
||||
|
||||
# Store the roster information.
|
||||
self.roster = config['roster']['items']
|
||||
|
||||
# The session_start event will be triggered when
|
||||
# the component establishes its connection with the
|
||||
# server and the XML streams are ready for use. We
|
||||
# want to listen for this event so that we we can
|
||||
# broadcast any needed initial presence stanzas.
|
||||
self.add_event_handler("session_start", self.start)
|
||||
|
||||
# The message event is triggered whenever a message
|
||||
# stanza is received. Be aware that that includes
|
||||
# MUC messages and error messages.
|
||||
self.add_event_handler("message", self.message)
|
||||
|
||||
def start(self, event):
|
||||
"""
|
||||
Process the session_start event.
|
||||
|
||||
The typical action for the session_start event in a component
|
||||
is to broadcast presence stanzas to all subscribers to the
|
||||
component. Note that the component does not have a roster
|
||||
provided by the XMPP server. In this case, we have possibly
|
||||
saved a roster in the component's configuration file.
|
||||
|
||||
Since the component may use any number of JIDs, you should
|
||||
also include the JID that is sending the presence.
|
||||
|
||||
Arguments:
|
||||
event -- An empty dictionary. The session_start
|
||||
event does not provide any additional
|
||||
data.
|
||||
"""
|
||||
for jid in self.roster:
|
||||
if self.roster[jid]['subscription'] != 'none':
|
||||
self.sendPresence(pfrom=self.jid, pto=jid)
|
||||
|
||||
def message(self, msg):
|
||||
"""
|
||||
Process incoming message stanzas. Be aware that this also
|
||||
includes MUC messages and error messages. It is usually
|
||||
a good idea to check the messages's type before processing
|
||||
or sending replies.
|
||||
|
||||
Since a component may send messages from any number of JIDs,
|
||||
it is best to always include a from JID.
|
||||
|
||||
Arguments:
|
||||
msg -- The received message stanza. See the documentation
|
||||
for stanza objects and the Message stanza to see
|
||||
how it may be used.
|
||||
"""
|
||||
# The reply method will use the messages 'to' JID as the
|
||||
# outgoing reply's 'from' JID.
|
||||
msg.reply("Thanks for sending\n%(body)s" % msg).send()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# Setup the command line arguments.
|
||||
optp = OptionParser()
|
||||
|
||||
# Output verbosity options.
|
||||
optp.add_option('-q', '--quiet', help='set logging to ERROR',
|
||||
action='store_const', dest='loglevel',
|
||||
const=logging.ERROR, default=logging.INFO)
|
||||
optp.add_option('-d', '--debug', help='set logging to DEBUG',
|
||||
action='store_const', dest='loglevel',
|
||||
const=logging.DEBUG, default=logging.INFO)
|
||||
optp.add_option('-v', '--verbose', help='set logging to COMM',
|
||||
action='store_const', dest='loglevel',
|
||||
const=5, default=logging.INFO)
|
||||
|
||||
# Component name and secret options.
|
||||
optp.add_option("-c", "--config", help="path to config file",
|
||||
dest="config", default="config.xml")
|
||||
|
||||
opts, args = optp.parse_args()
|
||||
|
||||
# Setup logging.
|
||||
logging.basicConfig(level=opts.loglevel,
|
||||
format='%(levelname)-8s %(message)s')
|
||||
|
||||
# Load configuration data.
|
||||
config_file = open(opts.config, 'r+')
|
||||
config_data = "\n".join([line for line in config_file])
|
||||
config = Config(xml=ET.fromstring(config_data))
|
||||
config_file.close()
|
||||
|
||||
# Setup the ConfigComponent and register plugins. Note that while plugins
|
||||
# may have interdependencies, the order in which you register them does
|
||||
# not matter.
|
||||
xmpp = ConfigComponent(config)
|
||||
xmpp.registerPlugin('xep_0030') # Service Discovery
|
||||
xmpp.registerPlugin('xep_0004') # Data Forms
|
||||
xmpp.registerPlugin('xep_0060') # PubSub
|
||||
xmpp.registerPlugin('xep_0199') # XMPP Ping
|
||||
|
||||
# Connect to the XMPP server and start processing XMPP stanzas.
|
||||
if xmpp.connect():
|
||||
xmpp.process(threaded=False)
|
||||
print("Done")
|
||||
else:
|
||||
print("Unable to connect.")
|
||||
@@ -10,7 +10,6 @@
|
||||
"""
|
||||
|
||||
import sys
|
||||
import time
|
||||
import logging
|
||||
import getpass
|
||||
from optparse import OptionParser
|
||||
@@ -25,6 +24,8 @@ import sleekxmpp
|
||||
if sys.version_info < (3, 0):
|
||||
reload(sys)
|
||||
sys.setdefaultencoding('utf8')
|
||||
else:
|
||||
raw_input = input
|
||||
|
||||
|
||||
class Disco(sleekxmpp.ClientXMPP):
|
||||
@@ -59,7 +60,7 @@ class Disco(sleekxmpp.ClientXMPP):
|
||||
# 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
|
||||
# listen for this event so that we we can initialize
|
||||
# our roster.
|
||||
self.add_event_handler("session_start", self.start)
|
||||
|
||||
@@ -68,7 +69,7 @@ class Disco(sleekxmpp.ClientXMPP):
|
||||
Process the session_start event.
|
||||
|
||||
Typical actions for the session_start event are
|
||||
requesting the roster and broadcasting an intial
|
||||
requesting the roster and broadcasting an initial
|
||||
presence stanza.
|
||||
|
||||
In this case, we send disco#info and disco#items
|
||||
@@ -186,13 +187,13 @@ if __name__ == '__main__':
|
||||
|
||||
# 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
|
||||
# If you do not have the dnspython 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)
|
||||
xmpp.process(block=True)
|
||||
else:
|
||||
print("Unable to connect.")
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
|
||||
import sys
|
||||
import logging
|
||||
import time
|
||||
import getpass
|
||||
from optparse import OptionParser
|
||||
|
||||
@@ -24,6 +23,8 @@ import sleekxmpp
|
||||
if sys.version_info < (3, 0):
|
||||
reload(sys)
|
||||
sys.setdefaultencoding('utf8')
|
||||
else:
|
||||
raw_input = input
|
||||
|
||||
|
||||
class EchoBot(sleekxmpp.ClientXMPP):
|
||||
@@ -39,7 +40,7 @@ class EchoBot(sleekxmpp.ClientXMPP):
|
||||
# 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
|
||||
# listen for this event so that we we can initialize
|
||||
# our roster.
|
||||
self.add_event_handler("session_start", self.start)
|
||||
|
||||
@@ -53,7 +54,7 @@ class EchoBot(sleekxmpp.ClientXMPP):
|
||||
Process the session_start event.
|
||||
|
||||
Typical actions for the session_start event are
|
||||
requesting the roster and broadcasting an intial
|
||||
requesting the roster and broadcasting an initial
|
||||
presence stanza.
|
||||
|
||||
Arguments:
|
||||
@@ -76,7 +77,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__':
|
||||
@@ -129,14 +131,14 @@ if __name__ == '__main__':
|
||||
|
||||
# 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
|
||||
# If you do not have the dnspython 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)
|
||||
xmpp.process(block=True)
|
||||
print("Done")
|
||||
else:
|
||||
print("Unable to connect.")
|
||||
|
||||
122
examples/echo_component.py
Executable file
122
examples/echo_component.py
Executable file
@@ -0,0 +1,122 @@
|
||||
#!/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 getpass
|
||||
from optparse import OptionParser
|
||||
|
||||
import sleekxmpp
|
||||
from sleekxmpp.componentxmpp import ComponentXMPP
|
||||
|
||||
# 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 EchoComponent(ComponentXMPP):
|
||||
|
||||
"""
|
||||
A simple SleekXMPP component that echoes messages.
|
||||
"""
|
||||
|
||||
def __init__(self, jid, secret, server, port):
|
||||
ComponentXMPP.__init__(self, jid, secret, server, port)
|
||||
|
||||
# You don't need a session_start handler, but that is
|
||||
# where you would broadcast initial presence.
|
||||
|
||||
# The message event is triggered whenever a message
|
||||
# stanza is received. Be aware that that includes
|
||||
# MUC messages and error messages.
|
||||
self.add_event_handler("message", self.message)
|
||||
|
||||
def message(self, msg):
|
||||
"""
|
||||
Process incoming message stanzas. Be aware that this also
|
||||
includes MUC messages and error messages. It is usually
|
||||
a good idea to check the messages's type before processing
|
||||
or sending replies.
|
||||
|
||||
Since a component may send messages from any number of JIDs,
|
||||
it is best to always include a from JID.
|
||||
|
||||
Arguments:
|
||||
msg -- The received message stanza. See the documentation
|
||||
for stanza objects and the Message stanza to see
|
||||
how it may be used.
|
||||
"""
|
||||
# The reply method will use the messages 'to' JID as the
|
||||
# outgoing reply's 'from' JID.
|
||||
msg.reply("Thanks for sending\n%(body)s" % msg).send()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# Setup the command line arguments.
|
||||
optp = OptionParser()
|
||||
|
||||
# Output verbosity options.
|
||||
optp.add_option('-q', '--quiet', help='set logging to ERROR',
|
||||
action='store_const', dest='loglevel',
|
||||
const=logging.ERROR, default=logging.INFO)
|
||||
optp.add_option('-d', '--debug', help='set logging to DEBUG',
|
||||
action='store_const', dest='loglevel',
|
||||
const=logging.DEBUG, default=logging.INFO)
|
||||
optp.add_option('-v', '--verbose', help='set logging to COMM',
|
||||
action='store_const', dest='loglevel',
|
||||
const=5, default=logging.INFO)
|
||||
|
||||
# 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("-s", "--server", dest="server",
|
||||
help="server to connect to")
|
||||
optp.add_option("-P", "--port", dest="port",
|
||||
help="port to connect to")
|
||||
|
||||
opts, args = optp.parse_args()
|
||||
|
||||
if opts.jid is None:
|
||||
opts.jid = raw_input("Component JID: ")
|
||||
if opts.password is None:
|
||||
opts.password = getpass.getpass("Password: ")
|
||||
if opts.server is None:
|
||||
opts.server = raw_input("Server: ")
|
||||
if opts.port is None:
|
||||
opts.port = int(raw_input("Port: "))
|
||||
|
||||
# Setup logging.
|
||||
logging.basicConfig(level=opts.loglevel,
|
||||
format='%(levelname)-8s %(message)s')
|
||||
|
||||
# Setup the EchoComponent and register plugins. Note that while plugins
|
||||
# may have interdependencies, the order in which you register them does
|
||||
# not matter.
|
||||
xmpp = EchoComponent(opts.jid, opts.password, opts.server, opts.port)
|
||||
xmpp.registerPlugin('xep_0030') # Service Discovery
|
||||
xmpp.registerPlugin('xep_0004') # Data Forms
|
||||
xmpp.registerPlugin('xep_0060') # PubSub
|
||||
xmpp.registerPlugin('xep_0199') # XMPP Ping
|
||||
|
||||
# Connect to the XMPP server and start processing XMPP stanzas.
|
||||
if xmpp.connect():
|
||||
xmpp.process(block=True)
|
||||
print("Done")
|
||||
else:
|
||||
print("Unable to connect.")
|
||||
@@ -11,7 +11,7 @@
|
||||
|
||||
import sys
|
||||
import logging
|
||||
import time
|
||||
import getpass
|
||||
from optparse import OptionParser
|
||||
|
||||
import sleekxmpp
|
||||
@@ -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):
|
||||
@@ -42,7 +44,7 @@ class MUCBot(sleekxmpp.ClientXMPP):
|
||||
# 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
|
||||
# listen for this event so that we we can initialize
|
||||
# our roster.
|
||||
self.add_event_handler("session_start", self.start)
|
||||
|
||||
@@ -59,14 +61,14 @@ 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):
|
||||
"""
|
||||
Process the session_start event.
|
||||
|
||||
Typical actions for the session_start event are
|
||||
requesting the roster and broadcasting an intial
|
||||
requesting the roster and broadcasting an initial
|
||||
presence stanza.
|
||||
|
||||
Arguments:
|
||||
@@ -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.
|
||||
"""
|
||||
@@ -159,9 +161,14 @@ if __name__ == '__main__':
|
||||
logging.basicConfig(level=opts.loglevel,
|
||||
format='%(levelname)-8s %(message)s')
|
||||
|
||||
if None in [opts.jid, opts.password, opts.room, opts.nick]:
|
||||
optp.print_help()
|
||||
sys.exit(1)
|
||||
if opts.jid is None:
|
||||
opts.jid = raw_input("Username: ")
|
||||
if opts.password is None:
|
||||
opts.password = getpass.getpass("Password: ")
|
||||
if opts.room is None:
|
||||
opts.room = raw_input("MUC room: ")
|
||||
if opts.nick is None:
|
||||
opts.nick = raw_input("MUC nickname: ")
|
||||
|
||||
# Setup the MUCBot and register plugins. Note that while plugins may
|
||||
# have interdependencies, the order in which you register them does
|
||||
@@ -173,14 +180,14 @@ if __name__ == '__main__':
|
||||
|
||||
# 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
|
||||
# If you do not have the dnspython 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)
|
||||
xmpp.process(block=True)
|
||||
print("Done")
|
||||
else:
|
||||
print("Unable to connect.")
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
|
||||
import sys
|
||||
import logging
|
||||
import time
|
||||
import getpass
|
||||
from optparse import OptionParser
|
||||
|
||||
@@ -24,6 +23,8 @@ import sleekxmpp
|
||||
if sys.version_info < (3, 0):
|
||||
reload(sys)
|
||||
sys.setdefaultencoding('utf8')
|
||||
else:
|
||||
raw_input = input
|
||||
|
||||
|
||||
class PingTest(sleekxmpp.ClientXMPP):
|
||||
@@ -42,7 +43,7 @@ class PingTest(sleekxmpp.ClientXMPP):
|
||||
# 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
|
||||
# listen for this event so that we we can initialize
|
||||
# our roster.
|
||||
self.add_event_handler("session_start", self.start)
|
||||
|
||||
@@ -51,7 +52,7 @@ class PingTest(sleekxmpp.ClientXMPP):
|
||||
Process the session_start event.
|
||||
|
||||
Typical actions for the session_start event are
|
||||
requesting the roster and broadcasting an intial
|
||||
requesting the roster and broadcasting an initial
|
||||
presence stanza.
|
||||
|
||||
Arguments:
|
||||
@@ -70,7 +71,7 @@ class PingTest(sleekxmpp.ClientXMPP):
|
||||
self.disconnect()
|
||||
sys.exit(1)
|
||||
else:
|
||||
logging.info("Success! RTT: %s" % str(result))
|
||||
logging.info("Success! RTT: %s", str(result))
|
||||
self.disconnect()
|
||||
|
||||
|
||||
@@ -127,14 +128,14 @@ if __name__ == '__main__':
|
||||
|
||||
# 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
|
||||
# If you do not have the dnspython 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)
|
||||
xmpp.process(block=True)
|
||||
print("Done")
|
||||
else:
|
||||
print("Unable to connect.")
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
|
||||
import sys
|
||||
import logging
|
||||
import time
|
||||
import getpass
|
||||
from optparse import OptionParser
|
||||
|
||||
@@ -24,6 +23,8 @@ import sleekxmpp
|
||||
if sys.version_info < (3, 0):
|
||||
reload(sys)
|
||||
sys.setdefaultencoding('utf8')
|
||||
else:
|
||||
raw_input = input
|
||||
|
||||
|
||||
class EchoBot(sleekxmpp.ClientXMPP):
|
||||
@@ -39,7 +40,7 @@ class EchoBot(sleekxmpp.ClientXMPP):
|
||||
# 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
|
||||
# listen for this event so that we we can initialize
|
||||
# our roster.
|
||||
self.add_event_handler("session_start", self.start)
|
||||
|
||||
@@ -53,7 +54,7 @@ class EchoBot(sleekxmpp.ClientXMPP):
|
||||
Process the session_start event.
|
||||
|
||||
Typical actions for the session_start event are
|
||||
requesting the roster and broadcasting an intial
|
||||
requesting the roster and broadcasting an initial
|
||||
presence stanza.
|
||||
|
||||
Arguments:
|
||||
@@ -154,14 +155,14 @@ if __name__ == '__main__':
|
||||
|
||||
# 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
|
||||
# If you do not have the dnspython 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)
|
||||
xmpp.process(block=True)
|
||||
print("Done")
|
||||
else:
|
||||
print("Unable to connect.")
|
||||
|
||||
172
examples/roster_browser.py
Normal file
172
examples/roster_browser.py
Normal file
@@ -0,0 +1,172 @@
|
||||
#!/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 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 initialize
|
||||
# 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 initial
|
||||
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 dnspython 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(block=True)
|
||||
else:
|
||||
print("Unable to connect.")
|
||||
|
||||
143
examples/send_client.py
Executable file
143
examples/send_client.py
Executable file
@@ -0,0 +1,143 @@
|
||||
#!/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 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 initialize
|
||||
# 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 initial
|
||||
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 dnspython 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(block=True)
|
||||
print("Done")
|
||||
else:
|
||||
print("Unable to connect.")
|
||||
192
setup.py
Normal file → Executable file
192
setup.py
Normal file → Executable file
@@ -1,91 +1,101 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2007-2008 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).'
|
||||
LONG_DESCRIPTION = """
|
||||
SleekXMPP is an elegant Python library for XMPP (aka Jabber, Google Talk, etc).
|
||||
"""
|
||||
|
||||
CLASSIFIERS = [ 'Intended Audience :: Developers',
|
||||
'License :: OSI Approved :: MIT',
|
||||
'Programming Language :: Python',
|
||||
'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://code.google.com/p/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.rst and LICENSE
|
||||
# file, which you should have received as part of this distribution.
|
||||
|
||||
import sys
|
||||
try:
|
||||
from setuptools import setup, Command
|
||||
except ImportError:
|
||||
from distutils.core import setup, Command
|
||||
# from ez_setup import use_setuptools
|
||||
|
||||
from testall import TestCommand
|
||||
from sleekxmpp.version import __version__
|
||||
# if 'cygwin' in sys.platform.lower():
|
||||
# min_version = '0.6c6'
|
||||
# else:
|
||||
# min_version = '0.6a9'
|
||||
#
|
||||
# try:
|
||||
# use_setuptools(min_version=min_version)
|
||||
# except TypeError:
|
||||
# # locally installed ez_setup won't have min_version
|
||||
# use_setuptools()
|
||||
#
|
||||
# from setuptools import setup, find_packages, Extension, Feature
|
||||
|
||||
VERSION = __version__
|
||||
DESCRIPTION = 'SleekXMPP is an elegant Python library for XMPP (aka Jabber, Google Talk, etc).'
|
||||
with open('README.rst') as readme:
|
||||
LONG_DESCRIPTION = ''.join(readme)
|
||||
|
||||
CLASSIFIERS = [ 'Intended Audience :: Developers',
|
||||
'License :: OSI Approved :: MIT License',
|
||||
'Programming Language :: Python',
|
||||
'Programming Language :: Python :: 2.6',
|
||||
'Programming Language :: Python :: 2.7',
|
||||
'Programming Language :: Python :: 3.1',
|
||||
'Programming Language :: Python :: 3.2',
|
||||
'Topic :: Software Development :: Libraries :: Python Modules',
|
||||
]
|
||||
|
||||
packages = [ 'sleekxmpp',
|
||||
'sleekxmpp/stanza',
|
||||
'sleekxmpp/test',
|
||||
'sleekxmpp/roster',
|
||||
'sleekxmpp/xmlstream',
|
||||
'sleekxmpp/xmlstream/matcher',
|
||||
'sleekxmpp/xmlstream/handler',
|
||||
'sleekxmpp/plugins',
|
||||
'sleekxmpp/plugins/xep_0004',
|
||||
'sleekxmpp/plugins/xep_0004/stanza',
|
||||
'sleekxmpp/plugins/xep_0009',
|
||||
'sleekxmpp/plugins/xep_0009/stanza',
|
||||
'sleekxmpp/plugins/xep_0030',
|
||||
'sleekxmpp/plugins/xep_0030/stanza',
|
||||
'sleekxmpp/plugins/xep_0050',
|
||||
'sleekxmpp/plugins/xep_0059',
|
||||
'sleekxmpp/plugins/xep_0060',
|
||||
'sleekxmpp/plugins/xep_0060/stanza',
|
||||
'sleekxmpp/plugins/xep_0066',
|
||||
'sleekxmpp/plugins/xep_0078',
|
||||
'sleekxmpp/plugins/xep_0085',
|
||||
'sleekxmpp/plugins/xep_0086',
|
||||
'sleekxmpp/plugins/xep_0092',
|
||||
'sleekxmpp/plugins/xep_0128',
|
||||
'sleekxmpp/plugins/xep_0199',
|
||||
'sleekxmpp/plugins/xep_0202',
|
||||
'sleekxmpp/plugins/xep_0203',
|
||||
'sleekxmpp/plugins/xep_0224',
|
||||
'sleekxmpp/plugins/xep_0249',
|
||||
'sleekxmpp/features',
|
||||
'sleekxmpp/features/feature_mechanisms',
|
||||
'sleekxmpp/features/feature_mechanisms/stanza',
|
||||
'sleekxmpp/features/feature_starttls',
|
||||
'sleekxmpp/features/feature_bind',
|
||||
'sleekxmpp/features/feature_session',
|
||||
'sleekxmpp/thirdparty',
|
||||
'sleekxmpp/thirdparty/suelta',
|
||||
'sleekxmpp/thirdparty/suelta/mechanisms',
|
||||
]
|
||||
|
||||
setup(
|
||||
name = "sleekxmpp",
|
||||
version = VERSION,
|
||||
description = DESCRIPTION,
|
||||
long_description = LONG_DESCRIPTION,
|
||||
author = 'Nathanael Fritz',
|
||||
author_email = 'fritzy [at] netflint.net',
|
||||
url = 'http://github.com/fritzy/SleekXMPP',
|
||||
license = 'MIT',
|
||||
platforms = [ 'any' ],
|
||||
packages = packages,
|
||||
requires = [ 'dnspython' ],
|
||||
classifiers = CLASSIFIERS,
|
||||
cmdclass = {'test': TestCommand}
|
||||
)
|
||||
|
||||
@@ -15,5 +15,4 @@ from sleekxmpp.xmlstream import XMLStream, RestartStream
|
||||
from sleekxmpp.xmlstream.matcher import *
|
||||
from sleekxmpp.xmlstream.stanzabase import StanzaBase, ET
|
||||
|
||||
__version__ = '1.0beta6'
|
||||
__version_info__ = (1, 0, 0, 'beta6', 0)
|
||||
from sleekxmpp.version import __version__, __version_info__
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
SleekXMPP: The Sleek XMPP Library
|
||||
Copyright (C) 2010 Nathanael C. Fritz
|
||||
This file is part of SleekXMPP.
|
||||
sleekxmpp.basexmpp
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
See the file LICENSE for copying permission.
|
||||
This module provides the common XMPP functionality
|
||||
for both clients and components.
|
||||
|
||||
Part of SleekXMPP: The Sleek XMPP Library
|
||||
|
||||
:copyright: (c) 2011 Nathanael C. Fritz
|
||||
:license: MIT, see LICENSE for more details
|
||||
"""
|
||||
|
||||
from __future__ import with_statement, unicode_literals
|
||||
@@ -13,7 +19,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
|
||||
@@ -42,70 +49,59 @@ class BaseXMPP(XMLStream):
|
||||
with XMPP. It also provides a plugin mechanism to easily extend
|
||||
and add support for new XMPP features.
|
||||
|
||||
Attributes:
|
||||
auto_authorize -- Manage automatically accepting roster
|
||||
subscriptions.
|
||||
auto_subscribe -- Manage automatically requesting mutual
|
||||
subscriptions.
|
||||
is_component -- Indicates if this stream is for an XMPP component.
|
||||
jid -- The XMPP JID for this stream.
|
||||
plugin -- A dictionary of loaded plugins.
|
||||
plugin_config -- A dictionary of plugin configurations.
|
||||
plugin_whitelist -- A list of approved plugins.
|
||||
sentpresence -- Indicates if an initial presence has been sent.
|
||||
roster -- A dictionary containing subscribed JIDs and
|
||||
their presence statuses.
|
||||
|
||||
Methods:
|
||||
Iq -- Factory for creating an Iq stanzas.
|
||||
Message -- Factory for creating Message stanzas.
|
||||
Presence -- Factory for creating Presence stanzas.
|
||||
get -- Return a plugin given its name.
|
||||
make_iq -- Create and initialize an Iq stanza.
|
||||
make_iq_error -- Create an Iq stanza of type 'error'.
|
||||
make_iq_get -- Create an Iq stanza of type 'get'.
|
||||
make_iq_query -- Create an Iq stanza with a given query.
|
||||
make_iq_result -- Create an Iq stanza of type 'result'.
|
||||
make_iq_set -- Create an Iq stanza of type 'set'.
|
||||
make_message -- Create and initialize a Message stanza.
|
||||
make_presence -- Create and initialize a Presence stanza.
|
||||
make_query_roster -- Create a roster query.
|
||||
process -- Overrides XMLStream.process.
|
||||
register_plugin -- Load and configure a plugin.
|
||||
register_plugins -- Load and configure multiple plugins.
|
||||
send_message -- Create and send a Message stanza.
|
||||
send_presence -- Create and send a Presence stanza.
|
||||
send_presence_subscribe -- Send a subscription request.
|
||||
:param default_ns: Ensure that the correct default XML namespace
|
||||
is used during initialization.
|
||||
"""
|
||||
|
||||
def __init__(self, default_ns='jabber:client'):
|
||||
"""
|
||||
Adapt an XML stream for use with XMPP.
|
||||
|
||||
Arguments:
|
||||
default_ns -- Ensure that the correct default XML namespace
|
||||
is used during initialization.
|
||||
"""
|
||||
def __init__(self, jid='', default_ns='jabber:client'):
|
||||
XMLStream.__init__(self)
|
||||
|
||||
# To comply with PEP8, method names now use underscores.
|
||||
# Deprecated method names are re-mapped for backwards compatibility.
|
||||
self.default_ns = default_ns
|
||||
self.stream_ns = 'http://etherx.jabber.org/streams'
|
||||
self.namespace_map[self.stream_ns] = 'stream'
|
||||
|
||||
self.boundjid = JID("")
|
||||
#: An identifier for the stream as given by the server.
|
||||
self.stream_id = None
|
||||
|
||||
#: The JabberID (JID) used by this connection.
|
||||
self.boundjid = JID(jid)
|
||||
|
||||
#: A dictionary mapping plugin names to plugins.
|
||||
self.plugin = {}
|
||||
self.plugin_config = {}
|
||||
self.plugin_whitelist = []
|
||||
self.roster = {}
|
||||
self.is_component = False
|
||||
self.auto_authorize = True
|
||||
self.auto_subscribe = True
|
||||
|
||||
#: Configuration options for whitelisted plugins.
|
||||
#: If a plugin is registered without any configuration,
|
||||
#: and there is an entry here, it will be used.
|
||||
self.plugin_config = {}
|
||||
|
||||
#: A list of plugins that will be loaded if
|
||||
#: :meth:`register_plugins` is called.
|
||||
self.plugin_whitelist = []
|
||||
|
||||
#: The main roster object. This roster supports multiple
|
||||
#: owner JIDs, as in the case for components. For clients
|
||||
#: which only have a single JID, see :attr:`client_roster`.
|
||||
self.roster = roster.Roster(self)
|
||||
self.roster.add(self.boundjid.bare)
|
||||
|
||||
#: The single roster for the bound JID. This is the
|
||||
#: equivalent of::
|
||||
#:
|
||||
#: self.roster[self.boundjid.bare]
|
||||
self.client_roster = self.roster[self.boundjid.bare]
|
||||
|
||||
#: The distinction between clients and components can be
|
||||
#: important, primarily for choosing how to handle the
|
||||
#: ``'to'`` and ``'from'`` JIDs of stanzas.
|
||||
self.is_component = False
|
||||
|
||||
#: Flag indicating that the initial presence broadcast has
|
||||
#: been sent. Until this happens, some servers may not
|
||||
#: behave as expected when sending stanzas.
|
||||
self.sentpresence = False
|
||||
|
||||
#: A reference to :mod:`sleekxmpp.stanza` to make accessing
|
||||
#: stanza classes easier.
|
||||
self.stanza = sleekxmpp.stanza
|
||||
|
||||
self.register_handler(
|
||||
@@ -122,10 +118,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,30 +154,37 @@ class BaseXMPP(XMLStream):
|
||||
register_stanza_plugin(Message, Nick)
|
||||
register_stanza_plugin(Message, HTMLIM)
|
||||
|
||||
def process(self, *args, **kwargs):
|
||||
"""
|
||||
Overrides XMLStream.process.
|
||||
def start_stream_handler(self, xml):
|
||||
"""Save the stream ID once the streams have been established.
|
||||
|
||||
Initialize the XML streams and begin processing events.
|
||||
:param xml: The incoming stream's root element.
|
||||
"""
|
||||
self.stream_id = xml.get('id', '')
|
||||
|
||||
def process(self, *args, **kwargs):
|
||||
"""Initialize plugins and begin processing the XML stream.
|
||||
|
||||
The number of threads used for processing stream events is determined
|
||||
by HANDLER_THREADS.
|
||||
by :data:`HANDLER_THREADS`.
|
||||
|
||||
Arguments:
|
||||
block -- If block=False then event dispatcher will run
|
||||
in a separate thread, allowing for the stream to be
|
||||
used in the background for another application.
|
||||
Otherwise, process(block=True) blocks the current thread.
|
||||
Defaults to False.
|
||||
:param bool block: If ``False``, then event dispatcher will run
|
||||
in a separate thread, allowing for the stream to be
|
||||
used in the background for another application.
|
||||
Otherwise, ``process(block=True)`` blocks the current
|
||||
thread. Defaults to ``False``.
|
||||
:param bool threaded: **DEPRECATED**
|
||||
If ``True``, then event dispatcher will run
|
||||
in a separate thread, allowing for the stream to be
|
||||
used in the background for another application.
|
||||
Defaults to ``True``. This does **not** mean that no
|
||||
threads are used at all if ``threaded=False``.
|
||||
|
||||
**threaded is deprecated and included for API compatibility**
|
||||
threaded -- If threaded=True then event dispatcher will run
|
||||
in a separate thread, allowing for the stream to be
|
||||
used in the background for another application.
|
||||
Defaults to True.
|
||||
Regardless of these threading options, these threads will
|
||||
always exist:
|
||||
|
||||
Event handlers and the send queue will be threaded
|
||||
regardless of these parameters.
|
||||
- The event queue processor
|
||||
- The send queue processor
|
||||
- The scheduler
|
||||
"""
|
||||
for name in self.plugin:
|
||||
if not self.plugin[name].post_inited:
|
||||
@@ -169,15 +192,13 @@ class BaseXMPP(XMLStream):
|
||||
return XMLStream.process(self, *args, **kwargs)
|
||||
|
||||
def register_plugin(self, plugin, pconfig={}, module=None):
|
||||
"""
|
||||
Register and configure a plugin for use in this stream.
|
||||
"""Register and configure a plugin for use in this stream.
|
||||
|
||||
Arguments:
|
||||
plugin -- The name of the plugin class. Plugin names must
|
||||
:param plugin: The name of the plugin class. Plugin names must
|
||||
be unique.
|
||||
pconfig -- A dictionary of configuration data for the plugin.
|
||||
Defaults to an empty dictionary.
|
||||
module -- Optional refence to the module containing the plugin
|
||||
:param pconfig: A dictionary of configuration data for the plugin.
|
||||
Defaults to an empty dictionary.
|
||||
:param module: Optional refence to the module containing the plugin
|
||||
class if using custom plugins.
|
||||
"""
|
||||
try:
|
||||
@@ -198,29 +219,32 @@ 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)
|
||||
|
||||
# Let XEP/RFC implementing plugins have some extra logging info.
|
||||
spec = '(CUSTOM) '
|
||||
spec = '(CUSTOM) %s'
|
||||
if self.plugin[plugin].xep:
|
||||
spec = "(XEP-%s) " % self.plugin[plugin].xep
|
||||
elif self.plugin[plugin].rfc:
|
||||
spec = "(RFC-%s) " % self.plugin[plugin].rfc
|
||||
|
||||
desc = (spec, self.plugin[plugin].description)
|
||||
log.debug("Loaded Plugin %s%s" % desc)
|
||||
log.debug("Loaded Plugin %s %s" % desc)
|
||||
except:
|
||||
log.exception("Unable to load plugin: %s", plugin)
|
||||
|
||||
def register_plugins(self):
|
||||
"""
|
||||
Register and initialize all built-in plugins.
|
||||
"""Register and initialize all built-in plugins.
|
||||
|
||||
Optionally, the list of plugins loaded may be limited to those
|
||||
contained in self.plugin_whitelist.
|
||||
contained in :attr:`plugin_whitelist`.
|
||||
|
||||
Plugin configurations stored in self.plugin_config will be used.
|
||||
Plugin configurations stored in :attr:`plugin_config` will be used.
|
||||
"""
|
||||
if self.plugin_whitelist:
|
||||
plugin_list = self.plugin_whitelist
|
||||
@@ -239,19 +263,15 @@ class BaseXMPP(XMLStream):
|
||||
self.plugin[plugin].post_init()
|
||||
|
||||
def __getitem__(self, key):
|
||||
"""
|
||||
Return a plugin given its name, if it has been registered.
|
||||
"""
|
||||
"""Return a plugin given its name, if it has been registered."""
|
||||
if key in self.plugin:
|
||||
return self.plugin[key]
|
||||
else:
|
||||
log.warning("""Plugin "%s" is not loaded.""" % key)
|
||||
log.warning("Plugin '%s' is not loaded.", key)
|
||||
return False
|
||||
|
||||
def get(self, key, default):
|
||||
"""
|
||||
Return a plugin given its name, if it has been registered.
|
||||
"""
|
||||
"""Return a plugin given its name, if it has been registered."""
|
||||
return self.plugin.get(key, default)
|
||||
|
||||
def Message(self, *args, **kwargs):
|
||||
@@ -267,16 +287,18 @@ class BaseXMPP(XMLStream):
|
||||
return Presence(self, *args, **kwargs)
|
||||
|
||||
def make_iq(self, id=0, ifrom=None, ito=None, itype=None, iquery=None):
|
||||
"""
|
||||
Create a new Iq stanza with a given Id and from JID.
|
||||
"""Create a new Iq stanza with a given Id and from JID.
|
||||
|
||||
Arguments:
|
||||
id -- An ideally unique ID value for this stanza thread.
|
||||
Defaults to 0.
|
||||
ifrom -- The from JID to use for this stanza.
|
||||
ito -- The destination JID for this stanza.
|
||||
itype -- The Iq's type, one of: get, set, result, or error.
|
||||
iquery -- Optional namespace for adding a query element.
|
||||
:param id: An ideally unique ID value for this stanza thread.
|
||||
Defaults to 0.
|
||||
:param ifrom: The from :class:`~sleekxmpp.xmlstream.jid.JID`
|
||||
to use for this stanza.
|
||||
:param ito: The destination :class:`~sleekxmpp.xmlstream.jid.JID`
|
||||
for this stanza.
|
||||
:param itype: The :class:`~sleekxmpp.stanza.iq.Iq`'s type,
|
||||
one of: ``'get'``, ``'set'``, ``'result'``,
|
||||
or ``'error'``.
|
||||
:param iquery: Optional namespace for adding a query element.
|
||||
"""
|
||||
iq = self.Iq()
|
||||
iq['id'] = str(id)
|
||||
@@ -287,17 +309,17 @@ class BaseXMPP(XMLStream):
|
||||
return iq
|
||||
|
||||
def make_iq_get(self, queryxmlns=None, ito=None, ifrom=None, iq=None):
|
||||
"""
|
||||
Create an Iq stanza of type 'get'.
|
||||
"""Create an :class:`~sleekxmpp.stanza.iq.Iq` stanza of type ``'get'``.
|
||||
|
||||
Optionally, a query element may be added.
|
||||
|
||||
Arguments:
|
||||
queryxmlns -- The namespace of the query to use.
|
||||
ito -- The destination JID for this stanza.
|
||||
ifrom -- The from JID to use for this stanza.
|
||||
iq -- Optionally use an existing stanza instead
|
||||
of generating a new one.
|
||||
:param queryxmlns: The namespace of the query to use.
|
||||
:param ito: The destination :class:`~sleekxmpp.xmlstream.jid.JID`
|
||||
for this stanza.
|
||||
:param ifrom: The ``'from'`` :class:`~sleekxmpp.xmlstream.jid.JID`
|
||||
to use for this stanza.
|
||||
:param iq: Optionally use an existing stanza instead
|
||||
of generating a new one.
|
||||
"""
|
||||
if not iq:
|
||||
iq = self.Iq()
|
||||
@@ -311,14 +333,16 @@ class BaseXMPP(XMLStream):
|
||||
|
||||
def make_iq_result(self, id=None, ito=None, ifrom=None, iq=None):
|
||||
"""
|
||||
Create an Iq stanza of type 'result' with the given ID value.
|
||||
Create an :class:`~sleekxmpp.stanza.iq.Iq` stanza of type
|
||||
``'result'`` with the given ID value.
|
||||
|
||||
Arguments:
|
||||
id -- An ideally unique ID value. May use self.new_id().
|
||||
ito -- The destination JID for this stanza.
|
||||
ifrom -- The from JID to use for this stanza.
|
||||
iq -- Optionally use an existing stanza instead
|
||||
of generating a new one.
|
||||
:param id: An ideally unique ID value. May use :meth:`new_id()`.
|
||||
:param ito: The destination :class:`~sleekxmpp.xmlstream.jid.JID`
|
||||
for this stanza.
|
||||
:param ifrom: The ``'from'`` :class:`~sleekxmpp.xmlstream.jid.JID`
|
||||
to use for this stanza.
|
||||
:param iq: Optionally use an existing stanza instead
|
||||
of generating a new one.
|
||||
"""
|
||||
if not iq:
|
||||
iq = self.Iq()
|
||||
@@ -334,17 +358,22 @@ class BaseXMPP(XMLStream):
|
||||
|
||||
def make_iq_set(self, sub=None, ito=None, ifrom=None, iq=None):
|
||||
"""
|
||||
Create an Iq stanza of type 'set'.
|
||||
Create an :class:`~sleekxmpp.stanza.iq.Iq` stanza of type ``'set'``.
|
||||
|
||||
Optionally, a substanza may be given to use as the
|
||||
stanza's payload.
|
||||
|
||||
Arguments:
|
||||
sub -- A stanza or XML object to use as the Iq's payload.
|
||||
ito -- The destination JID for this stanza.
|
||||
ifrom -- The from JID to use for this stanza.
|
||||
iq -- Optionally use an existing stanza instead
|
||||
of generating a new one.
|
||||
:param sub: Either an
|
||||
:class:`~sleekxmpp.xmlstream.stanzabase.ElementBase`
|
||||
stanza object or an
|
||||
:class:`~xml.etree.ElementTree.Element` XML object
|
||||
to use as the :class:`~sleekxmpp.stanza.iq.Iq`'s payload.
|
||||
:param ito: The destination :class:`~sleekxmpp.xmlstream.jid.JID`
|
||||
for this stanza.
|
||||
:param ifrom: The ``'from'`` :class:`~sleekxmpp.xmlstream.jid.JID`
|
||||
to use for this stanza.
|
||||
:param iq: Optionally use an existing stanza instead
|
||||
of generating a new one.
|
||||
"""
|
||||
if not iq:
|
||||
iq = self.Iq()
|
||||
@@ -361,19 +390,20 @@ class BaseXMPP(XMLStream):
|
||||
condition='feature-not-implemented',
|
||||
text=None, ito=None, ifrom=None, iq=None):
|
||||
"""
|
||||
Create an Iq stanza of type 'error'.
|
||||
Create an :class:`~sleekxmpp.stanza.iq.Iq` stanza of type ``'error'``.
|
||||
|
||||
Arguments:
|
||||
id -- An ideally unique ID value. May use self.new_id().
|
||||
type -- The type of the error, such as 'cancel' or 'modify'.
|
||||
Defaults to 'cancel'.
|
||||
condition -- The error condition.
|
||||
Defaults to 'feature-not-implemented'.
|
||||
text -- A message describing the cause of the error.
|
||||
ito -- The destination JID for this stanza.
|
||||
ifrom -- The from JID to use for this stanza.
|
||||
iq -- Optionally use an existing stanza instead
|
||||
of generating a new one.
|
||||
:param id: An ideally unique ID value. May use :meth:`new_id()`.
|
||||
:param type: The type of the error, such as ``'cancel'`` or
|
||||
``'modify'``. Defaults to ``'cancel'``.
|
||||
:param condition: The error condition. Defaults to
|
||||
``'feature-not-implemented'``.
|
||||
:param text: A message describing the cause of the error.
|
||||
:param ito: The destination :class:`~sleekxmpp.xmlstream.jid.JID`
|
||||
for this stanza.
|
||||
:param ifrom: The ``'from'`` :class:`~sleekxmpp.xmlstream.jid.JID`
|
||||
to use for this stanza.
|
||||
:param iq: Optionally use an existing stanza instead
|
||||
of generating a new one.
|
||||
"""
|
||||
if not iq:
|
||||
iq = self.Iq()
|
||||
@@ -389,15 +419,16 @@ class BaseXMPP(XMLStream):
|
||||
|
||||
def make_iq_query(self, iq=None, xmlns='', ito=None, ifrom=None):
|
||||
"""
|
||||
Create or modify an Iq stanza to use the given
|
||||
query namespace.
|
||||
Create or modify an :class:`~sleekxmpp.stanza.iq.Iq` stanza
|
||||
to use the given query namespace.
|
||||
|
||||
Arguments:
|
||||
iq -- Optional Iq stanza to modify. A new
|
||||
stanza is created otherwise.
|
||||
xmlns -- The query's namespace.
|
||||
ito -- The destination JID for this stanza.
|
||||
ifrom -- The from JID to use for this stanza.
|
||||
:param iq: Optionally use an existing stanza instead
|
||||
of generating a new one.
|
||||
:param xmlns: The query's namespace.
|
||||
:param ito: The destination :class:`~sleekxmpp.xmlstream.jid.JID`
|
||||
for this stanza.
|
||||
:param ifrom: The ``'from'`` :class:`~sleekxmpp.xmlstream.jid.JID`
|
||||
to use for this stanza.
|
||||
"""
|
||||
if not iq:
|
||||
iq = self.Iq()
|
||||
@@ -409,12 +440,10 @@ class BaseXMPP(XMLStream):
|
||||
return iq
|
||||
|
||||
def make_query_roster(self, iq=None):
|
||||
"""
|
||||
Create a roster query element.
|
||||
"""Create a roster query element.
|
||||
|
||||
Arguments:
|
||||
iq -- Optional Iq stanza to modify. A new stanza
|
||||
is created otherwise.
|
||||
:param iq: Optionally use an existing stanza instead
|
||||
of generating a new one.
|
||||
"""
|
||||
if iq:
|
||||
iq['query'] = 'jabber:iq:roster'
|
||||
@@ -423,18 +452,19 @@ class BaseXMPP(XMLStream):
|
||||
def make_message(self, mto, mbody=None, msubject=None, mtype=None,
|
||||
mhtml=None, mfrom=None, mnick=None):
|
||||
"""
|
||||
Create and initialize a new Message stanza.
|
||||
Create and initialize a new
|
||||
:class:`~sleekxmpp.stanza.message.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.
|
||||
:param mto: The recipient of the message.
|
||||
:param mbody: The main contents of the message.
|
||||
:param msubject: Optional subject for the message.
|
||||
:param mtype: The message's type, such as ``'chat'`` or
|
||||
``'groupchat'``.
|
||||
:param mhtml: Optional HTML body content in the form of a string.
|
||||
:param mfrom: The sender of the message. if sending from a client,
|
||||
be aware that some servers require that the full JID
|
||||
of the sender be used.
|
||||
:param mnick: Optional nickname of the sender.
|
||||
"""
|
||||
message = self.Message(sto=mto, stype=mtype, sfrom=mfrom)
|
||||
message['body'] = mbody
|
||||
@@ -446,67 +476,87 @@ 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.
|
||||
Create and initialize a new
|
||||
:class:`~sleekxmpp.stanza.presence.Presence` stanza.
|
||||
|
||||
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.
|
||||
:param pshow: The presence's show value.
|
||||
:param pstatus: The presence's status message.
|
||||
:param ppriority: This connection's priority.
|
||||
:param pto: The recipient of a directed presence.
|
||||
:param ptype: The type of presence, such as ``'subscribe'``.
|
||||
:param pfrom: The sender of the presence.
|
||||
:param pnick: Optional nickname of the presence's sender.
|
||||
"""
|
||||
presence = self.Presence(stype=ptype, sfrom=pfrom, sto=pto)
|
||||
if pshow is not None:
|
||||
presence['type'] = pshow
|
||||
if pfrom is None:
|
||||
if pfrom is None and self.is_component:
|
||||
presence['from'] = self.boundjid.full
|
||||
presence['priority'] = ppriority
|
||||
presence['status'] = pstatus
|
||||
presence['nick'] = pnick
|
||||
return presence
|
||||
|
||||
def send_message(self, mto, mbody, msubject=None, mtype=None,
|
||||
mhtml=None, mfrom=None, mnick=None):
|
||||
"""
|
||||
Create, initialize, and send a Message stanza.
|
||||
|
||||
Create, initialize, and send a new
|
||||
:class:`~sleekxmpp.stanza.message.Message` stanza.
|
||||
|
||||
:param mto: The recipient of the message.
|
||||
:param mbody: The main contents of the message.
|
||||
:param msubject: Optional subject for the message.
|
||||
:param mtype: The message's type, such as ``'chat'`` or
|
||||
``'groupchat'``.
|
||||
:param mhtml: Optional HTML body content in the form of a string.
|
||||
:param mfrom: The sender of the message. if sending from a client,
|
||||
be aware that some servers require that the full JID
|
||||
of the sender be used.
|
||||
:param mnick: Optional nickname of the sender.
|
||||
"""
|
||||
self.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.
|
||||
Create, initialize, and send a new
|
||||
:class:`~sleekxmpp.stanza.presence.Presence` stanza.
|
||||
|
||||
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.
|
||||
:param pshow: The presence's show value.
|
||||
:param pstatus: The presence's status message.
|
||||
:param ppriority: This connection's priority.
|
||||
:param pto: The recipient of a directed presence.
|
||||
:param ptype: The type of presence, such as ``'subscribe'``.
|
||||
:param pfrom: The sender of the presence.
|
||||
:param pnick: Optional nickname of the presence's sender.
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
Create, initialize, and send a Presence stanza of type 'subscribe'.
|
||||
Create, initialize, and send a new
|
||||
:class:`~sleekxmpp.stanza.presence.Presence` stanza of
|
||||
type ``'subscribe'``.
|
||||
|
||||
Arguments:
|
||||
pto -- The recipient of a directed presence.
|
||||
pfrom -- The sender of the presence.
|
||||
ptype -- The type of presence. Defaults to 'subscribe'.
|
||||
pnick -- Nickname of the presence's sender.
|
||||
:param pto: The recipient of a directed presence.
|
||||
:param pfrom: The sender of the presence.
|
||||
:param ptype: The type of presence, such as ``'subscribe'``.
|
||||
:param pnick: Optional nickname of the presence's sender.
|
||||
"""
|
||||
presence = self.makePresence(ptype=ptype,
|
||||
pfrom=pfrom,
|
||||
@@ -519,9 +569,7 @@ class BaseXMPP(XMLStream):
|
||||
|
||||
@property
|
||||
def jid(self):
|
||||
"""
|
||||
Attribute accessor for bare jid
|
||||
"""
|
||||
"""Attribute accessor for bare jid"""
|
||||
log.warning("jid property deprecated. Use boundjid.bare")
|
||||
return self.boundjid.bare
|
||||
|
||||
@@ -532,9 +580,7 @@ class BaseXMPP(XMLStream):
|
||||
|
||||
@property
|
||||
def fulljid(self):
|
||||
"""
|
||||
Attribute accessor for full jid
|
||||
"""
|
||||
"""Attribute accessor for full jid"""
|
||||
log.warning("fulljid property deprecated. Use boundjid.full")
|
||||
return self.boundjid.full
|
||||
|
||||
@@ -545,9 +591,7 @@ class BaseXMPP(XMLStream):
|
||||
|
||||
@property
|
||||
def resource(self):
|
||||
"""
|
||||
Attribute accessor for jid resource
|
||||
"""
|
||||
"""Attribute accessor for jid resource"""
|
||||
log.warning("resource property deprecated. Use boundjid.resource")
|
||||
return self.boundjid.resource
|
||||
|
||||
@@ -558,9 +602,7 @@ class BaseXMPP(XMLStream):
|
||||
|
||||
@property
|
||||
def username(self):
|
||||
"""
|
||||
Attribute accessor for jid usernode
|
||||
"""
|
||||
"""Attribute accessor for jid usernode"""
|
||||
log.warning("username property deprecated. Use boundjid.user")
|
||||
return self.boundjid.user
|
||||
|
||||
@@ -571,9 +613,7 @@ class BaseXMPP(XMLStream):
|
||||
|
||||
@property
|
||||
def server(self):
|
||||
"""
|
||||
Attribute accessor for jid host
|
||||
"""
|
||||
"""Attribute accessor for jid host"""
|
||||
log.warning("server property deprecated. Use boundjid.host")
|
||||
return self.boundjid.server
|
||||
|
||||
@@ -582,9 +622,35 @@ class BaseXMPP(XMLStream):
|
||||
log.warning("server property deprecated. Use boundjid.host")
|
||||
self.boundjid.server = value
|
||||
|
||||
@property
|
||||
def auto_authorize(self):
|
||||
"""Auto accept or deny subscription requests.
|
||||
|
||||
If ``True``, auto accept subscription requests.
|
||||
If ``False``, auto deny subscription requests.
|
||||
If ``None``, don't automatically respond.
|
||||
"""
|
||||
return self.roster.auto_authorize
|
||||
|
||||
@auto_authorize.setter
|
||||
def auto_authorize(self, value):
|
||||
self.roster.auto_authorize = value
|
||||
|
||||
@property
|
||||
def auto_subscribe(self):
|
||||
"""Auto send requests for mutual subscriptions.
|
||||
|
||||
If ``True``, auto send mutual subscription requests.
|
||||
"""
|
||||
return self.roster.auto_subscribe
|
||||
|
||||
@auto_subscribe.setter
|
||||
def auto_subscribe(self, value):
|
||||
self.roster.auto_subscribe = value
|
||||
|
||||
def set_jid(self, jid):
|
||||
"""Rip a JID apart and claim it as our own."""
|
||||
log.debug("setting jid to %s" % jid)
|
||||
log.debug("setting jid to %s", jid)
|
||||
self.boundjid.full = jid
|
||||
|
||||
def getjidresource(self, fulljid):
|
||||
@@ -598,7 +664,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,9 +673,66 @@ class BaseXMPP(XMLStream):
|
||||
"""Process incoming message stanzas."""
|
||||
self.event('message', msg)
|
||||
|
||||
def _handle_presence(self, presence):
|
||||
def _handle_available(self, presence):
|
||||
pto = presence['to'].bare
|
||||
pfrom = presence['from'].bare
|
||||
self.roster[pto][pfrom].handle_available(presence)
|
||||
|
||||
def _handle_unavailable(self, presence):
|
||||
pto = presence['to'].bare
|
||||
pfrom = presence['from'].bare
|
||||
self.roster[pto][pfrom].handle_unavailable(presence)
|
||||
|
||||
def _handle_new_subscription(self, stanza):
|
||||
"""Attempt to automatically handle subscription requests.
|
||||
|
||||
Subscriptions will be approved if the request is from
|
||||
a whitelisted JID, of :attr:`auto_authorize` is True. They
|
||||
will be rejected if :attr:`auto_authorize` is False. Setting
|
||||
:attr:`auto_authorize` to ``None`` will disable automatic
|
||||
subscription handling (except for whitelisted JIDs).
|
||||
|
||||
If a subscription is accepted, a request for a mutual
|
||||
subscription will be sent if :attr:`auto_subscribe` is ``True``.
|
||||
"""
|
||||
Process incoming presence stanzas.
|
||||
roster = self.roster[stanza['to'].bare]
|
||||
item = self.roster[stanza['to'].bare][stanza['from'].bare]
|
||||
if item['whitelisted']:
|
||||
item.authorize()
|
||||
elif roster.auto_authorize:
|
||||
item.authorize()
|
||||
if roster.auto_subscribe:
|
||||
item.subscribe()
|
||||
elif roster.auto_authorize == False:
|
||||
item.unauthorize()
|
||||
|
||||
def _handle_removed_subscription(self, presence):
|
||||
pto = presence['to'].bare
|
||||
pfrom = presence['from'].bare
|
||||
self.roster[pto][pfrom].unauthorize()
|
||||
|
||||
def _handle_subscribe(self, presence):
|
||||
pto = presence['to'].bare
|
||||
pfrom = presence['from'].bare
|
||||
self.roster[pto][pfrom].handle_subscribe(presence)
|
||||
|
||||
def _handle_subscribed(self, presence):
|
||||
pto = presence['to'].bare
|
||||
pfrom = presence['from'].bare
|
||||
self.roster[pto][pfrom].handle_subscribed(presence)
|
||||
|
||||
def _handle_unsubscribe(self, presence):
|
||||
pto = presence['to'].bare
|
||||
pfrom = presence['from'].bare
|
||||
self.roster[pto][pfrom].handle_unsubscribe(presence)
|
||||
|
||||
def _handle_unsubscribed(self, presence):
|
||||
pto = presence['to'].bare
|
||||
pfrom = presence['from'].bare
|
||||
self.roster[pto][pfrom].handle_unsubscribed(presence)
|
||||
|
||||
def _handle_presence(self, presence):
|
||||
"""Process incoming presence stanzas.
|
||||
|
||||
Update the roster with presence information.
|
||||
"""
|
||||
@@ -624,97 +747,25 @@ 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']
|
||||
def exception(self, exception):
|
||||
"""Process any uncaught exceptions, notably
|
||||
:class:`~sleekxmpp.exceptions.IqError` and
|
||||
:class:`~sleekxmpp.exceptions.IqTimeout` exceptions.
|
||||
|
||||
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):
|
||||
:param exception: An unhandled :class:`Exception` object.
|
||||
"""
|
||||
Automatically managage subscription requests.
|
||||
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)
|
||||
|
||||
Subscription behavior is controlled by the settings
|
||||
self.auto_authorize and self.auto_subscribe.
|
||||
|
||||
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.
|
||||
"""
|
||||
presence.reply()
|
||||
presence['to'] = presence['to'].bare
|
||||
|
||||
# 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
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
SleekXMPP: The Sleek XMPP Library
|
||||
Copyright (C) 2010 Nathanael C. Fritz
|
||||
This file is part of SleekXMPP.
|
||||
sleekxmpp.clientxmpp
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
See the file LICENSE for copying permission.
|
||||
This module provides XMPP functionality that
|
||||
is specific to client connections.
|
||||
|
||||
Part of SleekXMPP: The Sleek XMPP Library
|
||||
|
||||
:copyright: (c) 2011 Nathanael C. Fritz
|
||||
:license: MIT, see LICENSE for more details
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
@@ -27,11 +33,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__)
|
||||
@@ -40,45 +47,38 @@ log = logging.getLogger(__name__)
|
||||
class ClientXMPP(BaseXMPP):
|
||||
|
||||
"""
|
||||
SleekXMPP's client class. ( Use only for good, not for evil.)
|
||||
SleekXMPP's client class. (Use only for good, not for evil.)
|
||||
|
||||
Typical Use:
|
||||
xmpp = ClientXMPP('user@server.tld/resource', 'password')
|
||||
xmpp.process(block=False) // when block is True, it blocks the current
|
||||
// thread. False by default.
|
||||
Typical use pattern:
|
||||
|
||||
Attributes:
|
||||
.. code-block:: python
|
||||
|
||||
Methods:
|
||||
connect -- Overrides XMLStream.connect.
|
||||
del_roster_item -- Delete a roster item.
|
||||
get_roster -- Retrieve the roster from the server.
|
||||
register_feature -- Register a stream feature.
|
||||
update_roster -- Update a roster item.
|
||||
xmpp = ClientXMPP('user@server.tld/resource', 'password')
|
||||
# ... Register plugins and event handlers ...
|
||||
xmpp.connect()
|
||||
xmpp.process(block=False) # block=True will block the current
|
||||
# thread. By default, block=False
|
||||
|
||||
:param jid: The JID of the XMPP user account.
|
||||
:param password: The password for the XMPP user account.
|
||||
:param ssl: **Deprecated.**
|
||||
:param plugin_config: A dictionary of plugin configurations.
|
||||
:param plugin_whitelist: A list of approved plugins that
|
||||
will be loaded when calling
|
||||
:meth:`~sleekxmpp.basexmpp.BaseXMPP.register_plugins()`.
|
||||
:param escape_quotes: **Deprecated.**
|
||||
"""
|
||||
|
||||
def __init__(self, jid, password, ssl=False, plugin_config={},
|
||||
plugin_whitelist=[], escape_quotes=True):
|
||||
"""
|
||||
Create a new SleekXMPP client.
|
||||
|
||||
Arguments:
|
||||
jid -- The JID of the XMPP user account.
|
||||
password -- The password for the XMPP user account.
|
||||
ssl -- Deprecated.
|
||||
plugin_config -- A dictionary of plugin configurations.
|
||||
plugin_whitelist -- A list of approved plugins that will be loaded
|
||||
when calling register_plugins.
|
||||
escape_quotes -- Deprecated.
|
||||
"""
|
||||
BaseXMPP.__init__(self, 'jabber:client')
|
||||
plugin_whitelist=[], escape_quotes=True, sasl_mech=None):
|
||||
BaseXMPP.__init__(self, jid, 'jabber:client')
|
||||
|
||||
self.set_jid(jid)
|
||||
self.password = password
|
||||
self.escape_quotes = escape_quotes
|
||||
self.plugin_config = plugin_config
|
||||
self.plugin_whitelist = plugin_whitelist
|
||||
self.srv_support = SRV_SUPPORT
|
||||
self.default_port = 5222
|
||||
|
||||
self.stream_header = "<stream:stream to='%s' %s %s version='1.0'>" % (
|
||||
self.boundjid.host,
|
||||
@@ -113,87 +113,73 @@ class ClientXMPP(BaseXMPP):
|
||||
|
||||
# Setup default stream features
|
||||
self.register_plugin('feature_starttls')
|
||||
self.register_plugin('feature_mechanisms')
|
||||
self.register_plugin('feature_bind')
|
||||
self.register_plugin('feature_session')
|
||||
self.register_plugin('feature_mechanisms',
|
||||
pconfig={'use_mech': sasl_mech} if sasl_mech else None)
|
||||
|
||||
def connect(self, address=tuple(), reattempt=True, use_tls=True):
|
||||
"""
|
||||
Connect to the XMPP server.
|
||||
def connect(self, address=tuple(), reattempt=True,
|
||||
use_tls=True, use_ssl=False):
|
||||
"""Connect to the XMPP server.
|
||||
|
||||
When no address is given, a SRV lookup for the server will
|
||||
be attempted. If that fails, the server user in the JID
|
||||
will be used.
|
||||
|
||||
Arguments:
|
||||
address -- A tuple containing the server's host and port.
|
||||
reattempt -- If True, reattempt the connection if an
|
||||
error occurs. Defaults to True.
|
||||
use_tls -- Indicates if TLS should be used for the
|
||||
connection. Defaults to True.
|
||||
:param address -- A tuple containing the server's host and port.
|
||||
:param reattempt: If ``True``, repeat attempting to connect if an
|
||||
error occurs. Defaults to ``True``.
|
||||
:param use_tls: Indicates if TLS should be used for the
|
||||
connection. Defaults to ``True``.
|
||||
:param use_ssl: Indicates if the older SSL connection method
|
||||
should be used. Defaults to ``False``.
|
||||
"""
|
||||
self.session_started_event.clear()
|
||||
if not address 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)
|
||||
use_tls=use_tls, use_ssl=use_ssl,
|
||||
reattempt=reattempt)
|
||||
|
||||
def get_dns_records(self, domain, port=None):
|
||||
"""Get the DNS records for a domain, including SRV records.
|
||||
|
||||
:param domain: The domain in question.
|
||||
:param port: If the results don't include a port, use this one.
|
||||
"""
|
||||
if port is None:
|
||||
port = self.default_port
|
||||
if DNSPYTHON:
|
||||
try:
|
||||
record = "_xmpp-client._tcp.%s" % domain
|
||||
answers = []
|
||||
for answer in dns.resolver.query(record, dns.rdatatype.SRV):
|
||||
address = (answer.target.to_text()[:-1], answer.port)
|
||||
answers.append((address, answer.priority, answer.weight))
|
||||
except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer):
|
||||
log.warning("No SRV records for %s", domain)
|
||||
answers = super(ClientXMPP, self).get_dns_records(domain, port)
|
||||
except dns.exception.Timeout:
|
||||
log.warning("DNS resolution timed out " + \
|
||||
"for SRV record of %s", domain)
|
||||
answers = super(ClientXMPP, self).get_dns_records(domain, port)
|
||||
return answers
|
||||
else:
|
||||
log.warning("dnspython is not installed -- " + \
|
||||
"relying on OS A record resolution")
|
||||
return [((domain, port), 0, 0)]
|
||||
|
||||
def register_feature(self, name, handler, restart=False, order=5000):
|
||||
"""
|
||||
Register a stream feature.
|
||||
"""Register a stream feature handler.
|
||||
|
||||
Arguments:
|
||||
name -- The name of the stream feature.
|
||||
handler -- The function to execute if the feature is received.
|
||||
restart -- Indicates if feature processing should halt with
|
||||
this feature. Defaults to False.
|
||||
order -- The relative ordering in which the feature should
|
||||
be negotiated. Lower values will be attempted
|
||||
earlier when available.
|
||||
:param name: The name of the stream feature.
|
||||
:param handler: The function to execute if the feature is received.
|
||||
:param restart: Indicates if feature processing should halt with
|
||||
this feature. Defaults to ``False``.
|
||||
:param order: The relative ordering in which the feature should
|
||||
be negotiated. Lower values will be attempted
|
||||
earlier when available.
|
||||
"""
|
||||
self._stream_feature_handlers[name] = (handler, restart)
|
||||
self._stream_feature_order.append((order, name))
|
||||
@@ -201,72 +187,58 @@ class ClientXMPP(BaseXMPP):
|
||||
|
||||
def update_roster(self, jid, name=None, subscription=None, groups=[],
|
||||
block=True, timeout=None, callback=None):
|
||||
"""
|
||||
Add or change a roster item.
|
||||
"""Add or change a roster item.
|
||||
|
||||
Arguments:
|
||||
jid -- The JID of the entry to modify.
|
||||
name -- The user's nickname for this JID.
|
||||
subscription -- The subscription status. May be one of
|
||||
'to', 'from', 'both', or 'none'. If set
|
||||
to 'remove', the entry will be deleted.
|
||||
groups -- The roster groups that contain this item.
|
||||
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.
|
||||
:param jid: The JID of the entry to modify.
|
||||
:param name: The user's nickname for this JID.
|
||||
:param subscription: The subscription status. May be one of
|
||||
``'to'``, ``'from'``, ``'both'``, or
|
||||
``'none'``. If set to ``'remove'``,
|
||||
the entry will be deleted.
|
||||
:param groups: The roster groups that contain this item.
|
||||
:param block: Specify if the roster request will block
|
||||
until a response is received, or a timeout
|
||||
occurs. Defaults to ``True``.
|
||||
:param timeout: The length of time (in seconds) to wait
|
||||
for a response before continuing if blocking
|
||||
is used. Defaults to
|
||||
:attr:`~sleekxmpp.xmlstream.xmlstream.XMLStream.response_timeout`.
|
||||
:param callback: Optional reference to a stream handler function.
|
||||
Will be executed when the roster is received.
|
||||
Implies ``block=False``.
|
||||
"""
|
||||
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.update(jid, name, subscription, groups,
|
||||
block, timeout, callback)
|
||||
|
||||
def del_roster_item(self, jid):
|
||||
"""
|
||||
Remove an item from the roster by setting its subscription
|
||||
status to 'remove'.
|
||||
"""Remove an item from the roster.
|
||||
|
||||
This is done by setting its subscription status to ``'remove'``.
|
||||
|
||||
Arguments:
|
||||
jid -- The JID of the item to remove.
|
||||
:param 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):
|
||||
"""
|
||||
Request the roster from the server.
|
||||
"""Request the roster from the server.
|
||||
|
||||
Arguments:
|
||||
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
|
||||
:param block: Specify if the roster request will block until a
|
||||
response is received, or a timeout occurs.
|
||||
Defaults to ``True``.
|
||||
:param timeout: The length of time (in seconds) to wait for a response
|
||||
before continuing if blocking is used.
|
||||
Defaults to self.response_timeout.
|
||||
callback -- Optional reference to a stream handler function. Will
|
||||
be executed when the roster is received.
|
||||
Implies block=False.
|
||||
Defaults to
|
||||
:attr:`~sleekxmpp.xmlstream.xmlstream.XMLStream.response_timeout`.
|
||||
:param callback: Optional reference to a stream handler function. Will
|
||||
be executed when the roster is received.
|
||||
Implies ``block=False``.
|
||||
"""
|
||||
iq = self.Iq()
|
||||
iq['type'] = 'get'
|
||||
iq.enable('roster')
|
||||
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):
|
||||
@@ -277,19 +249,10 @@ class ClientXMPP(BaseXMPP):
|
||||
self.bindfail = False
|
||||
self.features = set()
|
||||
|
||||
def session_timeout():
|
||||
if not self.session_started_event.isSet():
|
||||
log.debug("Session start has taken more than 15 seconds")
|
||||
self.disconnect(reconnect=self.auto_reconnect)
|
||||
|
||||
self.schedule("session timeout checker", 15, session_timeout)
|
||||
|
||||
def _handle_stream_features(self, features):
|
||||
"""
|
||||
Process the received stream features.
|
||||
"""Process the received stream features.
|
||||
|
||||
Arguments:
|
||||
features -- The features stanza.
|
||||
:param features: The features stanza.
|
||||
"""
|
||||
for order, name in self._stream_feature_order:
|
||||
if name in features['features']:
|
||||
@@ -300,23 +263,22 @@ class ClientXMPP(BaseXMPP):
|
||||
return True
|
||||
|
||||
def _handle_roster(self, iq, request=False):
|
||||
"""
|
||||
Update the roster after receiving a roster stanza.
|
||||
"""Update the roster after receiving a roster stanza.
|
||||
|
||||
Arguments:
|
||||
iq -- The roster stanza.
|
||||
request -- Indicates if this stanza is a response
|
||||
to a request for the roster.
|
||||
:param iq: The roster stanza.
|
||||
:param request: Indicates if this stanza is a response
|
||||
to a request for the roster, and not an
|
||||
empty acknowledgement from the server.
|
||||
"""
|
||||
if iq['type'] == 'set' or (iq['type'] == 'result' and request):
|
||||
for jid in iq['roster']['items']:
|
||||
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)
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
SleekXMPP: The Sleek XMPP Library
|
||||
Copyright (C) 2010 Nathanael C. Fritz
|
||||
This file is part of SleekXMPP.
|
||||
sleekxmpp.clientxmpp
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
See the file LICENSE for copying permission.
|
||||
This module provides XMPP functionality that
|
||||
is specific to external server component connections.
|
||||
|
||||
Part of SleekXMPP: The Sleek XMPP Library
|
||||
|
||||
:copyright: (c) 2011 Nathanael C. Fritz
|
||||
:license: MIT, see LICENSE for more details
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import
|
||||
@@ -32,33 +38,27 @@ class ComponentXMPP(BaseXMPP):
|
||||
|
||||
Use only for good, not for evil.
|
||||
|
||||
Methods:
|
||||
connect -- Overrides XMLStream.connect.
|
||||
incoming_filter -- Overrides XMLStream.incoming_filter.
|
||||
start_stream_handler -- Overrides XMLStream.start_stream_handler.
|
||||
:param jid: The JID of the component.
|
||||
:param secret: The secret or password for the component.
|
||||
:param host: The server accepting the component.
|
||||
:param port: The port used to connect to the server.
|
||||
:param plugin_config: A dictionary of plugin configurations.
|
||||
:param plugin_whitelist: A list of approved plugins that
|
||||
will be loaded when calling
|
||||
:meth:`~sleekxmpp.basexmpp.BaseXMPP.register_plugins()`.
|
||||
:param use_jc_ns: Indicates if the ``'jabber:client'`` namespace
|
||||
should be used instead of the standard
|
||||
``'jabber:component:accept'`` namespace.
|
||||
Defaults to ``False``.
|
||||
"""
|
||||
|
||||
def __init__(self, jid, secret, host, port,
|
||||
def __init__(self, jid, secret, host=None, port=None,
|
||||
plugin_config={}, plugin_whitelist=[], use_jc_ns=False):
|
||||
"""
|
||||
Arguments:
|
||||
jid -- The JID of the component.
|
||||
secret -- The secret or password for the component.
|
||||
host -- The server accepting the component.
|
||||
port -- The port used to connect to the server.
|
||||
plugin_config -- A dictionary of plugin configurations.
|
||||
plugin_whitelist -- A list of desired plugins to load
|
||||
when using register_plugins.
|
||||
use_js_ns -- Indicates if the 'jabber:client' namespace
|
||||
should be used instead of the standard
|
||||
'jabber:component:accept' namespace.
|
||||
Defaults to False.
|
||||
"""
|
||||
if use_jc_ns:
|
||||
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,27 +78,45 @@ 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):
|
||||
"""
|
||||
Connect to the server.
|
||||
def connect(self, host=None, port=None, use_ssl=False,
|
||||
use_tls=True, reattempt=True):
|
||||
"""Connect to the server.
|
||||
|
||||
Overrides XMLStream.connect.
|
||||
Setting ``reattempt`` to ``True`` will cause connection attempts to
|
||||
be made every second until a successful connection is established.
|
||||
|
||||
:param host: The name of the desired server for the connection.
|
||||
Defaults to :attr:`server_host`.
|
||||
:param port: Port to connect to on the server.
|
||||
Defauts to :attr:`server_port`.
|
||||
:param use_ssl: Flag indicating if SSL should be used by connecting
|
||||
directly to a port using SSL.
|
||||
:param use_tls: Flag indicating if TLS should be used, allowing for
|
||||
connecting to a port without using SSL immediately and
|
||||
later upgrading the connection.
|
||||
:param reattempt: Flag indicating if the socket should reconnect
|
||||
after disconnections.
|
||||
"""
|
||||
log.debug("Connecting to %s:%s" % (self.server_host,
|
||||
self.server_port))
|
||||
return XMLStream.connect(self, self.server_host,
|
||||
self.server_port)
|
||||
if host is None:
|
||||
host = self.server_host
|
||||
if port is None:
|
||||
port = self.server_port
|
||||
log.debug("Connecting to %s:%s", host, port)
|
||||
return XMLStream.connect(self, host=host, port=port,
|
||||
use_ssl=use_ssl,
|
||||
use_tls=use_tls,
|
||||
reattempt=reattempt)
|
||||
|
||||
def incoming_filter(self, xml):
|
||||
"""
|
||||
Pre-process incoming XML stanzas by converting any 'jabber:client'
|
||||
namespaced elements to the component's default namespace.
|
||||
Pre-process incoming XML stanzas by converting any
|
||||
``'jabber:client'`` namespaced elements to the component's
|
||||
default namespace.
|
||||
|
||||
Overrides XMLStream.incoming_filter.
|
||||
|
||||
Arguments:
|
||||
xml -- The XML stanza to pre-process.
|
||||
:param xml: The XML stanza to pre-process.
|
||||
"""
|
||||
if xml.tag.startswith('{jabber:client}'):
|
||||
xml.tag = xml.tag.replace('jabber:client', self.default_ns)
|
||||
@@ -115,11 +133,10 @@ 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.
|
||||
|
||||
Arguments:
|
||||
xml -- The incoming stream's root element.
|
||||
:param xml: The incoming stream's root element.
|
||||
"""
|
||||
BaseXMPP.start_stream_handler(self, xml)
|
||||
|
||||
# Construct a hash of the stream ID and the component secret.
|
||||
sid = xml.get('id', '')
|
||||
pre_hash = '%s%s' % (sid, self.secret)
|
||||
@@ -132,11 +149,14 @@ class ComponentXMPP(BaseXMPP):
|
||||
self.send_xml(handshake, now=True)
|
||||
|
||||
def _handle_handshake(self, xml):
|
||||
"""
|
||||
The handshake has been accepted.
|
||||
"""The handshake has been accepted.
|
||||
|
||||
Arguments:
|
||||
xml -- The reply handshake stanza.
|
||||
:param xml: The reply handshake stanza.
|
||||
"""
|
||||
self.session_started_event.set()
|
||||
self.event("session_start")
|
||||
|
||||
def _handle_probe(self, presence):
|
||||
pto = presence['to'].bare
|
||||
pfrom = presence['from'].bare
|
||||
self.roster[pto][pfrom].handle_probe(presence)
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
SleekXMPP: The Sleek XMPP Library
|
||||
Copyright (C) 2010 Nathanael C. Fritz
|
||||
This file is part of SleekXMPP.
|
||||
sleekxmpp.exceptions
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
See the file LICENSE for copying permission.
|
||||
Part of SleekXMPP: The Sleek XMPP Library
|
||||
|
||||
:copyright: (c) 2011 Nathanael C. Fritz
|
||||
:license: MIT, see LICENSE for more details
|
||||
"""
|
||||
|
||||
|
||||
@@ -13,35 +16,35 @@ class XMPPError(Exception):
|
||||
A generic exception that may be raised while processing an XMPP stanza
|
||||
to indicate that an error response stanza should be sent.
|
||||
|
||||
The exception method for stanza objects extending RootStanza will create
|
||||
an error stanza and initialize any additional substanzas using the
|
||||
extension information included in the exception.
|
||||
The exception method for stanza objects extending
|
||||
:class:`~sleekxmpp.stanza.rootstanza.RootStanza` will create an error
|
||||
stanza and initialize any additional substanzas using the extension
|
||||
information included in the exception.
|
||||
|
||||
Meant for use in SleekXMPP plugins and applications using SleekXMPP.
|
||||
|
||||
Extension information can be included to add additional XML elements
|
||||
to the generated error stanza.
|
||||
|
||||
:param condition: The XMPP defined error condition.
|
||||
Defaults to ``'undefined-condition'``.
|
||||
:param text: Human readable text describing the error.
|
||||
:param etype: The XMPP error type, such as ``'cancel'`` or ``'modify'``.
|
||||
Defaults to ``'cancel'``.
|
||||
:param extension: Tag name of the extension's XML content.
|
||||
:param extension_ns: XML namespace of the extensions' XML content.
|
||||
:param extension_args: Content and attributes for the extension
|
||||
element. Same as the additional arguments to
|
||||
the :class:`~xml.etree.ElementTree.Element`
|
||||
constructor.
|
||||
:param clear: Indicates if the stanza's contents should be
|
||||
removed before replying with an error.
|
||||
Defaults to ``True``.
|
||||
"""
|
||||
|
||||
def __init__(self, condition='undefined-condition', text=None, etype=None,
|
||||
extension=None, extension_ns=None, extension_args=None,
|
||||
clear=True):
|
||||
"""
|
||||
Create a new XMPPError exception.
|
||||
|
||||
Extension information can be included to add additional XML elements
|
||||
to the generated error stanza.
|
||||
|
||||
Arguments:
|
||||
condition -- The XMPP defined error condition.
|
||||
text -- Human readable text describing the error.
|
||||
etype -- The XMPP error type, such as cancel or modify.
|
||||
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
|
||||
element. Same as the additional arguments to
|
||||
the ET.Element constructor.
|
||||
clear -- Indicates if the stanza's contents should be
|
||||
removed before replying with an error.
|
||||
Defaults to True.
|
||||
"""
|
||||
def __init__(self, condition='undefined-condition', text=None,
|
||||
etype='cancel', extension=None, extension_ns=None,
|
||||
extension_args=None, clear=True):
|
||||
if extension_args is None:
|
||||
extension_args = {}
|
||||
|
||||
@@ -52,3 +55,36 @@ 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')
|
||||
|
||||
#: The :class:`~sleekxmpp.stanza.iq.Iq` stanza whose response
|
||||
#: did not arrive before the timeout expired.
|
||||
self.iq = iq
|
||||
|
||||
class IqError(XMPPError):
|
||||
|
||||
"""
|
||||
An exception raised when an Iq stanza of type 'error' is received
|
||||
after making a blocking send call.
|
||||
"""
|
||||
|
||||
def __init__(self, iq):
|
||||
super(IqError, self).__init__(
|
||||
condition=iq['error']['condition'],
|
||||
text=iq['error']['text'],
|
||||
etype=iq['error']['type'])
|
||||
|
||||
#: The :class:`~sleekxmpp.stanza.iq.Iq` error result stanza.
|
||||
self.iq = iq
|
||||
|
||||
@@ -42,7 +42,7 @@ class feature_bind(base_plugin):
|
||||
Arguments:
|
||||
features -- The stream features stanza.
|
||||
"""
|
||||
log.debug("Requesting resource: %s" % self.xmpp.boundjid.resource)
|
||||
log.debug("Requesting resource: %s", self.xmpp.boundjid.resource)
|
||||
iq = self.xmpp.Iq()
|
||||
iq['type'] = 'set'
|
||||
iq.enable('bind')
|
||||
@@ -55,7 +55,7 @@ class feature_bind(base_plugin):
|
||||
|
||||
self.xmpp.features.add('bind')
|
||||
|
||||
log.info("Node set to: %s" % self.xmpp.boundjid.full)
|
||||
log.info("Node set to: %s", self.xmpp.boundjid.full)
|
||||
|
||||
if 'session' not in features['features']:
|
||||
log.debug("Established Session")
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -120,7 +123,7 @@ class feature_mechanisms(base_plugin):
|
||||
|
||||
def _handle_fail(self, stanza):
|
||||
"""SASL authentication failed. Disconnect and shutdown."""
|
||||
log.info("Authentication failed: %s" % stanza['condition'])
|
||||
log.info("Authentication failed: %s", stanza['condition'])
|
||||
self.xmpp.event("failed_auth", stanza, direct=True)
|
||||
self.xmpp.disconnect()
|
||||
return True
|
||||
|
||||
@@ -58,8 +58,8 @@ class feature_starttls(base_plugin):
|
||||
self.xmpp.send(features['starttls'], now=True)
|
||||
return True
|
||||
else:
|
||||
log.warning("The module tlslite is required to log in" +\
|
||||
" to some servers, and has not been found.")
|
||||
log.warning("The module tlslite is required to log in" + \
|
||||
" to some servers, and has not been found.")
|
||||
return False
|
||||
|
||||
def _handle_starttls_proceed(self, proceed):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -121,7 +121,7 @@ class gmail_notify(base.base_plugin):
|
||||
def handle_gmail(self, iq):
|
||||
mailbox = iq['mailbox']
|
||||
approx = ' approximately' if mailbox['estimated'] else ''
|
||||
log.info('Gmail: Received%s %s emails' % (approx, mailbox['total-matched']))
|
||||
log.info('Gmail: Received%s %s emails', approx, mailbox['total-matched'])
|
||||
self.last_result_time = mailbox['result-time']
|
||||
self.xmpp.event('gmail_messages', iq)
|
||||
|
||||
@@ -140,7 +140,7 @@ class gmail_notify(base.base_plugin):
|
||||
if query is None:
|
||||
log.info("Gmail: Checking for new emails")
|
||||
else:
|
||||
log.info('Gmail: Searching for emails matching: "%s"' % query)
|
||||
log.info('Gmail: Searching for emails matching: "%s"', query)
|
||||
iq = self.xmpp.Iq()
|
||||
iq['type'] = 'get'
|
||||
iq['to'] = self.xmpp.boundjid.bare
|
||||
|
||||
@@ -43,7 +43,7 @@ class jobs(base.base_plugin):
|
||||
iq['psstate']['payload'] = state
|
||||
result = iq.send()
|
||||
if result is None or type(result) == bool or result['type'] != 'result':
|
||||
log.error("Unable to change %s:%s to %s" % (node, jobid, state))
|
||||
log.error("Unable to change %s:%s to %s", node, jobid, state)
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
@@ -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)
|
||||
11
sleekxmpp/plugins/xep_0004/__init__.py
Normal file
11
sleekxmpp/plugins/xep_0004/__init__.py
Normal 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
|
||||
60
sleekxmpp/plugins/xep_0004/dataforms.py
Normal file
60
sleekxmpp/plugins/xep_0004/dataforms.py
Normal 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
|
||||
10
sleekxmpp/plugins/xep_0004/stanza/__init__.py
Normal file
10
sleekxmpp/plugins/xep_0004/stanza/__init__.py
Normal 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
|
||||
178
sleekxmpp/plugins/xep_0004/stanza/field.py
Normal file
178
sleekxmpp/plugins/xep_0004/stanza/field.py
Normal 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
|
||||
254
sleekxmpp/plugins/xep_0004/stanza/form.py
Normal file
254
sleekxmpp/plugins/xep_0004/stanza/form.py
Normal 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:
|
||||
field = FormField()
|
||||
field._type = self['reported'][var]['type']
|
||||
field['var'] = var
|
||||
field['value'] = values.get(var, None)
|
||||
itemXML.append(field.xml)
|
||||
|
||||
def add_reported(self, var, ftype=None, label='', desc='', **kwargs):
|
||||
kwtype = kwargs.get('type', None)
|
||||
if kwtype is None:
|
||||
kwtype = ftype
|
||||
reported = self.xml.find('{%s}reported' % self.namespace)
|
||||
if reported is None:
|
||||
reported = ET.Element('{%s}reported' % self.namespace)
|
||||
self.xml.append(reported)
|
||||
fieldXML = ET.Element('{%s}field' % FormField.namespace)
|
||||
reported.append(fieldXML)
|
||||
field = FormField(xml=fieldXML)
|
||||
field['var'] = var
|
||||
field['type'] = kwtype
|
||||
field['label'] = label
|
||||
field['desc'] = desc
|
||||
return field
|
||||
|
||||
def cancel(self):
|
||||
self['type'] = 'cancel'
|
||||
|
||||
def del_fields(self):
|
||||
fieldsXML = self.xml.findall('{%s}field' % FormField.namespace)
|
||||
for fieldXML in fieldsXML:
|
||||
self.xml.remove(fieldXML)
|
||||
|
||||
def del_instructions(self):
|
||||
instsXML = self.xml.findall('{%s}instructions')
|
||||
for instXML in instsXML:
|
||||
self.xml.remove(instXML)
|
||||
|
||||
def del_items(self):
|
||||
itemsXML = self.xml.find('{%s}item' % self.namespace)
|
||||
for itemXML in itemsXML:
|
||||
self.xml.remove(itemXML)
|
||||
|
||||
def del_reported(self):
|
||||
reportedXML = self.xml.find('{%s}reported' % self.namespace)
|
||||
if reportedXML is not None:
|
||||
self.xml.remove(reportedXML)
|
||||
|
||||
def get_fields(self, use_dict=False):
|
||||
fields = OrderedDict()
|
||||
fieldsXML = self.xml.findall('{%s}field' % FormField.namespace)
|
||||
for fieldXML in fieldsXML:
|
||||
field = FormField(xml=fieldXML)
|
||||
fields[field['var']] = field
|
||||
return fields
|
||||
|
||||
def get_instructions(self):
|
||||
instructions = ''
|
||||
instsXML = self.xml.findall('{%s}instructions' % self.namespace)
|
||||
return "\n".join([instXML.text for instXML in instsXML])
|
||||
|
||||
def get_items(self):
|
||||
items = []
|
||||
itemsXML = self.xml.findall('{%s}item' % self.namespace)
|
||||
for itemXML in itemsXML:
|
||||
item = OrderedDict()
|
||||
fieldsXML = itemXML.findall('{%s}field' % FormField.namespace)
|
||||
for fieldXML in fieldsXML:
|
||||
field = FormField(xml=fieldXML)
|
||||
item[field['var']] = field['value']
|
||||
items.append(item)
|
||||
return items
|
||||
|
||||
def get_reported(self):
|
||||
fields = OrderedDict()
|
||||
xml = self.xml.findall('{%s}reported/{%s}field' % (self.namespace,
|
||||
FormField.namespace))
|
||||
for field in xml:
|
||||
field = FormField(xml=field)
|
||||
fields[field['var']] = field
|
||||
return fields
|
||||
|
||||
def get_values(self):
|
||||
values = OrderedDict()
|
||||
fields = self['fields']
|
||||
for var in fields:
|
||||
values[var] = fields[var]['value']
|
||||
return values
|
||||
|
||||
def reply(self):
|
||||
if self['type'] == 'form':
|
||||
self['type'] = 'submit'
|
||||
elif self['type'] == 'submit':
|
||||
self['type'] = 'result'
|
||||
|
||||
def set_fields(self, fields):
|
||||
del self['fields']
|
||||
if not isinstance(fields, list):
|
||||
fields = fields.items()
|
||||
for var, field in fields:
|
||||
field['var'] = var
|
||||
self.add_field(**field)
|
||||
|
||||
def set_instructions(self, instructions):
|
||||
del self['instructions']
|
||||
if instructions in [None, '']:
|
||||
return
|
||||
instructions = instructions.split('\n')
|
||||
for instruction in instructions:
|
||||
inst = ET.Element('{%s}instructions' % self.namespace)
|
||||
inst.text = instruction
|
||||
self.xml.append(inst)
|
||||
|
||||
def set_items(self, items):
|
||||
for item in items:
|
||||
self.add_item(item)
|
||||
|
||||
def set_reported(self, reported):
|
||||
for var in reported:
|
||||
field = reported[var]
|
||||
field['var'] = var
|
||||
self.add_reported(var, **field)
|
||||
|
||||
def set_values(self, values):
|
||||
fields = self['fields']
|
||||
for field in values:
|
||||
fields[field]['value'] = values[field]
|
||||
|
||||
def merge(self, other):
|
||||
new = copy.copy(self)
|
||||
if type(other) == dict:
|
||||
new['values'] = other
|
||||
return new
|
||||
nfields = new['fields']
|
||||
ofields = other['fields']
|
||||
nfields.update(ofields)
|
||||
new['fields'] = nfields
|
||||
return new
|
||||
|
||||
|
||||
Form.setType = Form.set_type
|
||||
Form.addField = Form.add_field
|
||||
Form.addItem = Form.add_item
|
||||
Form.addReported = Form.add_reported
|
||||
Form.delFields = Form.del_fields
|
||||
Form.delInstructions = Form.del_instructions
|
||||
Form.delItems = Form.del_items
|
||||
Form.delReported = Form.del_reported
|
||||
Form.getFields = Form.get_fields
|
||||
Form.getInstructions = Form.get_instructions
|
||||
Form.getItems = Form.get_items
|
||||
Form.getReported = Form.get_reported
|
||||
Form.getValues = Form.get_values
|
||||
Form.setFields = Form.set_fields
|
||||
Form.setInstructions = Form.set_instructions
|
||||
Form.setItems = Form.set_items
|
||||
Form.setReported = Form.set_reported
|
||||
Form.setValues = Form.set_values
|
||||
@@ -42,46 +42,46 @@ def py2xml(*args):
|
||||
|
||||
def _py2xml(*args):
|
||||
for x in args:
|
||||
val = ET.Element("value")
|
||||
val = ET.Element("{%s}value" % _namespace)
|
||||
if x is None:
|
||||
nil = ET.Element("nil")
|
||||
nil = ET.Element("{%s}nil" % _namespace)
|
||||
val.append(nil)
|
||||
elif type(x) is int:
|
||||
i4 = ET.Element("i4")
|
||||
i4 = ET.Element("{%s}i4" % _namespace)
|
||||
i4.text = str(x)
|
||||
val.append(i4)
|
||||
elif type(x) is bool:
|
||||
boolean = ET.Element("boolean")
|
||||
boolean = ET.Element("{%s}boolean" % _namespace)
|
||||
boolean.text = str(int(x))
|
||||
val.append(boolean)
|
||||
elif type(x) is str:
|
||||
string = ET.Element("string")
|
||||
string = ET.Element("{%s}string" % _namespace)
|
||||
string.text = x
|
||||
val.append(string)
|
||||
elif type(x) is float:
|
||||
double = ET.Element("double")
|
||||
double = ET.Element("{%s}double" % _namespace)
|
||||
double.text = str(x)
|
||||
val.append(double)
|
||||
elif type(x) is rpcbase64:
|
||||
b64 = ET.Element("Base64")
|
||||
b64 = ET.Element("{%s}base64" % _namespace)
|
||||
b64.text = x.encoded()
|
||||
val.append(b64)
|
||||
elif type(x) is rpctime:
|
||||
iso = ET.Element("dateTime.iso8601")
|
||||
iso = ET.Element("{%s}dateTime.iso8601" % _namespace)
|
||||
iso.text = str(x)
|
||||
val.append(iso)
|
||||
elif type(x) in (list, tuple):
|
||||
array = ET.Element("array")
|
||||
data = ET.Element("data")
|
||||
array = ET.Element("{%s}array" % _namespace)
|
||||
data = ET.Element("{%s}data" % _namespace)
|
||||
for y in x:
|
||||
data.append(_py2xml(y))
|
||||
array.append(data)
|
||||
val.append(array)
|
||||
elif type(x) is dict:
|
||||
struct = ET.Element("struct")
|
||||
struct = ET.Element("{%s}struct" % _namespace)
|
||||
for y in x.keys():
|
||||
member = ET.Element("member")
|
||||
name = ET.Element("name")
|
||||
member = ET.Element("{%s}member" % _namespace)
|
||||
name = ET.Element("{%s}name" % _namespace)
|
||||
name.text = y
|
||||
member.append(name)
|
||||
member.append(_py2xml(x[y]))
|
||||
@@ -105,15 +105,18 @@ def _xml2py(value):
|
||||
if value.find('{%s}int' % namespace) is not None:
|
||||
return int(value.find('{%s}int' % namespace).text)
|
||||
if value.find('{%s}boolean' % namespace) is not None:
|
||||
return bool(value.find('{%s}boolean' % namespace).text)
|
||||
return bool(int(value.find('{%s}boolean' % namespace).text))
|
||||
if value.find('{%s}string' % namespace) is not None:
|
||||
return value.find('{%s}string' % namespace).text
|
||||
if value.find('{%s}double' % namespace) is not None:
|
||||
return float(value.find('{%s}double' % namespace).text)
|
||||
if value.find('{%s}Base64') is not None:
|
||||
return rpcbase64(value.find('Base64' % namespace).text)
|
||||
if value.find('{%s}dateTime.iso8601') is not None:
|
||||
return rpctime(value.find('{%s}dateTime.iso8601'))
|
||||
if value.find('{%s}base64' % namespace) is not None:
|
||||
return rpcbase64(value.find('{%s}base64' % namespace).text.encode())
|
||||
if value.find('{%s}Base64' % namespace) is not None:
|
||||
# Older versions of XEP-0009 used Base64
|
||||
return rpcbase64(value.find('{%s}Base64' % namespace).text.encode())
|
||||
if value.find('{%s}dateTime.iso8601' % namespace) is not None:
|
||||
return rpctime(value.find('{%s}dateTime.iso8601' % namespace).text)
|
||||
if value.find('{%s}struct' % namespace) is not None:
|
||||
struct = {}
|
||||
for member in value.find('{%s}struct' % namespace).findall('{%s}member' % namespace):
|
||||
@@ -135,13 +138,13 @@ class rpcbase64(object):
|
||||
self.data = data
|
||||
|
||||
def decode(self):
|
||||
return base64.decodestring(self.data)
|
||||
return base64.b64decode(self.data)
|
||||
|
||||
def __str__(self):
|
||||
return self.decode()
|
||||
return self.decode().decode()
|
||||
|
||||
def encoded(self):
|
||||
return self.data
|
||||
return self.data.decode()
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ log = logging.getLogger(__name__)
|
||||
|
||||
def _intercept(method, name, public):
|
||||
def _resolver(instance, *args, **kwargs):
|
||||
log.debug("Locally calling %s.%s with arguments %s." % (instance.FQN(), method.__name__, args))
|
||||
log.debug("Locally calling %s.%s with arguments %s.", instance.FQN(), method.__name__, args)
|
||||
try:
|
||||
value = method(instance, *args, **kwargs)
|
||||
if value == NotImplemented:
|
||||
@@ -113,6 +113,9 @@ class ACL:
|
||||
def check(cls, rules, jid, resource):
|
||||
if rules is None:
|
||||
return cls.DENY # No rules means no access!
|
||||
jid = str(jid) # Check the string representation of the JID.
|
||||
if not jid:
|
||||
return cls.DENY # Can't check an empty JID.
|
||||
for rule in rules:
|
||||
policy = cls._check(rule, jid, resource)
|
||||
if policy is not None:
|
||||
@@ -381,7 +384,7 @@ class Proxy(Endpoint):
|
||||
try:
|
||||
if attribute._rpc:
|
||||
def _remote_call(*args, **kwargs):
|
||||
log.debug("Remotely calling '%s.%s' with arguments %s." % (self._endpoint.FQN(), attribute._rpc_name, args))
|
||||
log.debug("Remotely calling '%s.%s' with arguments %s.", self._endpoint.FQN(), attribute._rpc_name, args)
|
||||
return self._endpoint.session._call_remote(self._endpoint.target_jid, "%s.%s" % (self._endpoint.FQN(), attribute._rpc_name), self._callback, *args, **kwargs)
|
||||
return _remote_call
|
||||
except:
|
||||
@@ -449,7 +452,7 @@ class RemoteSession(object):
|
||||
self._event.wait()
|
||||
|
||||
def _notify(self, event):
|
||||
log.debug("RPC Session as %s started." % self._client.boundjid.full)
|
||||
log.debug("RPC Session as %s started.", self._client.boundjid.full)
|
||||
self._client.sendPresence()
|
||||
self._event.set()
|
||||
pass
|
||||
@@ -461,7 +464,7 @@ class RemoteSession(object):
|
||||
if name is None:
|
||||
name = method.__name__
|
||||
key = "%s.%s" % (endpoint, name)
|
||||
log.debug("Registering call handler for %s (%s)." % (key, method))
|
||||
log.debug("Registering call handler for %s (%s).", key, method)
|
||||
with self._lock:
|
||||
if key in self._entries:
|
||||
raise KeyError("A handler for %s has already been regisered!" % endpoint)
|
||||
@@ -469,7 +472,7 @@ class RemoteSession(object):
|
||||
return key
|
||||
|
||||
def _register_acl(self, endpoint, acl):
|
||||
log.debug("Registering ACL %s for endpoint %s." % (repr(acl), endpoint))
|
||||
log.debug("Registering ACL %s for endpoint %s.", repr(acl), endpoint)
|
||||
with self._lock:
|
||||
self._acls[endpoint] = acl
|
||||
|
||||
@@ -562,7 +565,7 @@ class RemoteSession(object):
|
||||
iq.send()
|
||||
return future.get_value(30)
|
||||
else:
|
||||
log.debug("[RemoteSession] _call_remote %s" % callback)
|
||||
log.debug("[RemoteSession] _call_remote %s", callback)
|
||||
self._register_callback(pid, callback)
|
||||
iq.send()
|
||||
|
||||
@@ -601,11 +604,11 @@ class RemoteSession(object):
|
||||
error.send()
|
||||
except Exception as e:
|
||||
if isinstance(e, KeyError):
|
||||
log.error("No handler available for %s!" % pmethod)
|
||||
log.error("No handler available for %s!", pmethod)
|
||||
error = self._client.plugin['xep_0009']._item_not_found(iq)
|
||||
else:
|
||||
traceback.print_exc(file=sys.stderr)
|
||||
log.error("An unexpected problem occurred invoking method %s!" % pmethod)
|
||||
log.error("An unexpected problem occurred invoking method %s!", pmethod)
|
||||
error = self._client.plugin['xep_0009']._undefined_condition(iq)
|
||||
#! print "[REMOTE.PY] _handle_remote_procedure_call AN ERROR SHOULD BE SENT NOW %s " % e
|
||||
error.send()
|
||||
@@ -699,10 +702,10 @@ class Remote(object):
|
||||
with Remote._lock:
|
||||
del cls._sessions[client.boundjid.bare]
|
||||
result = RemoteSession(client, _session_close_callback)
|
||||
client.plugin['xep_0009'].xmpp.add_event_handler('jabber_rpc_method_call', result._on_jabber_rpc_method_call)
|
||||
client.plugin['xep_0009'].xmpp.add_event_handler('jabber_rpc_method_response', result._on_jabber_rpc_method_response)
|
||||
client.plugin['xep_0009'].xmpp.add_event_handler('jabber_rpc_method_fault', result._on_jabber_rpc_method_fault)
|
||||
client.plugin['xep_0009'].xmpp.add_event_handler('jabber_rpc_error', result._on_jabber_rpc_error)
|
||||
client.plugin['xep_0009'].xmpp.add_event_handler('jabber_rpc_method_call', result._on_jabber_rpc_method_call, threaded=True)
|
||||
client.plugin['xep_0009'].xmpp.add_event_handler('jabber_rpc_method_response', result._on_jabber_rpc_method_response, threaded=True)
|
||||
client.plugin['xep_0009'].xmpp.add_event_handler('jabber_rpc_method_fault', result._on_jabber_rpc_method_fault, threaded=True)
|
||||
client.plugin['xep_0009'].xmpp.add_event_handler('jabber_rpc_error', result._on_jabber_rpc_error, threaded=True)
|
||||
if callback is None:
|
||||
start_event_handler = result._notify
|
||||
else:
|
||||
|
||||
@@ -128,22 +128,22 @@ class xep_0009(base.base_plugin):
|
||||
def _handle_method_call(self, iq):
|
||||
type = iq['type']
|
||||
if type == 'set':
|
||||
log.debug("Incoming Jabber-RPC call from %s" % iq['from'])
|
||||
log.debug("Incoming Jabber-RPC call from %s", iq['from'])
|
||||
self.xmpp.event('jabber_rpc_method_call', iq)
|
||||
else:
|
||||
if type == 'error' and ['rpc_query'] is None:
|
||||
self.handle_error(iq)
|
||||
else:
|
||||
log.debug("Incoming Jabber-RPC error from %s" % iq['from'])
|
||||
log.debug("Incoming Jabber-RPC error from %s", iq['from'])
|
||||
self.xmpp.event('jabber_rpc_error', iq)
|
||||
|
||||
def _handle_method_response(self, iq):
|
||||
if iq['rpc_query']['method_response']['fault'] is not None:
|
||||
log.debug("Incoming Jabber-RPC fault from %s" % iq['from'])
|
||||
log.debug("Incoming Jabber-RPC fault from %s", iq['from'])
|
||||
#self._on_jabber_rpc_method_fault(iq)
|
||||
self.xmpp.event('jabber_rpc_method_fault', iq)
|
||||
else:
|
||||
log.debug("Incoming Jabber-RPC response from %s" % iq['from'])
|
||||
log.debug("Incoming Jabber-RPC response from %s", iq['from'])
|
||||
self.xmpp.event('jabber_rpc_method_response', iq)
|
||||
|
||||
def _handle_error(self, iq):
|
||||
|
||||
@@ -71,10 +71,10 @@ class xep_0012(base.base_plugin):
|
||||
|
||||
def handle_last_activity_query(self, iq):
|
||||
if iq['type'] == 'get':
|
||||
log.debug("Last activity requested by %s" % iq['from'])
|
||||
log.debug("Last activity requested by %s", iq['from'])
|
||||
self.xmpp.event('last_activity_request', iq)
|
||||
elif iq['type'] == 'result':
|
||||
log.debug("Last activity result from %s" % iq['from'])
|
||||
log.debug("Last activity result from %s", iq['from'])
|
||||
self.xmpp.event('last_activity', iq)
|
||||
|
||||
def handle_last_activity(self, iq):
|
||||
@@ -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']
|
||||
|
||||
@@ -268,7 +268,7 @@ class xep_0030(base_plugin):
|
||||
"""
|
||||
if local or jid is None:
|
||||
log.debug("Looking up local disco#info data " + \
|
||||
"for %s, node %s." % (jid, node))
|
||||
"for %s, node %s.", jid, node)
|
||||
info = self._run_node_handler('get_info', jid, node, kwargs)
|
||||
return self._fix_default_info(info)
|
||||
|
||||
@@ -542,7 +542,7 @@ class xep_0030(base_plugin):
|
||||
"""
|
||||
if iq['type'] == 'get':
|
||||
log.debug("Received disco info query from " + \
|
||||
"<%s> to <%s>." % (iq['from'], iq['to']))
|
||||
"<%s> to <%s>.", iq['from'], iq['to'])
|
||||
if self.xmpp.is_component:
|
||||
jid = iq['to'].full
|
||||
else:
|
||||
@@ -551,14 +551,17 @@ class xep_0030(base_plugin):
|
||||
jid,
|
||||
iq['disco_info']['node'],
|
||||
iq)
|
||||
iq.reply()
|
||||
if info:
|
||||
info = self._fix_default_info(info)
|
||||
iq.set_payload(info.xml)
|
||||
iq.send()
|
||||
if isinstance(info, Iq):
|
||||
info.send()
|
||||
else:
|
||||
iq.reply()
|
||||
if info:
|
||||
info = self._fix_default_info(info)
|
||||
iq.set_payload(info.xml)
|
||||
iq.send()
|
||||
elif iq['type'] == 'result':
|
||||
log.debug("Received disco info result from" + \
|
||||
"%s to %s." % (iq['from'], iq['to']))
|
||||
"%s to %s.", iq['from'], iq['to'])
|
||||
self.xmpp.event('disco_info', iq)
|
||||
|
||||
def _handle_disco_items(self, iq):
|
||||
@@ -572,21 +575,25 @@ class xep_0030(base_plugin):
|
||||
"""
|
||||
if iq['type'] == 'get':
|
||||
log.debug("Received disco items query from " + \
|
||||
"<%s> to <%s>." % (iq['from'], iq['to']))
|
||||
"<%s> to <%s>.", iq['from'], iq['to'])
|
||||
if self.xmpp.is_component:
|
||||
jid = iq['to'].full
|
||||
else:
|
||||
jid = iq['to'].bare
|
||||
items = self._run_node_handler('get_items',
|
||||
jid,
|
||||
iq['disco_items']['node'])
|
||||
iq.reply()
|
||||
if items:
|
||||
iq.set_payload(items.xml)
|
||||
iq.send()
|
||||
iq['disco_items']['node'],
|
||||
iq)
|
||||
if isinstance(items, Iq):
|
||||
items.send()
|
||||
else:
|
||||
iq.reply()
|
||||
if items:
|
||||
iq.set_payload(items.xml)
|
||||
iq.send()
|
||||
elif iq['type'] == 'result':
|
||||
log.debug("Received disco items result from" + \
|
||||
"%s to %s." % (iq['from'], iq['to']))
|
||||
"%s to %s.", iq['from'], iq['to'])
|
||||
self.xmpp.event('disco_items', iq)
|
||||
|
||||
def _fix_default_info(self, info):
|
||||
|
||||
@@ -14,6 +14,7 @@ from .. stanza.presence import Presence
|
||||
from .. xmlstream.handler.callback import Callback
|
||||
from .. xmlstream.matcher.xpath import MatchXPath
|
||||
from .. xmlstream.matcher.xmlmask import MatchXMLMask
|
||||
from sleekxmpp.exceptions import IqError, IqTimeout
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
@@ -126,7 +127,7 @@ class xep_0045(base.base_plugin):
|
||||
def handle_groupchat_invite(self, inv):
|
||||
""" Handle an invite into a muc.
|
||||
"""
|
||||
logging.debug("MUC invite to %s from %s: %s" % (inv['from'], inv["from"], inv))
|
||||
logging.debug("MUC invite to %s from %s: %s", inv['from'], inv["from"], inv)
|
||||
if inv['from'] not in self.rooms.keys():
|
||||
self.xmpp.event("groupchat_invite", inv)
|
||||
|
||||
@@ -148,7 +149,7 @@ class xep_0045(base.base_plugin):
|
||||
if entry['nick'] not in self.rooms[entry['room']]:
|
||||
got_online = True
|
||||
self.rooms[entry['room']][entry['nick']] = entry
|
||||
log.debug("MUC presence from %s/%s : %s" % (entry['room'],entry['nick'], entry))
|
||||
log.debug("MUC presence from %s/%s : %s", entry['room'],entry['nick'], entry)
|
||||
self.xmpp.event("groupchat_presence", pr)
|
||||
self.xmpp.event("muc::%s::presence" % entry['room'], pr)
|
||||
if got_offline:
|
||||
@@ -188,8 +189,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,15 +214,19 @@ 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
|
||||
|
||||
def joinMUC(self, room, nick, maxhistory="0", password='', wait=False, pstatus=None, pshow=None):
|
||||
def joinMUC(self, room, nick, maxhistory="0", password='', wait=False, pstatus=None, pshow=None, pfrom=None):
|
||||
""" Join the specified room, requesting 'maxhistory' lines of history.
|
||||
"""
|
||||
stanza = self.xmpp.makePresence(pto="%s/%s" % (room, nick), pstatus=pstatus, pshow=pshow)
|
||||
stanza = self.xmpp.makePresence(pto="%s/%s" % (room, nick), pstatus=pstatus, pshow=pshow, pfrom=pfrom)
|
||||
x = ET.Element('{http://jabber.org/protocol/muc}x')
|
||||
if password:
|
||||
passelement = ET.Element('password')
|
||||
@@ -254,12 +263,16 @@ 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
|
||||
|
||||
def setAffiliation(self, room, jid=None, nick=None, affiliation='member'):
|
||||
def setAffiliation(self, room, jid=None, nick=None, affiliation='member', ifrom=None):
|
||||
""" Change room affiliation."""
|
||||
if affiliation not in ('outcast', 'member', 'admin', 'owner', 'none'):
|
||||
raise TypeError
|
||||
@@ -271,9 +284,14 @@ 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
|
||||
iq['from'] = ifrom
|
||||
# For now, swallow errors to preserve existing API
|
||||
try:
|
||||
result = iq.send()
|
||||
except IqError:
|
||||
return False
|
||||
except IqTimeout:
|
||||
return False
|
||||
return True
|
||||
|
||||
def invite(self, room, jid, reason='', mfrom=''):
|
||||
@@ -290,33 +308,38 @@ class xep_0045(base.base_plugin):
|
||||
msg.append(x)
|
||||
self.xmpp.send(msg)
|
||||
|
||||
def leaveMUC(self, room, nick, msg=''):
|
||||
def leaveMUC(self, room, nick, msg='', pfrom=None):
|
||||
""" Leave the specified room.
|
||||
"""
|
||||
if msg:
|
||||
self.xmpp.sendPresence(pshow='unavailable', pto="%s/%s" % (room, nick), pstatus=msg)
|
||||
self.xmpp.sendPresence(pshow='unavailable', pto="%s/%s" % (room, nick), pstatus=msg, pfrom=pfrom)
|
||||
else:
|
||||
self.xmpp.sendPresence(pshow='unavailable', pto="%s/%s" % (room, nick))
|
||||
self.xmpp.sendPresence(pshow='unavailable', pto="%s/%s" % (room, nick), pfrom=pfrom)
|
||||
del self.rooms[room]
|
||||
|
||||
def getRoomConfig(self, room, ifrom=''):
|
||||
iq = self.xmpp.makeIqGet('http://jabber.org/protocol/muc#owner')
|
||||
iq['to'] = room
|
||||
iq['from'] = ifrom
|
||||
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:
|
||||
raise ValueError
|
||||
return self.xmpp.plugin['xep_0004'].buildForm(form)
|
||||
|
||||
def cancelConfig(self, room):
|
||||
def cancelConfig(self, room, ifrom=None):
|
||||
query = ET.Element('{http://jabber.org/protocol/muc#owner}query')
|
||||
x = ET.Element('{jabber:x:data}x', type='cancel')
|
||||
query.append(x)
|
||||
iq = self.xmpp.makeIqSet(query)
|
||||
iq['to'] = room
|
||||
iq['from'] = ifrom
|
||||
iq.send()
|
||||
|
||||
def setRoomConfig(self, room, config, ifrom=''):
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user