Compare commits

..

264 Commits

Author SHA1 Message Date
Nathan Fritz
b8f40eb843 xep_0199 ping now uses scheduler instead of dedicated thread 2010-11-16 17:43:05 -08:00
Florent Le Coz
b73a859031 Add a groupchat_subject event
Use this event to get notified of the subject changes (or to get the
subject of the room when joining one)
2010-11-10 05:54:22 +08:00
Florent Le Coz
9dbf246f0b Doesn't fail if host has NO SRV record
Just catch an other exception type coming from the dns resolver that
could be raised with hosts like "anon.example.com" which just don't have
any SRV record.
2010-11-09 01:53:41 +08:00
Lance Stout
4fb77ac878 Logging no longer uses root logger.
Each module should now log into its own logger.
2010-11-06 01:28:59 -04:00
Lance Stout
d0c506f930 Simplified SleekTest.
* check_stanza does not require stanza_class parameter. Introspection!
* check_message, check_iq, and check_presence removed -- use check
  instead.
* stream_send_stanza, stream_send_message, stream_send_iq, and
  stream_send_presence removed -- use send instead.
* Use recv instead of recv_message, recv_presence, etc.
* check_jid instead of check_JID
* stream_start may accept multi=True to return a new SleekTest instance
  for testing multiple streams at once.
2010-11-05 21:18:48 -04:00
Lance Stout
7351fe1a02 Fix bug introduced while fixing another bug.
Threaded event handlers now handle exceptions again.
2010-11-04 14:35:35 -04:00
Nathan Fritz
38c2f51f83 fixed indent errors 2010-11-04 11:39:41 -07:00
Lance Stout
1bf34caa5b Fixes for XEP-0199 plugin.
Quick fixes to get the XEP-0199 plugin working until a proper cleanup is
done.
2010-11-03 14:04:18 -04:00
Lance Stout
5769935720 Merge branch 'develop' of github.com:fritzy/SleekXMPP into develop 2010-11-03 12:39:44 -04:00
Lance Stout
0214db7545 Catch exceptions for direct events.
Events triggered with direct=True will have exceptions caught.

Note that all event handlers in a direct event will currently run
in the same thread.
2010-11-03 12:38:13 -04:00
Lance Stout
ffc6f031d9 Updated namespaced used in the XEP-0199 plugin. 2010-11-03 12:37:26 -04:00
Lance Stout
9e248bb852 Fix bug in XEP-0030 plugin.
xep_0030 still referenced event_handlers. Added the method event_handled
which will return the number of registered handlers for an event to
resolve the issue.
2010-10-31 18:27:52 -04:00
Lance Stout
973890e2c9 Added try/except for setting signal handlers.
Setting signal handlers from inside a thread is not supported in Python,
but some applications need to run Sleek from a child thread.

SleekXMPP applications that run inside a child thread will NOT be able
to detect SIGHUP or SIGTERM events. Those must be caught and managed by
the main program.
2010-10-28 10:42:23 -04:00
Lance Stout
9c08e56ed0 SSL and signal fixes.
Made setting the SIG* handlers conditional on if the signal defined for
the OS.

Added the attribute ssl_version to XMLStream to set the version of SSL
used during connection. It defaults to ssl.PROTOCOL_TLSv1, but OpenFire
tends to require ssl.PROTOCOL_SSLv23.
2010-10-27 19:27:47 -04:00
Lance Stout
b888610525 Added XEP-202 Entity Time plugin.
Contributed by Cesar Alcalde.
2010-10-25 21:26:25 -04:00
Lance Stout
6d68706326 Added XEP-0012 Last Activity plugin.
Contributed by Cesar Alcalde.
2010-10-25 20:37:02 -04:00
Lance Stout
5bdcd9ef9d Made exceptions work.
Raising an XMPPError exception from an event handler now works, even if
from a threaded handler.

Added stream tests to verify.

We should start using XMPPError, it really makes things simple!
2010-10-25 15:09:56 -04:00
Lance Stout
2eff35cc7a Added more presence stream tests.
Tests auto_authorize=False, and got_online.
2010-10-25 13:21:00 -04:00
Lance Stout
ac330b5c6c Fixed bug in presence subscription handling.
Subscription requests and responses were not setting the correct 'to'
attribute.
2010-10-25 12:52:32 -04:00
Lance Stout
46ffa8e9fe Added stream tests for presence events.
First batch of tests, currently focuses on the got_offline event.
2010-10-24 19:57:07 -04:00
Lance Stout
03847497cc Added test for error stanzas. 2010-10-24 19:56:42 -04:00
Lance Stout
185d7cf28e More JID unit tests.
sleekxmpp.xmlstream.jid now has 100% coverage!
2010-10-24 19:06:54 -04:00
Lance Stout
8aa3d0c047 Fixed got_offline triggering bug. 2010-10-24 18:56:50 -04:00
Lance Stout
9e3d506651 Fixed resource bug in JIDs.
JIDs without resources will return '' instead of the bare JID.

Cleaned up JID tests, and added check_JID to SleekTest.
2010-10-24 18:22:41 -04:00
Lance Stout
2f3ff37a24 Make SleekTest streams register all plugins.
Makes test coverage nicer.
2010-10-24 17:35:11 -04:00
Lance Stout
1f09d60a52 ComponentXMPP saves all of its config data now.
ComponentXMPP was ignoring plugin_config and plugin_whitelist
parameters, making register_plugins() fail.
2010-10-24 17:33:11 -04:00
Lance Stout
d528884723 Added stream tests for rosters. 2010-10-24 12:53:14 -04:00
Lance Stout
d9aff3d36f Merge branch 'develop' of github.com:fritzy/SleekXMPP into develop 2010-10-24 12:11:34 -04:00
Lance Stout
04cc48775d Fixed error in client roster handling.
The roster result iq was not being passed to the roster update
handler.
2010-10-24 12:08:59 -04:00
Nathan Fritz
27ebb6e8f6 presence no longer replies when exception is caught and tweaks to presence events 2010-10-21 16:59:15 -07:00
Lance Stout
8f55704928 Fixed mixed text and elements bug in tostring.
XML of the form <a>foo <b>bar</b> baz</a> was outputted as
<a>foo <b>bar</b> baz baz</a>.

Includes unit test.
2010-10-21 16:21:28 -04:00
Nathan Fritz
d88999691c misc small tweaks 2010-10-20 20:14:26 -07:00
Nathan Fritz
c4699b92e6 pep8 fixes on core library 2010-10-20 19:43:53 -07:00
Nathan Fritz
ce69213a1e when disconnected, reset the roster 2010-10-20 19:33:40 -07:00
Nathan Fritz
77eab6544f reconnect if session isn't established within 15 seconds 2010-10-20 19:18:27 -07:00
Nathan Fritz
11264fe0a8 capture SIGHUP and SIGTERM (windows) and disconnect; also testall no longer loads string26 with python3 2010-10-20 17:30:12 -07:00
Nathan Fritz
11a6e6d2e0 fixed logic error in state machine 2010-10-20 16:57:47 -07:00
Nathan Fritz
6e34b2cfdd fixed disconnect 2010-10-20 16:32:50 -07:00
Lance Stout
e18354ae0e Continue converting to underscored names. 2010-10-18 09:06:54 -04:00
Lance Stout
4375ac7d8b Underscore names by default.
Stanza objects now accept the use of underscored names.

The CamelCase versions are still available for backwards compatibility,
but are discouraged.

The property stanza.values now maps to the old getStanzaValues and
setStanzaValues, in addition to _set_stanza_values and
_get_stanza_values.
2010-10-17 22:04:42 -04:00
Lance Stout
faec86b3be Import plugins from string referenced modules. 2010-10-17 15:47:24 -04:00
Lance Stout
505a63da3a Cleanup, restore PEP8. 2010-10-16 21:15:31 -04:00
Florent Le Coz
93fbcad277 Fix the error on non-number priority
The priority is not a number: we consider it 0 as a default
2010-10-17 09:01:53 +08:00
Florent Le Coz
3625573c7d Default history is 0 2010-10-17 09:01:53 +08:00
Florent Le Coz
d9e7f555e6 MUC leave message and MUC history request
It is now possible to ask for "any number of history stanzas" when
joining a muc (with history=None).
Also we use "maxchars" when asking NO history ("0") since it's a MUST in
the XEP.
And you can specify a message when leaving a MUC.
2010-10-17 09:01:52 +08:00
Florent Le Coz
2755d732a4 Remove deprecation warnings
Remove all the deprecation warnings by using only boundjid.
And also fix a indentation error.
2010-10-17 08:55:30 +08:00
Florent Le Coz
2d18d905a5 Anonymous authentication
Implemented ANONYMOUS authentication on the ClientXMPP class.
To use it, you just need to provide a domain (e.g 'anon.example.com')
with an optional resource (e.g 'anon.example.com/resource') as the JID,
with no password. The JID class has been improved to accept
domains as fulljid.

You can test this with echo_client.py
python echo_client.py -j anon.louiz.org/  # anonymous with a resource
                                          # defined by the server
python echo_client.py -j anon.louiz.org/resource  # anonymous with given
                                                  # resource

The "normal" authentication method still works exactly like before.
2010-10-17 08:55:30 +08:00
Lance Stout
4eb4d729ee Fixed setup.py to use py_modules in the setup call. 2010-10-16 20:48:51 -04:00
Nathan Fritz
8b5c1010de fixed JID to accept server/domain/host as the same 2010-10-14 16:34:16 -07:00
Nathan Fritz
95ad8a1878 fixed stream test not disconnecting cleanly 2010-10-14 16:27:44 -07:00
Nathan Fritz
aeb7999e6a don't import statemachine 2010-10-14 16:08:50 -07:00
Nathan Fritz
8468332381 fixed stream tests 2010-10-14 15:53:10 -07:00
Nathan Fritz
dc001bb201 deprecated jid, fulljid, server, user, resource properties and added boundjid JID 2010-10-14 15:50:54 -07:00
Nathan Fritz
0d0b963fe5 fixed socket name collision in xmlstream.py and fixed python 3.x compatibility 2010-10-14 10:58:07 -07:00
Nathan Fritz
a41a4369c6 disconnect cleanly 2010-10-13 18:21:05 -07:00
Nathan Fritz
7ad7a29a8f new state machine in place 2010-10-13 18:15:21 -07:00
Lance Stout
b0e036d03c Added example live stream test.
Run using:
python tests/live_test.py
2010-10-07 19:46:40 -04:00
Lance Stout
a8b948cd33 SleekTest may now run against a live stream.
Moved SleekTest to sleekxmpp.test package.
Corrected error in XML compare method.
Added TestLiveSocket to run stream tests against live streams.
Modified XMLStream to work with TestLiveSocket.
2010-10-07 19:43:51 -04:00
Lance Stout
e02ffe8547 Corrected test errors.
There was a bug in the XML compare method.
2010-10-07 19:42:28 -04:00
Lance Stout
42bfca1c87 Removed debug log statement. 2010-10-07 19:41:33 -04:00
Lance Stout
0fffbb8200 Unit test reorganization.
Moved SleekTest to sleekxmpp.test.

Organized test suites by their focus.
- Suites focused on testing stanza objects are named test_stanza_X.py
- Suites focused on testing stream behavior are name test_stream_X.py
2010-10-07 10:58:13 -04:00
Lance Stout
21c32c6e1c Moved the pubsub tester to conn_tests. 2010-10-07 10:28:38 -04:00
Lance Stout
75a051556f Changed SleekTest to use underscored names. 2010-10-07 09:22:27 -04:00
Lance Stout
78141fe5f3 Fixed dealing with deleting handlers.
The call to .index() may raise a ValueError if the item is not in the
list. So both the .index() and .pop() calls should be in the try block.
2010-10-07 09:17:28 -04:00
Lance Stout
88d21d210c Corrected stream header tester.
Added test for testing stream headers.
2010-10-06 18:46:23 -04:00
Lance Stout
799645f13f Updated method names.
Using underscored names where possible.
2010-10-06 18:45:11 -04:00
Lance Stout
f234dc02cf Updated SleekTest and related tests.
May now use a component for stream testing.
Methods provided for testing stream headers.
2010-10-06 18:10:04 -04:00
Lance Stout
c294c1a85c More PEP8 compliance cleanups.
Cleaned up the atom entry stanza.
2010-10-06 15:12:39 -04:00
Lance Stout
cbe76c8a70 Cleaned up the Scheduler. 2010-10-06 15:03:21 -04:00
Lance Stout
77b8f0f4bb Fixed whitespace issue. 2010-10-06 14:31:33 -04:00
Lance Stout
259f91d2bd Updated todo list. 2010-10-06 14:23:46 -04:00
Lance Stout
ed366b338d Moved ClientXMPP to clientxmpp.py.
Cleaned up the __init__.py files.
2010-10-06 14:20:32 -04:00
Lance Stout
9e2cada19e Missed a few docstrings. 2010-10-06 14:09:14 -04:00
Lance Stout
d0ccbf6b7a Merge branch 'develop' of github.com:fritzy/SleekXMPP into develop 2010-10-06 14:06:02 -04:00
Lance Stout
e1866ab328 Made first pass at cleaning up ClientXMPP.
Added self.stream_ns to BaseXMPP.
Moved connected/disconnected events and logging to XMLStream.
2010-10-06 14:03:19 -04:00
fritzy
3ffa09ba7c deal with deleting handlers that are no longer there 2010-10-06 17:58:03 +00:00
Lance Stout
a7410f2146 Made a first pass at cleaning up ComponentXMPP. 2010-10-06 10:47:05 -04:00
Lance Stout
178608f4c0 XMLStream cleanup.
Added RestartStream as a top level item in sleekxmpp.xmlstream.

Fixed trailing whitespace.
2010-10-06 10:45:36 -04:00
Lance Stout
19ee6979a5 Updated 1.0 release todo list. 2010-10-03 22:31:22 -04:00
Lance Stout
9f0baec7b2 Made first pass at cleaning BaseXMPP.
Have not intregrated the new JID class yet.
2010-10-01 23:56:46 -04:00
Lance Stout
433c147627 Fixed typo in XEP-0033 plugin. 2010-10-01 21:25:27 -04:00
Lance Stout
9a34c9a9a1 Modified event handling to use the event queue.
Updated tests to match. (Needed to add a small wait to make sure
the event got through the queue before checking the results.)
2010-10-01 13:49:58 -04:00
Lance Stout
2662131124 Fixed tostring bug when using mapped namespaces. 2010-10-01 12:41:35 -04:00
Lance Stout
bb219595a7 Moved event functions to XMLStream.
This is just a transplant, modifying event to use the main
event queue has not been implemented yet.
2010-10-01 12:24:49 -04:00
Lance Stout
fcdd57ce54 Moved add_handler, send, and sendXML to XMLStream. 2010-10-01 11:15:51 -04:00
Lance Stout
5522443e0e Moved getNewId and getId to XMLStream.
This prepares the way for moving add_handler to XMLStream.

Since stanzas, matchers, and handlers in any XML stream will typically
use unique IDs, XMLStream is a good place for these methods.
2010-10-01 10:46:37 -04:00
Lance Stout
55cfe69fef Cleaned up trailing whitespace. 2010-10-01 10:09:10 -04:00
Lance Stout
6de87a1cbf Fixed line lengths and trailing whitespace.
The pep8 command is now pleased.
2010-09-30 13:06:16 -04:00
Lance Stout
7c10ff16fb Made a first pass at cleaning up XMLStream.
A few extra methods are mentioned in the docs, but those have not
been moved to XMLStream from BaseXMPP yet.
2010-09-30 12:56:22 -04:00
Nathan Fritz
c258d2f19d added room events for specific rooms, added buildForm to xep_0004 plugin 2010-09-23 00:51:23 +00:00
fritzy
d576e32f7a Merge branch 'develop' of git@github.com:fritzy/SleekXMPP into develop 2010-09-02 20:01:28 +00:00
Lance Stout
4a2e7c5393 Fixed linespacing and whitespace issues in examples to make them PEP8 compliant. 2010-09-01 18:21:09 -04:00
Lance Stout
0b4320a196 Updated the client and component examples.
The component example now actually uses a config.xml file for its
connection information, and to initialize a roster.
2010-09-01 18:18:30 -04:00
Lance Stout
9bef4b4d4d Move the examples to a top-level examples directory. 2010-09-01 14:47:42 -04:00
Lance Stout
5c3066ba30 Updated all of the matcher classes in sleekxmpp.xmlstream.matcher.
Matchers are now PEP8 compliant and have documentation.
2010-09-01 14:28:43 -04:00
Lance Stout
576eefb097 Fixed line spacing in filesocket.py to please pep8. 2010-09-01 14:25:30 -04:00
Lance Stout
aebd115ba2 A few cleanups to make things simpler. 2010-09-01 14:20:34 -04:00
fritzy
6dfea828be xep-0004 merge should deal with dictionaries 2010-08-31 14:44:24 +00:00
Lance Stout
3749c1b88c Fixed ElementBase.match to match using sub_interface elements. 2010-08-30 17:12:10 -04:00
Lance Stout
998741b87e Fixed typos in ElementBase._fix_ns 2010-08-30 15:25:59 -04:00
Lance Stout
9c62bce206 Updated ElementBase.match to respect namespaces with slashes.
Required adding option to _fix_ns to not propagate namespaces to child elements.
2010-08-30 14:55:30 -04:00
Lance Stout
f5ae27da4f Fix some documentation typos. 2010-08-27 18:16:09 -04:00
Lance Stout
89fb15e896 Updated the suite of handler classes with documentation.
Updated XMLStream to return True or False from removeHandler to indicate if the handler
existed and was removed.

Waiter handlers now unregister themselves after timing out.
2010-08-27 16:42:26 -04:00
Lance Stout
906aa0bd68 Fixed SleekTest compare method to check XML text.
Corrected resulting test failures. All pass again.
2010-08-27 15:48:48 -04:00
Lance Stout
bb6f4af8e2 Added unit tests for StanzaBase. 2010-08-27 12:22:35 -04:00
Lance Stout
6677df39f2 Updated xmlstream.filesocket. 2010-08-27 11:29:48 -04:00
Lance Stout
a2c515bc97 Updated StanzaBase with documentation. 2010-08-27 11:07:20 -04:00
Lance Stout
ca6ce26b0d Added comments to _fix_ns to clarify the cleaning procedure. 2010-08-26 18:40:58 -04:00
Lance Stout
37ff17b0cb Added unit test for _fix_ns for handling namespaces with forward slashes. 2010-08-26 18:27:18 -04:00
Lance Stout
00d7952001 Fixed ElementBase._fix_ns and related methods to respect namespaces which contain forward slashes. 2010-08-26 18:18:00 -04:00
Lance Stout
56766508b3 Fixed indentation in StanzaBase. 2010-08-26 14:19:36 -04:00
Lance Stout
5c59f5baca Clarify ElementBase documentation. 2010-08-26 14:07:09 -04:00
Lance Stout
e16b37d2be Fixed line lengths in ElementBase to comply with PEP8. 2010-08-26 13:55:23 -04:00
Lance Stout
d68bc2ba07 Finished the update of ElementBase with docs and unit tests.
Corrected bugs in equality comparisons between stanzas.
2010-08-26 13:49:36 -04:00
Lance Stout
10298a6eab Updated the remaining ElementBase methods.
Remaining ElementBase todos:
    Write the class documentation for ElementBase.
    Write unit tests for the __magic__ methods.
2010-08-26 10:08:22 -04:00
Lance Stout
a3580dcef9 Fixed ElementBase.match to respect namespaces. 2010-08-25 14:54:09 -04:00
Lance Stout
1eaa9cb28c Updated ElementBase.match and added unit tests. 2010-08-25 14:40:16 -04:00
Lance Stout
5d458bf6c2 Updated ElementBase._delSub and added unit tests.
_delSub can now accept a path and will optionally remove any empty parent elements after deleting the target elements.
2010-08-25 10:52:07 -04:00
Lance Stout
2fa58a74ab Fixed indenting issue. 2010-08-24 09:44:09 -04:00
Lance Stout
c8f406d1b3 Updated ElementBase._setSubText and added unit tests.
_setSubText can now handle elements specified by an XPath expression, and
will build up the element tree as needed, reusing an existing elements in
the path.
2010-08-24 09:37:42 -04:00
Lance Stout
203986dd7c Updated ElementBase._getSubText and added unit tests.
Also added ElementBase._fix_ns() to apply the stanza namespace to elements that don't have a namespace.
2010-08-24 08:55:37 -04:00
fritzy
f4ecf0bac4 fixed a but in stanza_pubsub 2010-08-22 06:08:48 +00:00
fritzy
345656926e added form compatibility with old api, stanzas now bool() to True on 2.x, jid attributes will return '' if not set 2010-08-21 22:48:43 +00:00
Nathan Fritz
c05ddcb7f5 Merge branch 'develop' of git@github.com:fritzy/SleekXMPP into develop 2010-08-19 19:54:09 -07:00
Nathan Fritz
eb9e72fe3e added some xep-0004 compatibility changes 2010-08-19 19:53:56 -07:00
Lance Stout
8a0616b3e0 Updated ElementBase methods _getAttr, _setAttr, and _delAttr with docs and tests. 2010-08-19 20:41:26 -04:00
Lance Stout
b71cfe0492 Small cleanup in ElementBase.__setitem__ 2010-08-19 19:14:18 -04:00
Lance Stout
fac3bca1f6 Updated ElementBase.__delitem__ and added unit tests. 2010-08-19 19:11:12 -04:00
Nathan Fritz
d150b35464 fixed todo merge 2010-08-19 16:09:47 -07:00
Nathan Fritz
21b7109c06 fixed jobs 2010-08-19 16:09:00 -07:00
Lance Stout
e4240dd593 Updated ElementBase.__setitem__ and added unit tests. 2010-08-19 14:21:58 -04:00
Lance Stout
2f6f4fc16d Updated ElementBase.__getitem__ with docs and unit tests. 2010-08-13 21:33:11 -04:00
Lance Stout
fe49b8c377 Updated getStanzaValues and setStanzaValues with docs and unit tests. 2010-08-13 20:05:24 -04:00
Lance Stout
b580a3138d Updated ElementBase.enable and ElementBase.initPlugin 2010-08-13 12:51:07 -04:00
Lance Stout
c20fab0f6c Updated ElementBase.setup, and added unit tests. 2010-08-13 12:24:47 -04:00
Lance Stout
c721fb4126 Added a generic checkStanza method to SleekTest. Updated the other check methods to use it. 2010-08-13 12:23:34 -04:00
Lance Stout
415520200e Updated ElementBase.__init__ 2010-08-13 10:26:33 -04:00
Lance Stout
747001d33c Adjust first level indenting in ElementBase to prepare for cleanup. 2010-08-13 10:15:52 -04:00
Lance Stout
b0fb205c16 Updated registerStanzaPlugin and the XML test type. 2010-08-13 10:12:51 -04:00
Lance Stout
4b52007e8c Cleaned stanzabase imports. 2010-08-12 23:24:09 -04:00
Lance Stout
5da7bd1866 Removed unused xmlcompare.py. 2010-08-12 01:26:01 -04:00
Lance Stout
22134c302b Updated SleekTest with docs and PEP8 style. 2010-08-12 01:25:42 -04:00
Lance Stout
b40a489796 Updated roster stanza with docs and PEP8 style. 2010-08-11 23:32:14 -04:00
Lance Stout
7a5ef28492 Updated SleekTest.streamClose to check that the stream was actually started before closing it.
Updated tests for Iq stanzas to not start a stream for every test; tests now run a lot faster.
The call to streamClose must still be in the tearDown method to ensure it is called in the
case of an error.
2010-08-11 18:41:57 -04:00
Lance Stout
c09e9c702c Updated sleekxmpp.exceptions with PEP8 style and docs. 2010-08-11 18:21:12 -04:00
Lance Stout
48ba7292bc Updated SleekTest to use the new tostring function instead of ET.tostring 2010-08-06 12:04:52 -04:00
Lance Stout
4d1f071f83 Updated the use of tostring in xmlstream.py
Now uses the xmlns and stream parameters to reduce the number of
extra xmlns attributes used in the logging output.

Added self.default_ns to XMLStream just to be safe.
2010-08-05 23:11:22 -04:00
Lance Stout
0d0c044a68 Add unit tests for the tostring function. 2010-08-05 20:57:55 -04:00
Lance Stout
3c0dfb56e6 Update tostring docs to clarify what the xmlns and stanza_ns parameters do. 2010-08-05 20:43:38 -04:00
Lance Stout
e077204a16 Replaced the ToString class with a tostring function.
The sleekxmpp.xmlstream.tostring and sleekxmpp.xmlstream.tostring26 packages
have been merged to sleekxmpp.xmlstream.tostring. The __init__.py file will
import the appropriate tostring function depending on the Python version.

The setup.py file has been updated with the package changes.

ElementBase is now a direct descendent of object and does not subclass ToString.

Stanza objects now return their XML contents for __repr__.
2010-08-05 20:26:41 -04:00
Lance Stout
58f77d898f Updated tests to use a relative import for SleekTest to please Python3.
Fixed some tabs/spaces issues.
2010-08-05 20:23:07 -04:00
Lance Stout
c54466596f Modified sleekxmpp.xmlstream.tostring to import ToString class based on Python version.
The package sleekxmpp.xmlstream.tostring26 remains for now until stanzabase is updated, but is no longer needed.
2010-08-04 14:41:37 -04:00
Lance Stout
aa1dbe97e0 Updated and simplified new JID class to have more documentation and use PEP8 style. 2010-08-04 00:33:28 -04:00
Lance Stout
fec69be731 Update nick stanza with documentation and PEP8 style. 2010-08-03 18:32:53 -04:00
Lance Stout
956fdf6970 Fix whitespace issues, and make some debugging statements clearer. 2010-08-03 18:32:19 -04:00
Lance Stout
183a3f1b87 Updated XHTML-IM stanza with documentation and PEP8 style. 2010-08-03 17:58:18 -04:00
Lance Stout
18683d2b75 Fixed typo in README 2010-08-03 17:32:12 -04:00
Lance Stout
41ab2b8460 Updated presence stanza with documentation and PEP8 style. 2010-08-03 17:30:34 -04:00
Lance Stout
939ae298c2 Updated message stanzas and tests with documentation and PEP8 style. 2010-08-03 12:26:36 -04:00
Nathan Fritz
851e90c572 added dnspython.org to requirements in README 2010-08-03 07:51:52 +00:00
Nathan Fritz
ecde696468 temporary disabled testall methodlength until pep-8 conversion is done 2010-08-03 07:37:58 +00:00
Lance Stout
1cedea2804 Added optional default value to _getAttr. 2010-07-30 14:11:24 -04:00
Lance Stout
cbed8029ba Updated, cleaned, and documented Iq stanza class. Also added unit tests. 2010-07-29 23:58:25 -04:00
Lance Stout
1da3e5b35e Added unit tests for error stanzas. Corrected error in deleting conditions. 2010-07-29 23:55:13 -04:00
Lance Stout
a96a046e27 Remove extra debugging lines and speed up stream testing in SleekTest. 2010-07-29 23:15:49 -04:00
Lance Stout
60a183b011 Added useful imports to the xmlstream, xmlstream.handler, and xmlstream.matcher __init__.py files to make it simpler to import common classes. 2010-07-29 20:18:04 -04:00
Lance Stout
a49f511a2f Added RESPONSE_TIMEOUT constant to sleekxmpp.xmlstream to serve as a single place to specify a default timeout value when waiting for a stanza response. 2010-07-29 20:16:57 -04:00
Lance Stout
25f43bd219 Updated error stanza to be PEP8 compliant and include documentation. 2010-07-29 11:06:10 -04:00
Lance Stout
d148f633f3 Modified ElementBase _getSubText, _setSubText, and _delSubText to
use the namespace in a tag name if one is given and to use
self.namespace otherwise.
2010-07-29 11:04:21 -04:00
Lance Stout
e8e934fa95 Fixed some PEP8 errors in RootStanza (trailing whitespace and line length) 2010-07-29 11:02:42 -04:00
Lance Stout
bd92ef6acf Updated RootStanza to use registerStanzaPlugin, and be PEP8 compliant. 2010-07-28 13:14:41 -04:00
Lance Stout
aa02ecd154 Added notes/ideas/comments on things that can be cleaned/simplified or needs to be expanded before the 1.0 release. 2010-07-27 02:07:22 -04:00
Lance Stout
aad185fe29 Update test to reflect change in reply() method that removes the from attribute. 2010-07-26 21:38:23 -04:00
Nathan Fritz
2b6454786a Merge branch 'experimental' of git@github.com:fritzy/SleekXMPP into experimental 2010-07-26 18:13:54 -07:00
Nathan Fritz
a349a2a317 removed jid from stanzabase to external file 2010-07-26 18:13:34 -07:00
Nathan Fritz
2cb82afc2c updated and moved jid class -- jids now have setters 2010-07-26 18:13:09 -07:00
Lance Stout
c8989c04f3 Replaced traceback calls to use logging.exception where applicable. 2010-07-26 21:02:25 -04:00
Lance Stout
241aba8c76 Merge branch 'experimental' of github.com:fritzy/SleekXMPP into experimental 2010-07-26 19:46:13 -04:00
Lance Stout
ec860bf9e2 Add StateManager as replacement for StateMachine. 2010-07-26 19:44:42 -04:00
Lance Stout
73a3d07ad9 Fix shebang line for testall.py 2010-07-26 19:43:58 -04:00
Lance Stout
07208a3eaf Fix shebang line for testall.py 2010-07-23 19:51:41 -04:00
Lance Stout
d0a5c539d8 Fix shebang lines to use #!/usr/bin/env python instead of hard coding a python version. 2010-07-23 19:47:54 -04:00
Joe Hildebrand
d70a6e6f32 Issue 26. Only set from address in reply() for components 2010-07-20 13:55:48 -07:00
Joe Hildebrand
66e92c6c9f Modified example to take JID and password on command line 2010-07-20 11:33:43 -07:00
Nathan Fritz
ca2c421e6c fixed resource binding element to conform to spec 2010-07-20 11:20:47 -07:00
Nathan Fritz
9fcd2e93a3 don't send resource in bind request if you don't have one 2010-07-20 11:15:59 -07:00
Lance Stout
75afefb5c6 Upated xep_0045 to use old_0004 for now. 2010-07-20 13:23:35 -04:00
Lance Stout
b67b930596 Updated xep_0050 to use old_0004 for now. 2010-07-20 12:27:22 -04:00
Lance Stout
5c9b47afbd Update test_events to use SleekTest to make everything consistent. 2010-07-20 12:22:25 -04:00
Lance Stout
7ad0143687 Updated pubsub stanzas to use xep_0004 stanza objects, and updated tests to match. 2010-07-20 12:18:38 -04:00
Lance Stout
de24e9ed45 Lots of XEP-0004 bug fixes.
Forms have default type of 'form'
setFields now uses a list of tuples instead of a dictionary because ordering is important.
getFields defaults to returning a list of tuples, but the use_dict parameter can change that
2010-07-20 12:16:57 -04:00
Lance Stout
9724efa123 Please tab nanny. 2010-07-20 12:16:06 -04:00
Lance Stout
690eaf8d3c Updated license notices to use the correct MIT format. Also corrected references to nonexistant license.txt to LICENSE. 2010-07-20 11:19:49 -04:00
Lance Stout
f505e229d6 Updated message stanza tests. 2010-07-20 01:56:18 -04:00
Lance Stout
9ca4bba2de Update XEP-0128 to use new xep_0004 2010-07-20 00:34:24 -04:00
Lance Stout
bb927c7e6a Updated presence stanza to include a 'show' interface. Presence stanza tests updated accordingly. 2010-07-20 00:04:34 -04:00
Lance Stout
14f1c3ba51 Updated SleekTest to implement the checkPresence method.
Also, removed unnecessary TestStream class and shortened timeout during stream connection.
2010-07-19 23:58:33 -04:00
Lance Stout
278a8bb443 Removed outdated MANIFEST file. Setuptools will generate a new one when needed. 2010-07-19 23:57:21 -04:00
Nathan Fritz
85ee30539d more set/get Values changes 2010-07-19 16:26:25 -07:00
Nathan Fritz
f74baf1c23 updated sleektest to use new stanza get/set values api 2010-07-19 16:25:01 -07:00
Lance Stout
b5a14a0190 Can now pass a name to add_handler so that the handler can be reliably removed later.
Updated uses of add_handler to include a name.
2010-07-19 19:19:33 -04:00
Nathan Fritz
fec8578cf6 stanza should not have setValues/getValues because that conflicts with attribute accessors 2010-07-19 15:38:48 -07:00
Nathan Fritz
f80b3285d4 indent problem on stanzabase 2010-07-19 14:57:21 -07:00
Nathan Fritz
130a148d34 added fromXML/getXML compatiblity to the new xep-0004 w/ deprecated warnings 2010-07-19 13:53:41 -07:00
Nathan Fritz
16104b6e56 made Lance's new XEP-4 stanzas the default, and put xep-0004 as old_0004 2010-07-19 13:36:28 -07:00
Lance Stout
d5e42ac0e7 Condensed all of the stanzaPlugin functions into a single registerStanzaPlugin function.
Updated plugins and tests to use new function.
2010-07-19 13:58:53 -04:00
Lance Stout
e6bec8681e Added implementation for XEP-0128 Service Discovery Extensions.
Uses the alt_0004 plugin for jabberdata stanza objects.
2010-07-19 04:22:31 -04:00
Lance Stout
797e92a6a3 Fixed error in updateRoster when the name keyword parameter is left out.
The Roster stanza object builds item elements manually, and did not handle the
case where the name attribute is set to None, which would crash SleekXMPP.
2010-07-19 04:12:54 -04:00
Lance Stout
1ef112966b Merge branch 'develop' of git://github.com/fritzy/SleekXMPP into develop 2010-07-19 04:02:28 -04:00
Nathan Fritz
078c71ed3f accidental debugging return left in the code from last commit 2010-07-15 14:25:10 -07:00
Nathan Fritz
bae082f437 fixed updateRoster and delRosterItem 2010-07-15 11:53:35 -07:00
Lance Stout
35212c7991 Updated SleekTest to be able to simulate and test interactions with an XML stream. 2010-07-14 15:32:14 -04:00
Lance Stout
48f0843ace Added initial stanza object version of the xep_0004 plugin. Items/reported elements still need to be unit tested 2010-07-14 11:59:58 -04:00
Lance Stout
b1c997be1d Reworked the Gmail notification plugin to use stanza objects and expose more information. 2010-07-11 22:01:51 -04:00
Lance Stout
d0cb400c54 Fixed tabs to please tab nanny. 2010-07-11 21:43:51 -04:00
Lance Stout
7f8179d91e Refactored unit tests for XEP-0030, XEP-0033, and XEP-0085 to use the new SleekTest class. 2010-06-27 17:47:32 -04:00
Lance Stout
37ada49802 Fixed indentation to please tab nanny during unit tests. 2010-06-27 17:39:16 -04:00
Lance Stout
5c76d969f7 Added a new SleekTest class that provides useful methods for test cases.
Can now use: (where self is a SleekTest instance)
self.stanzaPlugin(stanza, plugin)
self.Message()  \
self.Iq()        > Just like basexmpp.Message(), etc.
self.Presence() /
self.checkMessage(msg, xmlstring)
self.checkIq(iq, xmlstring)
self.checkPresence(pres, xmlstring) <- Not implemented yet, but stub is there.

The check* methods also accept a use_values keyword argument that defaults to True.
When this value is True, an additional test is executed by creating a stanza using
getValues() and setValues(). Since some stanza objects can override these two methods,
disabling this test is sometimes required.
2010-06-27 17:33:43 -04:00
Lance Stout
059cc9ccc4 Fixed several errors in xep_0033 plugin.
The method getAddresses was removing addresses by mistake.
Several instances of using self.attrib instead of self.xml.attrib.
2010-06-27 17:32:16 -04:00
Lance Stout
309c9e74eb Fixed error in setState() method. 2010-06-27 16:34:48 -04:00
Lance Stout
6041cd1952 Fixed typo 2010-06-27 16:33:59 -04:00
Lance Stout
acb53ba371 Fixed tab and spacing issue to please the Tab Nanny during unit tests. 2010-06-27 10:14:21 -04:00
Lance Stout
646a609c0b Added plugin and tests for XEP-0033, Extended Stanza Addresses.
XEP-0033 can be useful for interacting with XMPP<->Email gateways.
2010-06-22 23:22:50 -04:00
Lance Stout
8bb0f5e34c Needed to use copy.deepcopy() to copy XML objects to make sure that the entire tree is copied. 2010-06-07 19:55:39 -04:00
Lance Stout
3c939313d2 Modified basexmpp.event() to pass a copy of the event data to each handler. 2010-06-06 23:19:07 -04:00
Lance Stout
9962f1a664 Added a __copy__ method to both ElementBase and StanzaBase.
Stanzas may now be copied using copy.copy(), which will be useful to prevent
stanza objects from being shared between event handlers.
2010-06-06 23:12:54 -04:00
Lance Stout
253de8518c Modified xmlstream.py to pass a clean stanza object to each stream handler.
The previous version passed the same stanza object to each registered handler,
which can cause issues when the stanza object is modified by one handler. The next
handler receives the stanza with the modifications, not the original stanza.
2010-06-03 22:56:57 -04:00
Nathan Fritz
a38735cb2a added very, very, very basic atom stanza 2010-06-02 15:54:44 -07:00
Lance Stout
e700a54d11 Return result of iq.send() for disco requests. Events are still triggered, but now the caller can determine if there was a timeout. 2010-06-02 15:59:10 -04:00
Lance Stout
6469cdb4ca Merge branch 'develop' of git://github.com/fritzy/SleekXMPP into develop 2010-06-02 15:57:18 -04:00
Nathan Fritz
18e27d65ce Merge branch 'develop' of github.com:fritzy/SleekXMPP into develop 2010-06-01 21:45:15 -07:00
Nathan Fritz
0c39567f20 hack fix for session before bind 2010-06-01 21:44:54 -07:00
Nathan Fritz
f5491c901f if binding and session are advertised in the same go, do session first 2010-06-01 21:40:52 -07:00
Lance stout
f5cae85af5 Make sure that the id parameter used in xmpp.makeIq is converted to a string.
Otherwise, SleekXMPP will barf on trying to serialize an integer when it expects text.
2010-06-01 10:52:37 -04:00
Lance stout
01e8040a07 Added additional parameter to xep_0030's getInfo and getItems methods.
By using dfrom, a server component may send disco requests using any of its JIDS.
2010-06-01 10:51:03 -04:00
Nathan Fritz
aa916c9ac8 included jobs plugin 2010-05-31 13:57:39 -07:00
Lance stout
332eea3b3b Make sure that the node is alway set in disco responses. 2010-05-31 13:35:15 -04:00
Lance stout
109af1b1b6 Merge branch 'xep_0085' into develop 2010-05-31 13:31:11 -04:00
Lance stout
629f6e76a9 Added implementation and tests for XEP-0085 - Chat State Notifications.
Chat states may be set using:

msg['chat_state'].active()
msg['chat_state'].composing()
msg['chat_state'].gone()
msg['chat_state'].inactive()
msg['chat_state'].paused()

Checking a chat state can be done with either:

msg['chat_state'].getState()
msg['chat_state'].name

When a message with a chat state is receieved, the following events
may occur:

chatstate_active
chatstate_composing
chatstate_gone
chatstate_inactive
chatstate_paused

where the event data is the message stanza. Note that currently these
events are also triggered for messages sent by SleekXMPP, not just those
received.
2010-05-31 13:24:14 -04:00
Nathan Fritz
82a3918aa4 Scheduler waits too longer, and pubsubstate registration was backwards 2010-05-31 03:36:25 -07:00
Lance stout
cff3079a04 Added missing 'internal-server-error' condition to error stanza interface. 2010-05-31 05:30:50 +08:00
Lance stout
4f864a07f5 Touched up the style of creating an Iq stanza. 2010-05-31 05:30:49 +08:00
Lance stout
938066bd50 Added 'resource-constraint' to the list of error conditions. 2010-05-31 05:30:48 +08:00
Lance Stout
9fee87c258 Added unit tests for the new XEP-0030 stanza objects. All pass.
(cherry picked from commit e1b814f27bf160f20bb30c315ca30769d217482d)
2010-05-31 05:30:47 +08:00
Lance Stout
fd573880eb Updated the XEP-0030 plugin to work with stanza objects instead of manipulating XML directly.
Four new events have been added:
  disco_info - A disco#info result has been received
  disco_info_request - A disco#info request has been received
  disco_items - A disco#items result has been received
  disco_items_request - A disco#items request has been received

For disco_info_request and disco_items_request two default handlers are registered. These handlers will only run if they are the only handler for these two events so that multiple responses are not returned and cause errors.

In your own handlers for these two events, you can call the default handlers to preserve the static node behaviour as so:
  self.plugin['xep_0030'].handle_disco_info(iq, True)

The forwarded=True will disable the check for other registered handlers.

Agents can now dynamically respond to disco requests by using these events.
(cherry picked from commit 0fc3381492a8bd75e6a9858539a972334881d8ff)
2010-05-31 05:30:45 +08:00
Nathan Fritz
2f1ba368e2 control-c fixes 2010-05-28 19:19:28 -07:00
Nathan Fritz
bde1818400 added pubsubjobs test 2010-05-27 04:59:41 -07:00
Nathan Fritz
3a28f9e5d2 added pubsub state stanzas and scheduled events 2010-05-27 04:58:57 -07:00
Nathan Fritz
0bda5fd3f2 adding scheduler 2010-05-26 18:32:28 -07:00
Nathan Fritz
1e3a6e1b5f added muc room to readme 2010-05-26 11:46:56 -07:00
Nathan Fritz
fa92bc866b fixed dns unicode problem 2010-05-26 11:37:01 -07:00
Nathan Fritz
f4bc9d9722 plugins now are checked for post_init having ran when process() is called 2010-05-26 10:51:51 -07:00
Hernan E Grecco
9cfe19c1e1 Changed example.py to register first Xep_0030.
This a simple fix to prevent getting a key error as many plugins add
features to Xep_0030. A better fix would be to call pos_init after all
 plugins are loaded. An even better fix would be to define dependencies
for each plugin and registering on demand.
2010-05-26 06:49:01 +08:00
Hernan E Grecco
f18c790824 Fixed error registering a plugin. To add a feature to another plugin, it should look into xmpp.plugin dict 2010-05-26 06:49:01 +08:00
Nathan Fritz
f165b4b52b Merge branch 'master' of git@github.com:fritzy/SleekXMPP 2010-05-24 19:34:49 -07:00
Nathan Fritz
7ebc006516 updated README, index fix for component 2010-05-24 19:33:24 -07:00
Lance Stout
5ca4ede5ac Added a flag to registerPlugin to control calling the plugin's post_init method. 2010-05-25 07:28:48 +08:00
Lance Stout
35f4ef3452 Modified the return values for several methods so that they can be chained.
For example:

    iq.reply().error().setPayload(something.xml).send()
2010-05-25 07:28:43 +08:00
Lance Stout
828cba875f Added the error attribute 'code' to the Error object interface. 2010-05-25 07:28:43 +08:00
Nathan Fritz
3920ee3941 added plugin indexing to components 2010-05-24 14:27:13 -07:00
Nathan Fritz
feaa7539af added test_events and testing new del_event_handler 2010-05-20 13:09:04 -07:00
Lance Stout
c004f042f9 Added del_event_handler to remove handler functions for a given event.
All registered handlers for the event which use the given function will
be removed.

Using this method allows agents to reconfigure their behaviour on the fly
without needing to add extra state information to event handling functions.
2010-05-21 03:54:48 +08:00
Nathan Fritz
ae41c08fec added test for unsolicided unavailable presence and fixed bug to make it pass 2010-05-12 18:07:20 -07:00
Nathan Fritz
223507f36f fixed a rather large memory leak 2010-05-12 13:45:36 -07:00
107 changed files with 12429 additions and 4549 deletions

4
.gitignore vendored
View File

@@ -1,6 +1,2 @@
*.pyc
.project
build/
*.swp
.pydevproject
.settings

10
INSTALL
View File

@@ -1,8 +1,12 @@
Pre-requisites:
Python 3.1 or 2.6
- Python 3.1 or 2.6
Install:
python3 setup.py install
> python3 setup.py install
Root install:
sudo python3 setup.py install
> sudo python3 setup.py install
To test:
> cd examples
> python echo_client.py -v -j [USER@example.com] -p [PASSWORD]

View File

@@ -1,4 +1,4 @@
Copyright (c) 2010 ICRL
Copyright (c) 2010 Nathanael C. Fritz
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View File

@@ -1,39 +0,0 @@
setup.py
sleekxmpp/__init__.py
sleekxmpp/basexmpp.py
sleekxmpp/clientxmpp.py
sleekxmpp/example.py
sleekxmpp/plugins/__init__.py
sleekxmpp/plugins/base.py
sleekxmpp/plugins/gmail_notify.py
sleekxmpp/plugins/xep_0004.py
sleekxmpp/plugins/xep_0009.py
sleekxmpp/plugins/xep_0030.py
sleekxmpp/plugins/xep_0045.py
sleekxmpp/plugins/xep_0050.py
sleekxmpp/plugins/xep_0060.py
sleekxmpp/plugins/xep_0078.py
sleekxmpp/plugins/xep_0086.py
sleekxmpp/plugins/xep_0092.py
sleekxmpp/plugins/xep_0199.py
sleekxmpp/stanza/__init__.py
sleekxmpp/stanza/iq.py
sleekxmpp/stanza/message.py
sleekxmpp/stanza/presence.py
sleekxmpp/xmlstream/__init__.py
sleekxmpp/xmlstream/stanzabase.py
sleekxmpp/xmlstream/statemachine.py
sleekxmpp/xmlstream/test.py
sleekxmpp/xmlstream/testclient.py
sleekxmpp/xmlstream/xmlstream.py
sleekxmpp/xmlstream/handler/__init__.py
sleekxmpp/xmlstream/handler/base.py
sleekxmpp/xmlstream/handler/callback.py
sleekxmpp/xmlstream/handler/waiter.py
sleekxmpp/xmlstream/handler/xmlcallback.py
sleekxmpp/xmlstream/handler/xmlwaiter.py
sleekxmpp/xmlstream/matcher/__init__.py
sleekxmpp/xmlstream/matcher/base.py
sleekxmpp/xmlstream/matcher/many.py
sleekxmpp/xmlstream/matcher/xmlmask.py
sleekxmpp/xmlstream/matcher/xpath.py

5
README
View File

@@ -4,6 +4,11 @@ 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.

View File

@@ -1,19 +1,9 @@
"""
SleekXMPP: The Sleek XMPP Library
Copyright (C) 2010 Nathanael C. Fritz
This file is part of SleekXMPP.
SleekXMPP is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
SleekXMPP is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with SleekXMPP; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
See the file LICENSE for copying permission.
"""
import logging
@@ -34,9 +24,9 @@ class testps(sleekxmpp.ClientXMPP):
self.registerPlugin('xep_0030')
self.registerPlugin('xep_0060')
self.registerPlugin('xep_0092')
self.add_handler("<message xmlns='jabber:client'><event xmlns='http://jabber.org/protocol/pubsub#event' /></message>", self.pubsubEventHandler, threaded=True)
self.add_handler("<message xmlns='jabber:client'><event xmlns='http://jabber.org/protocol/pubsub#event' /></message>", self.pubsubEventHandler, name='Pubsub Event', threaded=True)
self.add_event_handler("session_start", self.start, threaded=True)
self.add_handler("<iq type='error' />", self.handleError)
self.add_handler("<iq type='error' />", self.handleError, name='Iq Error')
self.events = Queue.Queue()
self.default_config = None
self.ps = self.plugin['xep_0060']

View File

@@ -1,48 +0,0 @@
# coding=utf8
import sleekxmpp
import logging
from optparse import OptionParser
import time
import sys
if sys.version_info < (3,0):
reload(sys)
sys.setdefaultencoding('utf8')
class Example(sleekxmpp.ClientXMPP):
def __init__(self, jid, password):
sleekxmpp.ClientXMPP.__init__(self, jid, password)
self.add_event_handler("session_start", self.start)
self.add_event_handler("message", self.message)
def start(self, event):
self.getRoster()
self.sendPresence()
def message(self, msg):
msg.reply("Thanks for sending\n%(body)s" % msg).send()
if __name__ == '__main__':
#parse command line arguements
optp = OptionParser()
optp.add_option('-q','--quiet', help='set logging to ERROR', action='store_const', dest='loglevel', const=logging.ERROR, default=logging.INFO)
optp.add_option('-d','--debug', help='set logging to DEBUG', action='store_const', dest='loglevel', const=logging.DEBUG, default=logging.INFO)
optp.add_option('-v','--verbose', help='set logging to COMM', action='store_const', dest='loglevel', const=5, default=logging.INFO)
optp.add_option("-c","--config", dest="configfile", default="config.xml", help="set config file to use")
opts,args = optp.parse_args()
logging.basicConfig(level=opts.loglevel, format='%(levelname)-8s %(message)s')
xmpp = Example('user@gmail.com/sleekxmpp', 'password')
xmpp.registerPlugin('xep_0030')
xmpp.registerPlugin('xep_0004')
xmpp.registerPlugin('xep_0060')
xmpp.registerPlugin('xep_0199')
if xmpp.connect(('talk.google.com', 5222)):
xmpp.process(threaded=False)
print("done")
else:
print("Unable to connect.")

10
examples/config.xml Normal file
View File

@@ -0,0 +1,10 @@
<config xmlns="sleekxmpp:config">
<jid>component.localhost</jid>
<secret>ssshh</secret>
<server>localhost</server>
<port>8888</port>
<query xmlns="jabber:iq:roster">
<item jid="user@example.com" subscription="both" />
</query>
</config>

190
examples/config_component.py Executable file
View File

@@ -0,0 +1,190 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
SleekXMPP: The Sleek XMPP Library
Copyright (C) 2010 Nathanael C. Fritz
This file is part of SleekXMPP.
See the file LICENSE for copying permission.
"""
import sys
import logging
import time
from optparse import OptionParser
import sleekxmpp
from sleekxmpp.componentxmpp import ComponentXMPP
from sleekxmpp.stanza.roster import Roster
from sleekxmpp.xmlstream import ElementBase
from sleekxmpp.xmlstream.stanzabase import ET, registerStanzaPlugin
# Python versions before 3.0 do not use UTF-8 encoding
# by default. To ensure that Unicode is handled properly
# throughout SleekXMPP, we will set the default encoding
# ourselves to UTF-8.
if sys.version_info < (3, 0):
reload(sys)
sys.setdefaultencoding('utf8')
class Config(ElementBase):
"""
In order to make loading and manipulating an XML config
file easier, we will create a custom stanza object for
our config XML file contents. See the documentation
on stanza objects for more information on how to create
and use stanza objects and stanza plugins.
We will reuse the IQ roster query stanza to store roster
information since it already exists.
Example config XML:
<config xmlns="sleekxmpp:config">
<jid>component.localhost</jid>
<secret>ssshh</secret>
<server>localhost</server>
<port>8888</port>
<query xmlns="jabber:iq:roster">
<item jid="user@example.com" subscription="both" />
</query>
</config>
"""
name = "config"
namespace = "sleekxmpp:config"
interfaces = set(('jid', 'secret', 'server', 'port'))
sub_interfaces = interfaces
registerStanzaPlugin(Config, Roster)
class ConfigComponent(ComponentXMPP):
"""
A simple SleekXMPP component that uses an external XML
file to store its configuration data. To make testing
that the component works, it will also echo messages sent
to it.
"""
def __init__(self, config):
"""
Create a ConfigComponent.
Arguments:
config -- The XML contents of the config file.
config_file -- The XML config file object itself.
"""
ComponentXMPP.__init__(self, config['jid'],
config['secret'],
config['server'],
config['port'])
# Store the roster information.
self.roster = config['roster']['items']
# The session_start event will be triggered when
# the component establishes its connection with the
# server and the XML streams are ready for use. We
# want to listen for this event so that we we can
# broadcast any needed initial presence stanzas.
self.add_event_handler("session_start", self.start)
# The message event is triggered whenever a message
# stanza is received. Be aware that that includes
# MUC messages and error messages.
self.add_event_handler("message", self.message)
def start(self, event):
"""
Process the session_start event.
The typical action for the session_start event in a component
is to broadcast presence stanzas to all subscribers to the
component. Note that the component does not have a roster
provided by the XMPP server. In this case, we have possibly
saved a roster in the component's configuration file.
Since the component may use any number of JIDs, you should
also include the JID that is sending the presence.
Arguments:
event -- An empty dictionary. The session_start
event does not provide any additional
data.
"""
for jid in self.roster:
if self.roster[jid]['subscription'] != 'none':
self.sendPresence(pfrom=self.jid, pto=jid)
def message(self, msg):
"""
Process incoming message stanzas. Be aware that this also
includes MUC messages and error messages. It is usually
a good idea to check the messages's type before processing
or sending replies.
Since a component may send messages from any number of JIDs,
it is best to always include a from JID.
Arguments:
msg -- The received message stanza. See the documentation
for stanza objects and the Message stanza to see
how it may be used.
"""
# The reply method will use the messages 'to' JID as the
# outgoing reply's 'from' JID.
msg.reply("Thanks for sending\n%(body)s" % msg).send()
if __name__ == '__main__':
# Setup the command line arguments.
optp = OptionParser()
# Output verbosity options.
optp.add_option('-q', '--quiet', help='set logging to ERROR',
action='store_const', dest='loglevel',
const=logging.ERROR, default=logging.INFO)
optp.add_option('-d', '--debug', help='set logging to DEBUG',
action='store_const', dest='loglevel',
const=logging.DEBUG, default=logging.INFO)
optp.add_option('-v', '--verbose', help='set logging to COMM',
action='store_const', dest='loglevel',
const=5, default=logging.INFO)
# Component name and secret options.
optp.add_option("-c", "--config", help="path to config file",
dest="config", default="config.xml")
opts, args = optp.parse_args()
# Setup logging.
logging.basicConfig(level=opts.loglevel,
format='%(levelname)-8s %(message)s')
# Load configuration data.
config_file = open(opts.config, 'r+')
config_data = "\n".join([line for line in config_file])
config = Config(xml=ET.fromstring(config_data))
config_file.close()
# Setup the ConfigComponent and register plugins. Note that while plugins
# may have interdependencies, the order in which you register them does
# not matter.
xmpp = ConfigComponent(config)
xmpp.registerPlugin('xep_0030') # Service Discovery
xmpp.registerPlugin('xep_0004') # Data Forms
xmpp.registerPlugin('xep_0060') # PubSub
xmpp.registerPlugin('xep_0199') # XMPP Ping
# Connect to the XMPP server and start processing XMPP stanzas.
if xmpp.connect():
xmpp.process(threaded=False)
print("Done")
else:
print("Unable to connect.")

129
examples/echo_client.py Executable file
View File

@@ -0,0 +1,129 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
SleekXMPP: The Sleek XMPP Library
Copyright (C) 2010 Nathanael C. Fritz
This file is part of SleekXMPP.
See the file LICENSE for copying permission.
"""
import sys
import logging
import time
from optparse import OptionParser
import sleekxmpp
# Python versions before 3.0 do not use UTF-8 encoding
# by default. To ensure that Unicode is handled properly
# throughout SleekXMPP, we will set the default encoding
# ourselves to UTF-8.
if sys.version_info < (3, 0):
reload(sys)
sys.setdefaultencoding('utf8')
class EchoBot(sleekxmpp.ClientXMPP):
"""
A simple SleekXMPP bot that will echo messages it
receives, along with a short thank you message.
"""
def __init__(self, jid, password):
sleekxmpp.ClientXMPP.__init__(self, jid, password)
# The session_start event will be triggered when
# the bot establishes its connection with the server
# and the XML streams are ready for use. We want to
# listen for this event so that we we can intialize
# our roster.
self.add_event_handler("session_start", self.start)
# The message event is triggered whenever a message
# stanza is received. Be aware that that includes
# MUC messages and error messages.
self.add_event_handler("message", self.message)
def start(self, event):
"""
Process the session_start event.
Typical actions for the session_start event are
requesting the roster and broadcasting an intial
presence stanza.
Arguments:
event -- An empty dictionary. The session_start
event does not provide any additional
data.
"""
self.getRoster()
self.sendPresence()
def message(self, msg):
"""
Process incoming message stanzas. Be aware that this also
includes MUC messages and error messages. It is usually
a good idea to check the messages's type before processing
or sending replies.
Arguments:
msg -- The received message stanza. See the documentation
for stanza objects and the Message stanza to see
how it may be used.
"""
msg.reply("Thanks for sending\n%(body)s" % msg).send()
if __name__ == '__main__':
# Setup the command line arguments.
optp = OptionParser()
# Output verbosity options.
optp.add_option('-q', '--quiet', help='set logging to ERROR',
action='store_const', dest='loglevel',
const=logging.ERROR, default=logging.INFO)
optp.add_option('-d', '--debug', help='set logging to DEBUG',
action='store_const', dest='loglevel',
const=logging.DEBUG, default=logging.INFO)
optp.add_option('-v', '--verbose', help='set logging to COMM',
action='store_const', dest='loglevel',
const=5, default=logging.INFO)
# JID and password options.
optp.add_option("-j", "--jid", dest="jid",
help="JID to use")
optp.add_option("-p", "--password", dest="password",
help="password to use")
opts, args = optp.parse_args()
# Setup logging.
logging.basicConfig(level=opts.loglevel,
format='%(levelname)-8s %(message)s')
# Setup the EchoBot and register plugins. Note that while plugins may
# have interdependencies, the order in which you register them does
# not matter.
xmpp = EchoBot(opts.jid, opts.password)
xmpp.registerPlugin('xep_0030') # Service Discovery
xmpp.registerPlugin('xep_0004') # Data Forms
xmpp.registerPlugin('xep_0060') # PubSub
xmpp.registerPlugin('xep_0199') # XMPP Ping
# Connect to the XMPP server and start processing XMPP stanzas.
if xmpp.connect():
# If you do not have the pydns library installed, you will need
# to manually specify the name of the server if it does not match
# the one in the JID. For example, to use Google Talk you would
# need to use:
#
# if xmpp.connect(('talk.google.com', 5222)):
# ...
xmpp.process(threaded=False)
print("Done")
else:
print("Unable to connect.")

View File

@@ -16,16 +16,16 @@ import sys
# 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 = '0.2.3.1'
VERSION = '1.0.0.0'
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).
@@ -37,17 +37,20 @@ CLASSIFIERS = [ 'Intended Audience :: Developers',
'Topic :: Software Development :: Libraries :: Python Modules',
]
packages = [ 'sleekxmpp',
'sleekxmpp/plugins',
'sleekxmpp/stanza',
'sleekxmpp/xmlstream',
'sleekxmpp/xmlstream/matcher',
'sleekxmpp/xmlstream/handler' ]
packages = [ 'sleekxmpp',
'sleekxmpp/plugins',
'sleekxmpp/stanza',
'sleekxmpp/test',
'sleekxmpp/xmlstream',
'sleekxmpp/xmlstream/matcher',
'sleekxmpp/xmlstream/handler',
'sleekxmpp/thirdparty',
]
if sys.version_info < (3, 0):
packages.append('sleekxmpp/xmlstream/tostring26')
py_modules = ['sleekxmpp.xmlstream.tostring.tostring26']
else:
packages.append('sleekxmpp/xmlstream/tostring')
py_modules = ['sleekxmpp.xmlstream.tostring.tostring']
setup(
name = "sleekxmpp",
@@ -59,7 +62,8 @@ setup(
url = 'http://code.google.com/p/sleekxmpp',
license = 'MIT',
platforms = [ 'any' ],
packages = packages,
packages = packages,
py_modules = py_modules,
requires = [ 'tlslite', 'pythondns' ],
)

View File

@@ -1,275 +1,16 @@
#!/usr/bin/python2.5
"""
SleekXMPP: The Sleek XMPP Library
Copyright (C) 2010 Nathanael C. Fritz
This file is part of SleekXMPP.
SleekXMPP: The Sleek XMPP Library
Copyright (C) 2010 Nathanael C. Fritz
This file is part of SleekXMPP.
See the file license.txt for copying permission.
See the file LICENSE for copying permission.
"""
from __future__ import absolute_import, unicode_literals
from . basexmpp import basexmpp
from xml.etree import cElementTree as ET
from . xmlstream.xmlstream import XMLStream
from . xmlstream.xmlstream import RestartStream
from . xmlstream.matcher.xmlmask import MatchXMLMask
from . xmlstream.matcher.xpath import MatchXPath
from . xmlstream.matcher.many import MatchMany
from . xmlstream.handler.callback import Callback
from . xmlstream.stanzabase import StanzaBase
from . xmlstream import xmlstream as xmlstreammod
from . stanza.message import Message
from . stanza.iq import Iq
import time
import logging
import base64
import sys
import random
import copy
from . import plugins
#from . import stanza
srvsupport = True
try:
import dns.resolver
import dns.rdatatype
except ImportError:
srvsupport = False
#class PresenceStanzaType(object):
#
# def fromXML(self, xml):
# self.ptype = xml.get('type')
class ClientXMPP(basexmpp, XMLStream):
"""SleekXMPP's client class. Use only for good, not evil."""
def __init__(self, jid, password, ssl=False, plugin_config = {}, plugin_whitelist=[], escape_quotes=True):
global srvsupport
XMLStream.__init__(self)
self.default_ns = 'jabber:client'
basexmpp.__init__(self)
self.plugin_config = plugin_config
self.escape_quotes = escape_quotes
self.set_jid(jid)
self.server = None
self.port = 5222 # not used if DNS SRV is used
self.plugin_whitelist = plugin_whitelist
self.auto_reconnect = True
self.srvsupport = srvsupport
self.password = password
self.registered_features = []
self.stream_header = """<stream:stream to='%s' xmlns:stream='http://etherx.jabber.org/streams' xmlns='%s' version='1.0'>""" % (self.domain,self.default_ns)
self.stream_footer = "</stream:stream>"
#self.map_namespace('http://etherx.jabber.org/streams', 'stream')
#self.map_namespace('jabber:client', '')
self.features = []
#TODO: Use stream state here
self.authenticated = False
self.sessionstarted = False
self.bound = False
self.bindfail = False
self.registerHandler(Callback('Stream Features', MatchXPath('{http://etherx.jabber.org/streams}features'), self._handleStreamFeatures, thread=True))
self.registerHandler(Callback('Roster Update', MatchXPath('{%s}iq/{jabber:iq:roster}query' % self.default_ns), self._handleRoster, thread=True))
#self.registerHandler(Callback('Roster Update', MatchXMLMask("<presence xmlns='%s' type='subscribe' />" % self.default_ns), self._handlePresenceSubscribe, thread=True))
self.registerFeature("<starttls xmlns='urn:ietf:params:xml:ns:xmpp-tls' />", self.handler_starttls, True)
self.registerFeature("<mechanisms xmlns='urn:ietf:params:xml:ns:xmpp-sasl' />", self.handler_sasl_auth, True)
self.registerFeature("<bind xmlns='urn:ietf:params:xml:ns:xmpp-bind' />", self.handler_bind_resource)
self.registerFeature("<session xmlns='urn:ietf:params:xml:ns:xmpp-session' />", self.handler_start_session)
#self.registerStanzaExtension('PresenceStanza', PresenceStanzaType)
#self.register_plugins()
def __getitem__(self, key):
if key in self.plugin:
return self.plugin[key]
else:
logging.warning("""Plugin "%s" is not loaded.""" % key)
return False
def get(self, key, default):
return self.plugin.get(key, default)
def connect(self, host=None, port=None):
"""Connect to the Jabber Server. Attempts SRV lookup, and if it fails, uses
the JID server."""
if self.state['connected']: return True
if host:
self.server = host
if port is None: port = self.port
else:
if not self.srvsupport:
logging.debug("Did not supply (address, port) to connect to and no SRV support is installed (http://www.dnspython.org). Continuing to attempt connection, using domain from JID.")
else:
logging.debug("Since no address is supplied, attempting SRV lookup.")
try:
answers = dns.resolver.query("_xmpp-client._tcp.%s" % self.domain,
dns.rdatatype.SRV )
except dns.resolver.NXDOMAIN:
logging.debug("No appropriate SRV record found. Using JID server name.")
else:
# pick a random answer, weighted by priority
# there are less verbose ways of doing this (random.choice() with answer * priority), but I chose this way anyway
# suggestions are welcome
addresses = {}
intmax = 0
priorities = []
for answer in answers:
intmax += answer.priority
addresses[intmax] = (answer.target.to_text()[:-1], answer.port)
priorities.append(intmax) # sure, I could just do priorities = addresses.keys()\n priorities.sort()
picked = random.randint(0, intmax)
for priority in priorities:
if picked <= priority:
(host,port) = addresses[priority]
break
# if SRV lookup was successful, we aren't using a particular server.
self.server = None
if not host:
# if all else fails take server from JID.
(host,port) = (self.domain, self.port)
self.server = None
logging.debug('Attempting connection to %s:%d', host, port )
#TODO option to not use TLS?
result = XMLStream.connect(self, host, port, use_tls=True)
if result:
self.event("connected")
else:
logging.warning("Failed to connect")
self.event("disconnected")
return result
# overriding reconnect and disconnect so that we can get some events
# should events be part of or required by xmlstream? Maybe that would be cleaner
def reconnect(self):
self.disconnect(reconnect=True)
def disconnect(self, reconnect=False):
self.event("disconnected")
self.authenticated = False
self.sessionstarted = False
XMLStream.disconnect(self, reconnect)
def registerFeature(self, mask, pointer, breaker = False):
"""Register a stream feature."""
self.registered_features.append((MatchXMLMask(mask), pointer, breaker))
def updateRoster(self, jid, name=None, subscription=None, groups=[]):
"""Add or change a roster item."""
iq = self.Iq().setValues({'type': 'set'})
iq['roster'] = {jid: {'name': name, 'subscription': subscription, 'groups': groups}}
#self.send(iq, self.Iq().setValues({'id': iq['id']}))
r = iq.send()
return r['type'] == 'result'
def getRoster(self):
"""Request the roster be sent."""
iq = self.Iq().setValues({'type': 'get'}).enable('roster').send()
self._handleRoster(iq, request=True)
def _handleStreamFeatures(self, features):
logging.debug('handling stream features')
self.features = []
for sub in features.xml:
self.features.append(sub.tag)
for subelement in features.xml:
for feature in self.registered_features:
if feature[0].match(subelement):
#if self.maskcmp(subelement, feature[0], True):
# This calls the feature handler & optionally breaks
if feature[1](subelement) and feature[2]: #if breaker, don't continue
return True
def handler_starttls(self, xml):
logging.debug( 'TLS start handler; SSL support: %s', self.ssl_support )
if not self.authenticated and self.ssl_support:
_stanza = "<proceed xmlns='urn:ietf:params:xml:ns:xmpp-tls' />"
if not self.event_handlers.get(_stanza,None): # don't add handler > once
self.add_handler( _stanza, self.handler_tls_start, instream=True )
self.sendXML(xml)
return True
else:
logging.warning("The module tlslite is required in to some servers, and has not been found.")
return False
def handler_tls_start(self, xml):
logging.debug("Starting TLS")
if self.startTLS():
raise RestartStream()
def handler_sasl_auth(self, xml):
if '{urn:ietf:params:xml:ns:xmpp-tls}starttls' in self.features:
return False
logging.debug("Starting SASL Auth")
self.add_handler("<success xmlns='urn:ietf:params:xml:ns:xmpp-sasl' />", self.handler_auth_success, instream=True)
self.add_handler("<failure xmlns='urn:ietf:params:xml:ns:xmpp-sasl' />", self.handler_auth_fail, instream=True)
sasl_mechs = xml.findall('{urn:ietf:params:xml:ns:xmpp-sasl}mechanism')
if len(sasl_mechs):
for sasl_mech in sasl_mechs:
self.features.append("sasl:%s" % sasl_mech.text)
if 'sasl:PLAIN' in self.features:
if sys.version_info < (3,0):
self.send("""<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' mechanism='PLAIN'>%s</auth>""" % base64.b64encode(b'\x00' + bytes(self.username) + b'\x00' + bytes(self.password)).decode('utf-8'))
else:
self.send("""<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' mechanism='PLAIN'>%s</auth>""" % base64.b64encode(b'\x00' + bytes(self.username, 'utf-8') + b'\x00' + bytes(self.password, 'utf-8')).decode('utf-8'))
else:
logging.error("No appropriate login method.")
self.disconnect()
#if 'sasl:DIGEST-MD5' in self.features:
# self._auth_digestmd5()
return True
def handler_auth_success(self, xml):
logging.debug("Authentication successful.")
self.authenticated = True
self.features = []
raise RestartStream()
def handler_auth_fail(self, xml):
logging.warning("Authentication failed.")
self.disconnect()
self.event("failed_auth")
def handler_bind_resource(self, xml):
logging.debug("Requesting resource: %s" % self.resource)
iq = self.Iq(stype='set')
res = ET.Element('resource')
res.text = self.resource
xml.append(res)
iq.append(xml)
response = iq.send()
#response = self.send(iq, self.Iq(sid=iq['id']))
self.set_jid(response.xml.find('{urn:ietf:params:xml:ns:xmpp-bind}bind/{urn:ietf:params:xml:ns:xmpp-bind}jid').text)
self.bound = True
logging.info("Node set to: %s" % self.fulljid)
if "{urn:ietf:params:xml:ns:xmpp-session}session" not in self.features or self.bindfail:
logging.debug("Established Session")
self.sessionstarted = True
self.event("session_start")
def handler_start_session(self, xml):
if self.authenticated and self.bound:
iq = self.makeIqSet(xml)
response = iq.send()
logging.debug("Established Session")
self.sessionstarted = True
self.event("session_start")
else:
#bind probably hasn't happened yet
self.bindfail = True
def _handleRoster(self, iq, request=False):
if iq['type'] == 'set' or (iq['type'] == 'result' and request):
for jid in iq['roster']['items']:
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])
if iq['type'] == 'set':
self.send(self.Iq().setValues({'type': 'result', 'id': iq['id']}).enable('roster'))
self.event("roster_update", iq)
from sleekxmpp.basexmpp import BaseXMPP
from sleekxmpp.clientxmpp import ClientXMPP
from sleekxmpp.componentxmpp import ComponentXMPP
from sleekxmpp.stanza import Message, Presence, Iq
from sleekxmpp.xmlstream.handler import *
from sleekxmpp.xmlstream import XMLStream, RestartStream
from sleekxmpp.xmlstream.matcher import *
from sleekxmpp.xmlstream.stanzabase import StanzaBase, ET

View File

@@ -1,318 +1,634 @@
"""
SleekXMPP: The Sleek XMPP Library
Copyright (C) 2010 Nathanael C. Fritz
This file is part of SleekXMPP.
SleekXMPP: The Sleek XMPP Library
Copyright (C) 2010 Nathanael C. Fritz
This file is part of SleekXMPP.
See the file license.txt for copying permission.
See the file LICENSE for copying permission.
"""
from __future__ import with_statement, unicode_literals
from xml.etree import cElementTree as ET
from . xmlstream.xmlstream import XMLStream
from . xmlstream.matcher.xmlmask import MatchXMLMask
from . xmlstream.matcher.many import MatchMany
from . xmlstream.handler.xmlcallback import XMLCallback
from . xmlstream.handler.xmlwaiter import XMLWaiter
from . xmlstream.handler.waiter import Waiter
from . xmlstream.handler.callback import Callback
from . import plugins
from . stanza.message import Message
from . stanza.iq import Iq
from . stanza.presence import Presence
from . stanza.roster import Roster
from . stanza.nick import Nick
from . stanza.htmlim import HTMLIM
from . stanza.error import Error
import logging
import threading
import sys
import copy
import logging
if sys.version_info < (3,0):
reload(sys)
sys.setdefaultencoding('utf8')
import sleekxmpp
from sleekxmpp import plugins
from sleekxmpp.stanza import Message, Presence, Iq, Error
from sleekxmpp.stanza.roster import Roster
from sleekxmpp.stanza.nick import Nick
from sleekxmpp.stanza.htmlim import HTMLIM
from sleekxmpp.xmlstream import XMLStream, JID, tostring
from sleekxmpp.xmlstream import ET, register_stanza_plugin
from sleekxmpp.xmlstream.matcher import *
from sleekxmpp.xmlstream.handler import *
def stanzaPlugin(stanza, plugin):
stanza.plugin_attrib_map[plugin.plugin_attrib] = plugin
stanza.plugin_tag_map["{%s}%s" % (plugin.namespace, plugin.name)] = plugin
log = logging.getLogger(__name__)
# In order to make sure that Unicode is handled properly
# in Python 2.x, reset the default encoding.
if sys.version_info < (3, 0):
reload(sys)
sys.setdefaultencoding('utf8')
class basexmpp(object):
def __init__(self):
self.id = 0
self.id_lock = threading.Lock()
self.sentpresence = False
self.fulljid = ''
self.resource = ''
self.jid = ''
self.username = ''
self.domain = ''
self.plugin = {}
self.auto_authorize = True
self.auto_subscribe = True
self.event_handlers = {}
self.roster = {}
self.registerHandler(Callback('IM', MatchXMLMask("<message xmlns='%s'><body /></message>" % self.default_ns), self._handleMessage))
self.registerHandler(Callback('Presence', MatchXMLMask("<presence xmlns='%s' />" % self.default_ns), self._handlePresence))
self.add_event_handler('presence_subscribe', self._handlePresenceSubscribe)
self.registerStanza(Message)
self.registerStanza(Iq)
self.registerStanza(Presence)
self.stanzaPlugin(Iq, Roster)
self.stanzaPlugin(Message, Nick)
self.stanzaPlugin(Message, HTMLIM)
class BaseXMPP(XMLStream):
def stanzaPlugin(self, stanza, plugin):
stanza.plugin_attrib_map[plugin.plugin_attrib] = plugin
stanza.plugin_tag_map["{%s}%s" % (plugin.namespace, plugin.name)] = plugin
def Message(self, *args, **kwargs):
return Message(self, *args, **kwargs)
"""
The BaseXMPP class adapts the generic XMLStream class for use
with XMPP. It also provides a plugin mechanism to easily extend
and add support for new XMPP features.
def Iq(self, *args, **kwargs):
return Iq(self, *args, **kwargs)
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.
def Presence(self, *args, **kwargs):
return Presence(self, *args, **kwargs)
def set_jid(self, jid):
"""Rip a JID apart and claim it as our own."""
self.fulljid = jid
self.resource = self.getjidresource(jid)
self.jid = self.getjidbare(jid)
self.username = jid.split('@', 1)[0]
self.domain = jid.split('@',1)[-1].split('/', 1)[0]
def process(self, *args, **kwargs):
for idx in self.plugin:
if not self.plugin[idx].post_inited: self.plugin[idx].post_init()
return super(basexmpp, self).process(*args, **kwargs)
def registerPlugin(self, plugin, pconfig = {}, pluginModule = None):
"""Register a plugin not in plugins.__init__.__all__ but in the plugins
directory."""
# discover relative "path" to the plugins module from the main app, and import it.
# TODO:
# gross, this probably isn't necessary anymore, especially for an installed module
try:
if pluginModule:
module = __import__(pluginModule, globals(), locals(), [plugin])
else:
module = __import__("%s.%s" % (globals()['plugins'].__name__, plugin), globals(), locals(), [plugin])
# init the plugin class
self.plugin[plugin] = getattr(module, plugin)(self, pconfig) # eek
# all of this for a nice debug? sure.
xep = ''
if hasattr(self.plugin[plugin], 'xep'):
xep = "(XEP-%s) " % self.plugin[plugin].xep
logging.debug("Loaded Plugin %s%s" % (xep, self.plugin[plugin].description))
except:
logging.error("Unable to load plugin: %s" %(plugin) )
def register_plugins(self):
"""Initiates all plugins in the plugins/__init__.__all__"""
if self.plugin_whitelist:
plugin_list = self.plugin_whitelist
else:
plugin_list = plugins.__all__
for plugin in plugin_list:
if plugin in plugins.__all__:
self.registerPlugin(plugin, self.plugin_config.get(plugin, {}), False)
else:
raise NameError("No plugin by the name of %s listed in plugins.__all__." % plugin)
# run post_init() for cross-plugin interaction
for plugin in self.plugin:
self.plugin[plugin].post_init()
def getNewId(self):
with self.id_lock:
self.id += 1
return self.getId()
def add_handler(self, mask, pointer, disposable=False, threaded=False, filter=False, instream=False):
#logging.warning("Deprecated add_handler used for %s: %s." % (mask, pointer))
self.registerHandler(XMLCallback('add_handler_%s' % self.getNewId(), MatchXMLMask(mask), pointer, threaded, disposable, instream))
def getId(self):
return "%x".upper() % self.id
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.
"""
def sendXML(self, data, mask=None, timeout=10):
return self.send(self.tostring(data), mask, timeout)
def send(self, data, mask=None, timeout=10):
#logging.warning("Deprecated send used for \"%s\"" % (data,))
#if not type(data) == type(''):
# data = self.tostring(data)
if hasattr(mask, 'xml'):
mask = mask.xml
data = str(data)
if mask is not None:
logging.warning("Use of send mask waiters is deprecated")
waitfor = Waiter('SendWait_%s' % self.getNewId(), MatchXMLMask(mask))
self.registerHandler(waitfor)
self.sendRaw(data)
if mask is not None:
return waitfor.wait(timeout)
def makeIq(self, id=0, ifrom=None):
return self.Iq().setValues({'id': id, 'from': ifrom})
def makeIqGet(self, queryxmlns = None):
iq = self.Iq().setValues({'type': 'get'})
if queryxmlns:
iq.append(ET.Element("{%s}query" % queryxmlns))
return iq
def makeIqResult(self, id):
return self.Iq().setValues({'id': id, 'type': 'result'})
def makeIqSet(self, sub=None):
iq = self.Iq().setValues({'type': 'set'})
if sub != None:
iq.append(sub)
return iq
def __init__(self, default_ns='jabber:client'):
"""
Adapt an XML stream for use with XMPP.
def makeIqError(self, id, type='cancel', condition='feature-not-implemented', text=None):
iq = self.Iq().setValues({'id': id})
iq['error'].setValues({'type': type, 'condition': condition, 'text': text})
return iq
Arguments:
default_ns -- Ensure that the correct default XML namespace
is used during initialization.
"""
XMLStream.__init__(self)
def makeIqQuery(self, iq, xmlns):
query = ET.Element("{%s}query" % xmlns)
iq.append(query)
return iq
def makeQueryRoster(self, iq=None):
query = ET.Element("{jabber:iq:roster}query")
if iq:
iq.append(query)
return query
def add_event_handler(self, name, pointer, threaded=False, disposable=False):
if not name in self.event_handlers:
self.event_handlers[name] = []
self.event_handlers[name].append((pointer, threaded, disposable))
# To comply with PEP8, method names now use underscores.
# Deprecated method names are re-mapped for backwards compatibility.
self.registerPlugin = self.register_plugin
self.makeIq = self.make_iq
self.makeIqGet = self.make_iq_get
self.makeIqResult = self.make_iq_result
self.makeIqSet = self.make_iq_set
self.makeIqError = self.make_iq_error
self.makeIqQuery = self.make_iq_query
self.makeQueryRoster = self.make_query_roster
self.makeMessage = self.make_message
self.makePresence = self.make_presence
self.sendMessage = self.send_message
self.sendPresence = self.send_presence
self.sendPresenceSubscription = self.send_presence_subscription
def del_event_handler(self, name, pointer):
"""Remove a handler for an event."""
if not name in self.event_handlers:
return
# Need to keep handlers that do not use
# the given function pointer
def filter_pointers(handler):
return handler[0] != pointer
self.default_ns = default_ns
self.stream_ns = 'http://etherx.jabber.org/streams'
self.event_handlers[name] = filter(filter_pointers,
self.event_handlers[name])
self.boundjid = JID("")
def event(self, name, eventdata = {}): # called on an event
for handler in self.event_handlers.get(name, []):
if handler[1]: #if threaded
#thread.start_new(handler[0], (eventdata,))
x = threading.Thread(name="Event_%s" % str(handler[0]), target=handler[0], args=(eventdata,))
x.start()
else:
handler[0](eventdata)
if handler[2]: #disposable
with self.lock:
self.event_handlers[name].pop(self.event_handlers[name].index(handler))
def makeMessage(self, mto, mbody=None, msubject=None, mtype=None, mhtml=None, mfrom=None, mnick=None):
message = self.Message(sto=mto, stype=mtype, sfrom=mfrom)
message['body'] = mbody
message['subject'] = msubject
if mnick is not None: message['nick'] = mnick
if mhtml is not None: message['html']['html'] = mhtml
return message
def makePresence(self, pshow=None, pstatus=None, ppriority=None, pto=None, ptype=None, pfrom=None):
presence = self.Presence(stype=ptype, sfrom=pfrom, sto=pto)
if pshow is not None: presence['type'] = pshow
if pfrom is None: #maybe this should be done in stanzabase
presence['from'] = self.fulljid
presence['priority'] = ppriority
presence['status'] = pstatus
return presence
def sendMessage(self, mto, mbody, msubject=None, mtype=None, mhtml=None, mfrom=None, mnick=None):
self.send(self.makeMessage(mto,mbody,msubject,mtype,mhtml,mfrom,mnick))
def sendPresence(self, pshow=None, pstatus=None, ppriority=None, pto=None, pfrom=None, ptype=None):
self.send(self.makePresence(pshow,pstatus,ppriority,pto, ptype=ptype, pfrom=pfrom))
if not self.sentpresence:
self.event('sent_presence')
self.sentpresence = True
self.plugin = {}
self.roster = {}
self.is_component = False
self.auto_authorize = True
self.auto_subscribe = True
def sendPresenceSubscription(self, pto, pfrom=None, ptype='subscribe', pnick=None) :
presence = self.makePresence(ptype=ptype, pfrom=pfrom, pto=self.getjidbare(pto))
if pnick :
nick = ET.Element('{http://jabber.org/protocol/nick}nick')
nick.text = pnick
presence.append(nick)
self.send(presence)
def getjidresource(self, fulljid):
if '/' in fulljid:
return fulljid.split('/', 1)[-1]
else:
return ''
def getjidbare(self, fulljid):
return fulljid.split('/', 1)[0]
self.sentpresence = False
def _handleMessage(self, msg):
self.event('message', msg)
def _handlePresence(self, presence):
"""Update roster items based on presence"""
self.event("presence_%s" % presence['type'], presence)
if presence['type'] in ('subscribe', 'subscribed', 'unsubscribe', 'unsubscribed'):
self.event('changed_subscription', presence)
return
elif not presence['type'] in ('available', 'unavailable') and not presence['type'] in presence.showtypes:
return
jid = presence['from'].bare
resource = presence['from'].resource
show = presence['type']
status = presence['status']
priority = presence['priority']
wasoffline = False
oldroster = self.roster.get(jid, {}).get(resource, {})
if not presence['from'].bare in self.roster:
self.roster[jid] = {'groups': [], 'name': '', 'subscription': 'none', 'presence': {}, 'in_roster': False}
if not resource in self.roster[jid]['presence']:
if (show == 'available' or show in presence.showtypes):
self.event("got_online", presence)
wasoffline = True
self.roster[jid]['presence'][resource] = {}
if self.roster[jid]['presence'][resource].get('show', 'unavailable') == 'unavailable':
wasoffline = True
self.roster[jid]['presence'][resource] = {'show': show, 'status': status, 'priority': priority}
name = self.roster[jid].get('name', '')
if show == 'unavailable':
logging.debug("%s %s got offline" % (jid, resource))
del self.roster[jid]['presence'][resource]
if len(self.roster[jid]['presence']) == 0 and not self.roster[jid]['in_roster']:
del self.roster[jid]
if not wasoffline:
self.event("got_offline", presence)
else:
return False
self.event("changed_status", presence)
name = ''
if name:
name = "(%s) " % name
logging.debug("STATUS: %s%s/%s[%s]: %s" % (name, jid, resource, show,status))
def _handlePresenceSubscribe(self, presence):
"""Handling subscriptions automatically."""
if self.auto_authorize == True:
self.send(self.makePresence(ptype='subscribed', pto=presence['from'].bare))
if self.auto_subscribe:
self.send(self.makePresence(ptype='subscribe', pto=presence['from'].bare))
elif self.auto_authorize == False:
self.send(self.makePresence(ptype='unsubscribed', pto=presence['from'].bare))
self.register_handler(
Callback('IM',
MatchXPath('{%s}message/{%s}body' % (self.default_ns,
self.default_ns)),
self._handle_message))
self.register_handler(
Callback('Presence',
MatchXPath("{%s}presence" % self.default_ns),
self._handle_presence))
self.add_event_handler('presence_subscribe',
self._handle_subscribe)
self.add_event_handler('disconnected',
self._handle_disconnected)
# Set up the XML stream with XMPP's root stanzas.
self.registerStanza(Message)
self.registerStanza(Iq)
self.registerStanza(Presence)
# Initialize a few default stanza plugins.
register_stanza_plugin(Iq, Roster)
register_stanza_plugin(Message, Nick)
register_stanza_plugin(Message, HTMLIM)
def process(self, *args, **kwargs):
"""
Ensure that plugin inter-dependencies are handled before starting
event processing.
Overrides XMLStream.process.
"""
for name in self.plugin:
if not self.plugin[name].post_inited:
self.plugin[name].post_init()
return XMLStream.process(self, *args, **kwargs)
def register_plugin(self, plugin, pconfig={}, module=None):
"""
Register and configure a plugin for use in this stream.
Arguments:
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
class if using custom plugins.
"""
try:
# Import the given module that contains the plugin.
if not module:
module = sleekxmpp.plugins
module = __import__("%s.%s" % (module.__name__, plugin),
globals(), locals(), [plugin])
if isinstance(module, str):
# We probably want to load a module from outside
# the sleekxmpp package, so leave out the globals().
module = __import__(module, fromlist=[plugin])
# Load the plugin class from the module.
self.plugin[plugin] = getattr(module, plugin)(self, pconfig)
# Let XEP implementing plugins have some extra logging info.
xep = ''
if hasattr(self.plugin[plugin], 'xep'):
xep = "(XEP-%s) " % self.plugin[plugin].xep
desc = (xep, self.plugin[plugin].description)
log.debug("Loaded Plugin %s%s" % desc)
except:
log.exception("Unable to load plugin: %s", plugin)
def register_plugins(self):
"""
Register and initialize all built-in plugins.
Optionally, the list of plugins loaded may be limited to those
contained in self.plugin_whitelist.
Plugin configurations stored in self.plugin_config will be used.
"""
if self.plugin_whitelist:
plugin_list = self.plugin_whitelist
else:
plugin_list = plugins.__all__
for plugin in plugin_list:
if plugin in plugins.__all__:
self.register_plugin(plugin,
self.plugin_config.get(plugin, {}))
else:
raise NameError("Plugin %s not in plugins.__all__." % plugin)
# Resolve plugin inter-dependencies.
for plugin in self.plugin:
self.plugin[plugin].post_init()
def __getitem__(self, key):
"""
Return a plugin given its name, if it has been registered.
"""
if key in self.plugin:
return self.plugin[key]
else:
log.warning("""Plugin "%s" is not loaded.""" % key)
return False
def get(self, key, default):
"""
Return a plugin given its name, if it has been registered.
"""
return self.plugin.get(key, default)
def Message(self, *args, **kwargs):
"""Create a Message stanza associated with this stream."""
return Message(self, *args, **kwargs)
def Iq(self, *args, **kwargs):
"""Create an Iq stanza associated with this stream."""
return Iq(self, *args, **kwargs)
def Presence(self, *args, **kwargs):
"""Create a Presence stanza associated with this stream."""
return Presence(self, *args, **kwargs)
def make_iq(self, id=0, ifrom=None):
"""
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.
"""
return self.Iq()._set_stanza_values({'id': str(id),
'from': ifrom})
def make_iq_get(self, queryxmlns=None):
"""
Create an Iq stanza of type 'get'.
Optionally, a query element may be added.
Arguments:
queryxmlns -- The namespace of the query to use.
"""
return self.Iq()._set_stanza_values({'type': 'get',
'query': queryxmlns})
def make_iq_result(self, id):
"""
Create an Iq stanza of type 'result' with the given ID value.
Arguments:
id -- An ideally unique ID value. May use self.new_id().
"""
return self.Iq()._set_stanza_values({'id': id,
'type': 'result'})
def make_iq_set(self, sub=None):
"""
Create an 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.
"""
iq = self.Iq()._set_stanza_values({'type': 'set'})
if sub != None:
iq.append(sub)
return iq
def make_iq_error(self, id, type='cancel',
condition='feature-not-implemented', text=None):
"""
Create an 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.
"""
iq = self.Iq()._set_stanza_values({'id': id})
iq['error']._set_stanza_values({'type': type,
'condition': condition,
'text': text})
return iq
def make_iq_query(self, iq=None, xmlns=''):
"""
Create or modify an 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.
"""
if not iq:
iq = self.Iq()
iq['query'] = xmlns
return iq
def make_query_roster(self, iq=None):
"""
Create a roster query element.
Arguments:
iq -- Optional Iq stanza to modify. A new stanza
is created otherwise.
"""
if iq:
iq['query'] = 'jabber:iq:roster'
return ET.Element("{jabber:iq:roster}query")
def make_message(self, mto, mbody=None, msubject=None, mtype=None,
mhtml=None, mfrom=None, mnick=None):
"""
Create and initialize a new 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.
"""
message = self.Message(sto=mto, stype=mtype, sfrom=mfrom)
message['body'] = mbody
message['subject'] = msubject
if mnick is not None:
message['nick'] = mnick
if mhtml is not None:
message['html']['body'] = mhtml
return message
def make_presence(self, pshow=None, pstatus=None, ppriority=None,
pto=None, ptype=None, pfrom=None):
"""
Create and initialize a new 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.
"""
presence = self.Presence(stype=ptype, sfrom=pfrom, sto=pto)
if pshow is not None:
presence['type'] = pshow
if pfrom is None:
presence['from'] = self.boundjid.full
presence['priority'] = ppriority
presence['status'] = pstatus
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.
"""
self.makeMessage(mto, mbody, msubject, mtype,
mhtml, mfrom, mnick).send()
def send_presence(self, pshow=None, pstatus=None, ppriority=None,
pto=None, pfrom=None, ptype=None):
"""
Create, initialize, and send a Presence stanza.
Arguments:
pshow -- The presence's show value.
pstatus -- The presence's status message.
ppriority -- This connections' priority.
pto -- The recipient of a directed presence.
ptype -- The type of presence, such as 'subscribe'.
pfrom -- The sender of the presence.
"""
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
def send_presence_subscription(self, pto, pfrom=None,
ptype='subscribe', pnick=None):
"""
Create, initialize, and send a 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.
"""
presence = self.makePresence(ptype=ptype,
pfrom=pfrom,
pto=self.getjidbare(pto))
if pnick:
nick = ET.Element('{http://jabber.org/protocol/nick}nick')
nick.text = pnick
presence.append(nick)
presence.send()
@property
def jid(self):
"""
Attribute accessor for bare jid
"""
log.warning("jid property deprecated. Use boundjid.bare")
return self.boundjid.bare
@jid.setter
def jid(self, value):
log.warning("jid property deprecated. Use boundjid.bare")
self.boundjid.bare = value
@property
def fulljid(self):
"""
Attribute accessor for full jid
"""
log.warning("fulljid property deprecated. Use boundjid.full")
return self.boundjid.full
@fulljid.setter
def fulljid(self, value):
log.warning("fulljid property deprecated. Use boundjid.full")
self.boundjid.full = value
@property
def resource(self):
"""
Attribute accessor for jid resource
"""
log.warning("resource property deprecated. Use boundjid.resource")
return self.boundjid.resource
@resource.setter
def resource(self, value):
log.warning("fulljid property deprecated. Use boundjid.full")
self.boundjid.resource = value
@property
def username(self):
"""
Attribute accessor for jid usernode
"""
log.warning("username property deprecated. Use boundjid.user")
return self.boundjid.user
@username.setter
def username(self, value):
log.warning("username property deprecated. Use boundjid.user")
self.boundjid.user = value
@property
def server(self):
"""
Attribute accessor for jid host
"""
log.warning("server property deprecated. Use boundjid.host")
return self.boundjid.server
@server.setter
def server(self, value):
log.warning("server property deprecated. Use boundjid.host")
self.boundjid.server = value
def set_jid(self, jid):
"""Rip a JID apart and claim it as our own."""
log.debug("setting jid to %s" % jid)
self.boundjid.full = jid
def getjidresource(self, fulljid):
if '/' in fulljid:
return fulljid.split('/', 1)[-1]
else:
return ''
def getjidbare(self, fulljid):
return fulljid.split('/', 1)[0]
def _handle_disconnected(self, event):
"""When disconnected, reset the roster"""
self.roster = {}
def _handle_message(self, msg):
"""Process incoming message stanzas."""
self.event('message', msg)
def _handle_presence(self, presence):
"""
Process incoming presence stanzas.
Update the roster with presence information.
"""
self.event("presence_%s" % presence['type'], presence)
# Check for changes in subscription state.
if presence['type'] in ('subscribe', 'subscribed',
'unsubscribe', 'unsubscribed'):
self.event('changed_subscription', presence)
return
elif not presence['type'] in ('available', 'unavailable') and \
not presence['type'] in presence.showtypes:
return
# Strip the information from the stanza.
jid = presence['from'].bare
resource = presence['from'].resource
show = presence['type']
status = presence['status']
priority = presence['priority']
was_offline = False
got_online = False
old_roster = self.roster.get(jid, {}).get(resource, {})
# Create a new roster entry if needed.
if not jid in self.roster:
self.roster[jid] = {'groups': [],
'name': '',
'subscription': 'none',
'presence': {},
'in_roster': False}
# Alias to simplify some references.
connections = self.roster[jid]['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]['in_roster']:
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):
"""
Automatically managage subscription requests.
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

436
sleekxmpp/clientxmpp.py Normal file
View File

@@ -0,0 +1,436 @@
"""
SleekXMPP: The Sleek XMPP Library
Copyright (C) 2010 Nathanael C. Fritz
This file is part of SleekXMPP.
See the file LICENSE for copying permission.
"""
from __future__ import absolute_import, unicode_literals
import logging
import base64
import sys
import hashlib
import random
import threading
from sleekxmpp import plugins
from sleekxmpp import stanza
from sleekxmpp.basexmpp import BaseXMPP
from sleekxmpp.stanza import Message, Presence, Iq
from sleekxmpp.xmlstream import XMLStream, RestartStream
from sleekxmpp.xmlstream import StanzaBase, ET
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
log = logging.getLogger(__name__)
class ClientXMPP(BaseXMPP):
"""
SleekXMPP's client class.
Use only for good, not for evil.
Attributes:
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.
"""
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')
# To comply with PEP8, method names now use underscores.
# Deprecated method names are re-mapped for backwards compatibility.
self.updateRoster = self.update_roster
self.delRosterItem = self.del_roster_item
self.getRoster = self.get_roster
self.registerFeature = self.register_feature
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.session_started_event = threading.Event()
self.session_started_event.clear()
self.stream_header = "<stream:stream to='%s' %s %s version='1.0'>" % (
self.boundjid.host,
"xmlns:stream='%s'" % self.stream_ns,
"xmlns='%s'" % self.default_ns)
self.stream_footer = "</stream:stream>"
self.features = []
self.registered_features = []
#TODO: Use stream state here
self.authenticated = False
self.sessionstarted = False
self.bound = False
self.bindfail = False
self.add_event_handler('connected', self.handle_connected)
self.register_handler(
Callback('Stream Features',
MatchXPath('{%s}features' % self.stream_ns),
self._handle_stream_features))
self.register_handler(
Callback('Roster Update',
MatchXPath('{%s}iq/{%s}query' % (
self.default_ns,
'jabber:iq:roster')),
self._handle_roster))
self.register_feature(
"<starttls xmlns='urn:ietf:params:xml:ns:xmpp-tls' />",
self._handle_starttls, True)
self.register_feature(
"<mechanisms xmlns='urn:ietf:params:xml:ns:xmpp-sasl' />",
self._handle_sasl_auth, True)
self.register_feature(
"<bind xmlns='urn:ietf:params:xml:ns:xmpp-bind' />",
self._handle_bind_resource)
self.register_feature(
"<session xmlns='urn:ietf:params:xml:ns:xmpp-session' />",
self._handle_start_session)
def handle_connected(self, event=None):
#TODO: Use stream state here
self.authenticated = False
self.sessionstarted = False
self.bound = False
self.bindfail = False
self.schedule("session timeout checker", 15,
self._session_timeout_check)
def _session_timeout_check(self):
if not self.session_started_event.isSet():
log.debug("Session start has taken more than 15 seconds")
self.disconnect(reconnect=self.auto_reconnect)
def connect(self, address=tuple()):
"""
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.
"""
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.server
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.")
else:
# Pick a random server, weighted by priority.
addresses = {}
intmax = 0
for answer in answers:
intmax += answer.priority
addresses[intmax] = (answer.target.to_text()[:-1],
answer.port)
#python3 returns a generator for dictionary keys
priorities = [x for x in addresses.keys()]
priorities.sort()
picked = random.randint(0, intmax)
for priority in priorities:
if picked <= priority:
address = addresses[priority]
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=True)
def register_feature(self, mask, pointer, breaker=False):
"""
Register a stream feature.
Arguments:
mask -- An XML string matching the feature's element.
pointer -- The function to execute if the feature is received.
breaker -- Indicates if feature processing should halt with
this feature. Defaults to False.
"""
self.registered_features.append((MatchXMLMask(mask),
pointer,
breaker))
def update_roster(self, jid, name=None, subscription=None, groups=[]):
"""
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.
"""
iq = self.Iq()._set_stanza_values({'type': 'set'})
iq['roster']['items'] = {jid: {'name': name,
'subscription': subscription,
'groups': groups}}
response = iq.send()
return response['type'] == 'result'
def del_roster_item(self, jid):
"""
Remove an item from the roster by setting its subscription
status to 'remove'.
Arguments:
jid -- The JID of the item to remove.
"""
return self.update_roster(jid, subscription='remove')
def get_roster(self):
"""Request the roster from the server."""
iq = self.Iq()._set_stanza_values({'type': 'get'}).enable('roster')
response = iq.send()
self._handle_roster(response, request=True)
def _handle_stream_features(self, features):
"""
Process the received stream features.
Arguments:
features -- The features stanza.
"""
# Record all of the features.
self.features = []
for sub in features.xml:
self.features.append(sub.tag)
# Process the features.
for sub in features.xml:
for feature in self.registered_features:
mask, handler, halt = feature
if mask.match(sub):
if handler(sub) and halt:
# Don't continue if the feature was
# marked as a breaker.
return True
def _handle_starttls(self, xml):
"""
Handle notification that the server supports TLS.
Arguments:
xml -- The STARTLS proceed element.
"""
if not self.authenticated and self.ssl_support:
tls_ns = 'urn:ietf:params:xml:ns:xmpp-tls'
self.add_handler("<proceed xmlns='%s' />" % tls_ns,
self._handle_tls_start,
name='TLS Proceed',
instream=True)
self.send_xml(xml)
return True
else:
log.warning("The module tlslite is required to log in" +\
" to some servers, and has not been found.")
return False
def _handle_tls_start(self, xml):
"""
Handle encrypting the stream using TLS.
Restarts the stream.
"""
log.debug("Starting TLS")
if self.start_tls():
raise RestartStream()
def _handle_sasl_auth(self, xml):
"""
Handle authenticating using SASL.
Arguments:
xml -- The SASL mechanisms stanza.
"""
if '{urn:ietf:params:xml:ns:xmpp-tls}starttls' in self.features:
return False
log.debug("Starting SASL Auth")
sasl_ns = 'urn:ietf:params:xml:ns:xmpp-sasl'
self.add_handler("<success xmlns='%s' />" % sasl_ns,
self._handle_auth_success,
name='SASL Sucess',
instream=True)
self.add_handler("<failure xmlns='%s' />" % sasl_ns,
self._handle_auth_fail,
name='SASL Failure',
instream=True)
sasl_mechs = xml.findall('{%s}mechanism' % sasl_ns)
if sasl_mechs:
for sasl_mech in sasl_mechs:
self.features.append("sasl:%s" % sasl_mech.text)
if 'sasl:PLAIN' in self.features and self.boundjid.user:
if sys.version_info < (3, 0):
user = bytes(self.boundjid.user)
password = bytes(self.password)
else:
user = bytes(self.boundjid.user, 'utf-8')
password = bytes(self.password, 'utf-8')
auth = base64.b64encode(b'\x00' + user + \
b'\x00' + password).decode('utf-8')
self.send("<auth xmlns='%s' mechanism='PLAIN'>%s</auth>" % (
sasl_ns,
auth))
elif 'sasl:ANONYMOUS' in self.features and not self.boundjid.user:
self.send("<auth xmlns='%s' mechanism='%s' />" % (
sasl_ns,
'ANONYMOUS'))
else:
log.error("No appropriate login method.")
self.disconnect()
return True
def _handle_auth_success(self, xml):
"""
SASL authentication succeeded. Restart the stream.
Arguments:
xml -- The SASL authentication success element.
"""
self.authenticated = True
self.features = []
raise RestartStream()
def _handle_auth_fail(self, xml):
"""
SASL authentication failed. Disconnect and shutdown.
Arguments:
xml -- The SASL authentication failure element.
"""
log.info("Authentication failed.")
self.event("failed_auth", direct=True)
self.disconnect()
def _handle_bind_resource(self, xml):
"""
Handle requesting a specific resource.
Arguments:
xml -- The bind feature element.
"""
log.debug("Requesting resource: %s" % self.boundjid.resource)
xml.clear()
iq = self.Iq(stype='set')
if self.boundjid.resource:
res = ET.Element('resource')
res.text = self.boundjid.resource
xml.append(res)
iq.append(xml)
response = iq.send()
bind_ns = 'urn:ietf:params:xml:ns:xmpp-bind'
self.set_jid(response.xml.find('{%s}bind/{%s}jid' % (bind_ns,
bind_ns)).text)
self.bound = True
log.info("Node set to: %s" % self.boundjid.fulljid)
session_ns = 'urn:ietf:params:xml:ns:xmpp-session'
if "{%s}session" % session_ns not in self.features or self.bindfail:
log.debug("Established Session")
self.sessionstarted = True
self.session_started_event.set()
self.event("session_start")
def _handle_start_session(self, xml):
"""
Handle the start of the session.
Arguments:
xml -- The session feature element.
"""
if self.authenticated and self.bound:
iq = self.makeIqSet(xml)
response = iq.send()
log.debug("Established Session")
self.sessionstarted = True
self.session_started_event.set()
self.event("session_start")
else:
# Bind probably hasn't happened yet.
self.bindfail = True
def _handle_roster(self, iq, request=False):
"""
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.
"""
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])
self.event("roster_update", iq)
if iq['type'] == 'set':
iq.reply()
iq.enable('roster')
iq.send()

View File

@@ -1,41 +0,0 @@
import sleekxmpp.componentxmpp
import logging
from optparse import OptionParser
import time
class Example(sleekxmpp.componentxmpp.ComponentXMPP):
def __init__(self, jid, password):
sleekxmpp.componentxmpp.ComponentXMPP.__init__(self, jid, password, 'vm1', 5230)
self.add_event_handler("session_start", self.start)
self.add_event_handler("message", self.message)
def start(self, event):
#self.getRoster()
#self.sendPresence(pto='admin@tigase.netflint.net/sarkozy')
#self.sendPresence(pto='tigase.netflint.net')
pass
def message(self, event):
self.sendMessage("%s/%s" % (event['jid'], event['resource']), "Thanks for sending me, \"%s\"." % event['message'], mtype=event['type'])
if __name__ == '__main__':
#parse command line arguements
optp = OptionParser()
optp.add_option('-q','--quiet', help='set logging to ERROR', action='store_const', dest='loglevel', const=logging.ERROR, default=logging.INFO)
optp.add_option('-d','--debug', help='set logging to DEBUG', action='store_const', dest='loglevel', const=logging.DEBUG, default=logging.INFO)
optp.add_option('-v','--verbose', help='set logging to COMM', action='store_const', dest='loglevel', const=5, default=logging.INFO)
optp.add_option("-c","--config", dest="configfile", default="config.xml", help="set config file to use")
opts,args = optp.parse_args()
logging.basicConfig(level=opts.loglevel, format='%(levelname)-8s %(message)s')
xmpp = Example('component.vm1', 'secreteating')
xmpp.registerPlugin('xep_0004')
xmpp.registerPlugin('xep_0030')
xmpp.registerPlugin('xep_0060')
xmpp.registerPlugin('xep_0199')
if xmpp.connect():
xmpp.process(threaded=False)
print("done")
else:
print("Unable to connect.")

199
sleekxmpp/componentxmpp.py Executable file → Normal file
View File

@@ -1,88 +1,141 @@
#!/usr/bin/python2.6
"""
SleekXMPP: The Sleek XMPP Library
Copyright (C) 2010 Nathanael C. Fritz
This file is part of SleekXMPP.
See the file license.txt for copying permission.
See the file LICENSE for copying permission.
"""
from __future__ import absolute_import
from . basexmpp import basexmpp
from xml.etree import cElementTree as ET
from . xmlstream.xmlstream import XMLStream
from . xmlstream.xmlstream import RestartStream
from . xmlstream.matcher.xmlmask import MatchXMLMask
from . xmlstream.matcher.xpath import MatchXPath
from . xmlstream.matcher.many import MatchMany
from . xmlstream.handler.callback import Callback
from . xmlstream.stanzabase import StanzaBase
from . xmlstream import xmlstream as xmlstreammod
import time
from __future__ import absolute_import
import logging
import base64
import sys
import random
import copy
from . import plugins
from . import stanza
import hashlib
srvsupport = True
try:
import dns.resolver
except ImportError:
srvsupport = False
from sleekxmpp import plugins
from sleekxmpp import stanza
from sleekxmpp.basexmpp import BaseXMPP
from sleekxmpp.xmlstream import XMLStream, RestartStream
from sleekxmpp.xmlstream import StanzaBase, ET
from sleekxmpp.xmlstream.matcher import *
from sleekxmpp.xmlstream.handler import *
class ComponentXMPP(basexmpp, XMLStream):
"""SleekXMPP's client class. Use only for good, not evil."""
log = logging.getLogger(__name__)
def __init__(self, jid, secret, host, port, plugin_config = {}, plugin_whitelist=[], use_jc_ns=False):
XMLStream.__init__(self)
if use_jc_ns:
self.default_ns = 'jabber:client'
else:
self.default_ns = 'jabber:component:accept'
basexmpp.__init__(self)
self.auto_authorize = None
self.stream_header = "<stream:stream xmlns='jabber:component:accept' xmlns:stream='http://etherx.jabber.org/streams' to='%s'>" % jid
self.stream_footer = "</stream:stream>"
self.server_host = host
self.server_port = port
self.set_jid(jid)
self.secret = secret
self.registerHandler(Callback('Handshake', MatchXPath('{jabber:component:accept}handshake'), self._handleHandshake))
def __getitem__(self, key):
if key in self.plugin:
return self.plugin[key]
else:
logging.warning("""Plugin "%s" is not loaded.""" % key)
return False
def get(self, key, default):
return self.plugin.get(key, default)
def incoming_filter(self, xmlobj):
if xmlobj.tag.startswith('{jabber:client}'):
xmlobj.tag = xmlobj.tag.replace('jabber:client', self.default_ns)
for sub in xmlobj:
self.incoming_filter(sub)
return xmlobj
def start_stream_handler(self, xml):
sid = xml.get('id', '')
handshake = ET.Element('{jabber:component:accept}handshake')
if sys.version_info < (3,0):
handshake.text = hashlib.sha1("%s%s" % (sid, self.secret)).hexdigest().lower()
else:
handshake.text = hashlib.sha1(bytes("%s%s" % (sid, self.secret), 'utf-8')).hexdigest().lower()
self.sendXML(handshake)
def _handleHandshake(self, xml):
self.event("session_start")
def connect(self):
logging.debug("Connecting to %s:%s" % (self.server_host, self.server_port))
return xmlstreammod.XMLStream.connect(self, self.server_host, self.server_port)
class ComponentXMPP(BaseXMPP):
"""
SleekXMPP's basic XMPP server component.
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.
"""
def __init__(self, jid, secret, host, port,
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)
self.auto_authorize = None
self.stream_header = "<stream:stream %s %s to='%s'>" % (
'xmlns="jabber:component:accept"',
'xmlns:stream="%s"' % self.stream_ns,
jid)
self.stream_footer = "</stream:stream>"
self.server_host = host
self.server_port = port
self.set_jid(jid)
self.secret = secret
self.plugin_config = plugin_config
self.plugin_whitelist = plugin_whitelist
self.is_component = True
self.register_handler(
Callback('Handshake',
MatchXPath('{jabber:component:accept}handshake'),
self._handle_handshake))
def connect(self):
"""
Connect to the server.
Overrides XMLStream.connect.
"""
log.debug("Connecting to %s:%s" % (self.server_host,
self.server_port))
return XMLStream.connect(self, self.server_host,
self.server_port)
def incoming_filter(self, xml):
"""
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.
"""
if xml.tag.startswith('{jabber:client}'):
xml.tag = xml.tag.replace('jabber:client', self.default_ns)
# The incoming_filter call is only made on top level stanza
# elements. So we manually continue filtering on sub-elements.
for sub in xml:
self.incoming_filter(sub)
return xml
def start_stream_handler(self, xml):
"""
Once the streams are established, attempt to handshake
with the server to be accepted as a component.
Overrides XMLStream.start_stream_handler.
Arguments:
xml -- The incoming stream's root element.
"""
# Construct a hash of the stream ID and the component secret.
sid = xml.get('id', '')
pre_hash = '%s%s' % (sid, self.secret)
if sys.version_info >= (3, 0):
# Handle Unicode byte encoding in Python 3.
pre_hash = bytes(pre_hash, 'utf-8')
handshake = ET.Element('{jabber:component:accept}handshake')
handshake.text = hashlib.sha1(pre_hash).hexdigest().lower()
self.send_xml(handshake)
def _handle_handshake(self, xml):
"""
The handshake has been accepted.
Arguments:
xml -- The reply handshake stanza.
"""
self.event("session_start")

View File

@@ -3,14 +3,47 @@
Copyright (C) 2010 Nathanael C. Fritz
This file is part of SleekXMPP.
See the file license.txt for copying permission.
See the file LICENSE for copying permission.
"""
class XMPPError(Exception):
def __init__(self, condition='undefined-condition', text=None, etype=None, extension=None, extension_ns=None, extension_args=None):
self.condition = condition
self.text = text
self.etype = etype
self.extension = extension
self.extension_ns = extension_ns
self.extension_args = extension_args
"""
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.
Meant for use in SleekXMPP plugins and applications using SleekXMPP.
"""
def __init__(self, condition='undefined-condition', text=None, etype=None,
extension=None, extension_ns=None, extension_args=None):
"""
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.
"""
if extension_args is None:
extension_args = {}
self.condition = condition
self.text = text
self.etype = etype
self.extension = extension
self.extension_ns = extension_ns
self.extension_args = extension_args

View File

@@ -1,20 +1,10 @@
"""
SleekXMPP: The Sleek XMPP Library
Copyright (C) 2007 Nathanael C. Fritz
Copyright (C) 2010 Nathanael C. Fritz
This file is part of SleekXMPP.
SleekXMPP is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
SleekXMPP is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with SleekXMPP; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
See the file LICENSE for copying permission.
"""
__all__ = ['xep_0004', 'xep_0030', 'xep_0045', 'xep_0050', 'xep_0078', 'xep_0092', 'xep_0199', 'gmail_notify', 'xep_0060']
__all__ = ['xep_0004', 'xep_0012', 'xep_0030', 'xep_0033', 'xep_0045',
'xep_0050', 'xep_0085', 'xep_0092', 'xep_0199', 'gmail_notify',
'xep_0060', 'xep_0202']

View File

@@ -1,36 +1,26 @@
"""
SleekXMPP: The Sleek XMPP Library
Copyright (C) 2007 Nathanael C. Fritz
This file is part of SleekXMPP.
SleekXMPP: The Sleek XMPP Library
Copyright (C) 2010 Nathanael C. Fritz
This file is part of SleekXMPP.
SleekXMPP is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
SleekXMPP is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with SleekXMPP; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
See the file LICENSE for copying permission.
"""
class base_plugin(object):
def __init__(self, xmpp, config):
self.xep = 'base'
self.description = 'Base Plugin'
self.xmpp = xmpp
self.config = config
self.post_inited = False
self.enable = config.get('enable', True)
if self.enable:
self.plugin_init()
def plugin_init(self):
pass
def post_init(self):
self.post_inited = True
def __init__(self, xmpp, config):
self.xep = 'base'
self.description = 'Base Plugin'
self.xmpp = xmpp
self.config = config
self.post_inited = False
self.enable = config.get('enable', True)
if self.enable:
self.plugin_init()
def plugin_init(self):
pass
def post_init(self):
self.post_inited = True

View File

@@ -1,57 +1,149 @@
"""
SleekXMPP: The Sleek XMPP Library
Copyright (C) 2007 Nathanael C. Fritz
This file is part of SleekXMPP.
SleekXMPP: The Sleek XMPP Library
Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout
This file is part of SleekXMPP.
SleekXMPP is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
SleekXMPP is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with SleekXMPP; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
See the file LICENSE for copying permission.
"""
from __future__ import with_statement
from . import base
import logging
from xml.etree import cElementTree as ET
import traceback
import time
from . import base
from .. xmlstream.handler.callback import Callback
from .. xmlstream.matcher.xpath import MatchXPath
from .. xmlstream.stanzabase import registerStanzaPlugin, ElementBase, ET, JID
from .. stanza.iq import Iq
log = logging.getLogger(__name__)
class GmailQuery(ElementBase):
namespace = 'google:mail:notify'
name = 'query'
plugin_attrib = 'gmail'
interfaces = set(('newer-than-time', 'newer-than-tid', 'q', 'search'))
def getSearch(self):
return self['q']
def setSearch(self, search):
self['q'] = search
def delSearch(self):
del self['q']
class MailBox(ElementBase):
namespace = 'google:mail:notify'
name = 'mailbox'
plugin_attrib = 'mailbox'
interfaces = set(('result-time', 'total-matched', 'total-estimate',
'url', 'threads', 'matched', 'estimate'))
def getThreads(self):
threads = []
for threadXML in self.xml.findall('{%s}%s' % (MailThread.namespace,
MailThread.name)):
threads.append(MailThread(xml=threadXML, parent=None))
return threads
def getMatched(self):
return self['total-matched']
def getEstimate(self):
return self['total-estimate'] == '1'
class MailThread(ElementBase):
namespace = 'google:mail:notify'
name = 'mail-thread-info'
plugin_attrib = 'thread'
interfaces = set(('tid', 'participation', 'messages', 'date',
'senders', 'url', 'labels', 'subject', 'snippet'))
sub_interfaces = set(('labels', 'subject', 'snippet'))
def getSenders(self):
senders = []
sendersXML = self.xml.find('{%s}senders' % self.namespace)
if sendersXML is not None:
for senderXML in sendersXML.findall('{%s}sender' % self.namespace):
senders.append(MailSender(xml=senderXML, parent=None))
return senders
class MailSender(ElementBase):
namespace = 'google:mail:notify'
name = 'sender'
plugin_attrib = 'sender'
interfaces = set(('address', 'name', 'originator', 'unread'))
def getOriginator(self):
return self.xml.attrib.get('originator', '0') == '1'
def getUnread(self):
return self.xml.attrib.get('unread', '0') == '1'
class NewMail(ElementBase):
namespace = 'google:mail:notify'
name = 'new-mail'
plugin_attrib = 'new-mail'
class gmail_notify(base.base_plugin):
def plugin_init(self):
self.description = 'Google Talk Gmail Notification'
self.xmpp.add_event_handler('sent_presence', self.handler_gmailcheck, threaded=True)
self.emails = []
def handler_gmailcheck(self, payload):
#TODO XEP 30 should cache results and have getFeature
result = self.xmpp['xep_0030'].getInfo(self.xmpp.server)
features = []
for feature in result.findall('{http://jabber.org/protocol/disco#info}query/{http://jabber.org/protocol/disco#info}feature'):
features.append(feature.get('var'))
if 'google:mail:notify' in features:
logging.debug("Server supports Gmail Notify")
self.xmpp.add_handler("<iq type='set' xmlns='%s'><new-mail xmlns='google:mail:notify' /></iq>" % self.xmpp.default_ns, self.handler_notify)
self.getEmail()
def handler_notify(self, xml):
logging.info("New Gmail recieved!")
self.xmpp.event('gmail_notify')
def getEmail(self):
iq = self.xmpp.makeIqGet()
iq.attrib['from'] = self.xmpp.fulljid
iq.attrib['to'] = self.xmpp.jid
self.xmpp.makeIqQuery(iq, 'google:mail:notify')
emails = iq.send()
mailbox = emails.find('{google:mail:notify}mailbox')
total = int(mailbox.get('total-matched', 0))
logging.info("%s New Gmail Messages" % total)
"""
Google Talk: Gmail Notifications
"""
def plugin_init(self):
self.description = 'Google Talk: Gmail Notifications'
self.xmpp.registerHandler(
Callback('Gmail Result',
MatchXPath('{%s}iq/{%s}%s' % (self.xmpp.default_ns,
MailBox.namespace,
MailBox.name)),
self.handle_gmail))
self.xmpp.registerHandler(
Callback('Gmail New Mail',
MatchXPath('{%s}iq/{%s}%s' % (self.xmpp.default_ns,
NewMail.namespace,
NewMail.name)),
self.handle_new_mail))
registerStanzaPlugin(Iq, GmailQuery)
registerStanzaPlugin(Iq, MailBox)
registerStanzaPlugin(Iq, NewMail)
self.last_result_time = None
def handle_gmail(self, iq):
mailbox = iq['mailbox']
approx = ' approximately' if mailbox['estimated'] else ''
log.info('Gmail: Received%s %s emails' % (approx, mailbox['total-matched']))
self.last_result_time = mailbox['result-time']
self.xmpp.event('gmail_messages', iq)
def handle_new_mail(self, iq):
log.info("Gmail: New emails received!")
self.xmpp.event('gmail_notify')
self.checkEmail()
def getEmail(self, query=None):
return self.search(query)
def checkEmail(self):
return self.search(newer=self.last_result_time)
def search(self, query=None, newer=None):
if query is None:
log.info("Gmail: Checking for new emails")
else:
log.info('Gmail: Searching for emails matching: "%s"' % query)
iq = self.xmpp.Iq()
iq['type'] = 'get'
iq['to'] = self.xmpp.jid
iq['gmail']['q'] = query
iq['gmail']['newer-than-time'] = newer
return iq.send()

View File

@@ -1,16 +1,21 @@
from . import base
import logging
from xml.etree import cElementTree as ET
import types
log = logging.getLogger(__name__)
class jobs(base.base_plugin):
def plugin_init(self):
self.xep = 'pubsubjob'
self.description = "Job distribution over Pubsub"
def post_init(self):
pass
#TODO add event
def createJobNode(self, host, jid, node, config=None):
pass
@@ -20,7 +25,7 @@ class jobs(base.base_plugin):
def claimJob(self, host, node, jobid, ifrom=None):
return self._setState(host, node, jobid, ET.Element('{http://andyet.net/protocol/pubsubjob}claimed'))
def unclaimJob(self, jobid):
def unclaimJob(self, host, node, jobid):
return self._setState(host, node, jobid, ET.Element('{http://andyet.net/protocol/pubsubjob}unclaimed'))
def finishJob(self, host, node, jobid, payload=None):
@@ -38,7 +43,8 @@ class jobs(base.base_plugin):
iq['psstate']['item'] = jobid
iq['psstate']['payload'] = state
result = iq.send()
if result is None or result['type'] != 'result':
if result is None or type(result) == types.BooleanType or result['type'] != 'result':
log.error("Unable to change %s:%s to %s" % (node, jobid, state))
return False
return True

View File

@@ -0,0 +1,421 @@
"""
SleekXMPP: The Sleek XMPP Library
Copyright (C) 2010 Nathanael C. Fritz
This file is part of SleekXMPP.
See the file LICENSE for copying permission.
"""
from . import base
import log
from xml.etree import cElementTree as ET
import copy
import logging
#TODO support item groups and results
log = logging.getLogger(__name__)
class old_0004(base.base_plugin):
def plugin_init(self):
self.xep = '0004'
self.description = '*Deprecated Data Forms'
self.xmpp.add_handler("<message><x xmlns='jabber:x:data' /></message>", self.handler_message_xform, name='Old Message Form')
def post_init(self):
base.base_plugin.post_init(self)
self.xmpp.plugin['xep_0030'].add_feature('jabber:x:data')
log.warning("This implementation of XEP-0004 is deprecated.")
def handler_message_xform(self, xml):
object = self.handle_form(xml)
self.xmpp.event("message_form", object)
def handler_presence_xform(self, xml):
object = self.handle_form(xml)
self.xmpp.event("presence_form", object)
def handle_form(self, xml):
xmlform = xml.find('{jabber:x:data}x')
object = self.buildForm(xmlform)
self.xmpp.event("message_xform", object)
return object
def buildForm(self, xml):
form = Form(ftype=xml.attrib['type'])
form.fromXML(xml)
return form
def makeForm(self, ftype='form', title='', instructions=''):
return Form(self.xmpp, ftype, title, instructions)
class FieldContainer(object):
def __init__(self, stanza = 'form'):
self.fields = []
self.field = {}
self.stanza = stanza
def addField(self, var, ftype='text-single', label='', desc='', required=False, value=None):
self.field[var] = FormField(var, ftype, label, desc, required, value)
self.fields.append(self.field[var])
return self.field[var]
def buildField(self, xml):
self.field[xml.get('var', '__unnamed__')] = FormField(xml.get('var', '__unnamed__'), xml.get('type', 'text-single'))
self.fields.append(self.field[xml.get('var', '__unnamed__')])
self.field[xml.get('var', '__unnamed__')].buildField(xml)
def buildContainer(self, xml):
self.stanza = xml.tag
for field in xml.findall('{jabber:x:data}field'):
self.buildField(field)
def getXML(self, ftype):
container = ET.Element(self.stanza)
for field in self.fields:
container.append(field.getXML(ftype))
return container
class Form(FieldContainer):
types = ('form', 'submit', 'cancel', 'result')
def __init__(self, xmpp=None, ftype='form', title='', instructions=''):
if not ftype in self.types:
raise ValueError("Invalid Form Type")
FieldContainer.__init__(self)
self.xmpp = xmpp
self.type = ftype
self.title = title
self.instructions = instructions
self.reported = []
self.items = []
def merge(self, form2):
form1 = Form(ftype=self.type)
form1.fromXML(self.getXML(self.type))
for field in form2.fields:
if not field.var in form1.field:
form1.addField(field.var, field.type, field.label, field.desc, field.required, field.value)
else:
form1.field[field.var].value = field.value
for option, label in field.options:
if (option, label) not in form1.field[field.var].options:
form1.fields[field.var].addOption(option, label)
return form1
def copy(self):
newform = Form(ftype=self.type)
newform.fromXML(self.getXML(self.type))
return newform
def update(self, form):
values = form.getValues()
for var in values:
if var in self.fields:
self.fields[var].setValue(self.fields[var])
def getValues(self):
result = {}
for field in self.fields:
value = field.value
if len(value) == 1:
value = value[0]
result[field.var] = value
return result
def setValues(self, values={}):
for field in values:
if field in self.field:
if isinstance(values[field], list) or isinstance(values[field], tuple):
for value in values[field]:
self.field[field].setValue(value)
else:
self.field[field].setValue(values[field])
def fromXML(self, xml):
self.buildForm(xml)
def addItem(self):
newitem = FieldContainer('item')
self.items.append(newitem)
return newitem
def buildItem(self, xml):
newitem = self.addItem()
newitem.buildContainer(xml)
def addReported(self):
reported = FieldContainer('reported')
self.reported.append(reported)
return reported
def buildReported(self, xml):
reported = self.addReported()
reported.buildContainer(xml)
def setTitle(self, title):
self.title = title
def setInstructions(self, instructions):
self.instructions = instructions
def setType(self, ftype):
self.type = ftype
def getXMLMessage(self, to):
msg = self.xmpp.makeMessage(to)
msg.append(self.getXML())
return msg
def buildForm(self, xml):
self.type = xml.get('type', 'form')
if xml.find('{jabber:x:data}title') is not None:
self.setTitle(xml.find('{jabber:x:data}title').text)
if xml.find('{jabber:x:data}instructions') is not None:
self.setInstructions(xml.find('{jabber:x:data}instructions').text)
for field in xml.findall('{jabber:x:data}field'):
self.buildField(field)
for reported in xml.findall('{jabber:x:data}reported'):
self.buildReported(reported)
for item in xml.findall('{jabber:x:data}item'):
self.buildItem(item)
#def getXML(self, tostring = False):
def getXML(self, ftype=None):
if ftype:
self.type = ftype
form = ET.Element('{jabber:x:data}x')
form.attrib['type'] = self.type
if self.title and self.type in ('form', 'result'):
title = ET.Element('{jabber:x:data}title')
title.text = self.title
form.append(title)
if self.instructions and self.type == 'form':
instructions = ET.Element('{jabber:x:data}instructions')
instructions.text = self.instructions
form.append(instructions)
for field in self.fields:
form.append(field.getXML(self.type))
for reported in self.reported:
form.append(reported.getXML('{jabber:x:data}reported'))
for item in self.items:
form.append(item.getXML(self.type))
#if tostring:
# form = self.xmpp.tostring(form)
return form
def getXHTML(self):
form = ET.Element('{http://www.w3.org/1999/xhtml}form')
if self.title:
title = ET.Element('h2')
title.text = self.title
form.append(title)
if self.instructions:
instructions = ET.Element('p')
instructions.text = self.instructions
form.append(instructions)
for field in self.fields:
form.append(field.getXHTML())
for field in self.reported:
form.append(field.getXHTML())
for field in self.items:
form.append(field.getXHTML())
return form
def makeSubmit(self):
self.setType('submit')
class FormField(object):
types = ('boolean', 'fixed', 'hidden', 'jid-multi', 'jid-single', 'list-multi', 'list-single', 'text-multi', 'text-private', 'text-single')
listtypes = ('jid-multi', 'jid-single', 'list-multi', 'list-single')
lbtypes = ('fixed', 'text-multi')
def __init__(self, var, ftype='text-single', label='', desc='', required=False, value=None):
if not ftype in self.types:
raise ValueError("Invalid Field Type")
self.type = ftype
self.var = var
self.label = label
self.desc = desc
self.options = []
self.required = False
self.value = []
if self.type in self.listtypes:
self.islist = True
else:
self.islist = False
if self.type in self.lbtypes:
self.islinebreak = True
else:
self.islinebreak = False
if value:
self.setValue(value)
def addOption(self, value, label):
if self.islist:
self.options.append((value, label))
else:
raise ValueError("Cannot add options to non-list type field.")
def setTrue(self):
if self.type == 'boolean':
self.value = [True]
def setFalse(self):
if self.type == 'boolean':
self.value = [False]
def require(self):
self.required = True
def setDescription(self, desc):
self.desc = desc
def setValue(self, value):
if self.type == 'boolean':
if value in ('1', 1, True, 'true', 'True', 'yes'):
value = True
else:
value = False
if self.islinebreak and value is not None:
self.value += value.split('\n')
else:
if len(self.value) and (not self.islist or self.type == 'list-single'):
self.value = [value]
else:
self.value.append(value)
def delValue(self, value):
if type(self.value) == type([]):
try:
idx = self.value.index(value)
if idx != -1:
self.value.pop(idx)
except ValueError:
pass
else:
self.value = ''
def setAnswer(self, value):
self.setValue(value)
def buildField(self, xml):
self.type = xml.get('type', 'text-single')
self.label = xml.get('label', '')
for option in xml.findall('{jabber:x:data}option'):
self.addOption(option.find('{jabber:x:data}value').text, option.get('label', ''))
for value in xml.findall('{jabber:x:data}value'):
self.setValue(value.text)
if xml.find('{jabber:x:data}required') is not None:
self.require()
if xml.find('{jabber:x:data}desc') is not None:
self.setDescription(xml.find('{jabber:x:data}desc').text)
def getXML(self, ftype):
field = ET.Element('{jabber:x:data}field')
if ftype != 'result':
field.attrib['type'] = self.type
if self.type != 'fixed':
if self.var:
field.attrib['var'] = self.var
if self.label:
field.attrib['label'] = self.label
if ftype == 'form':
for option in self.options:
optionxml = ET.Element('{jabber:x:data}option')
optionxml.attrib['label'] = option[1]
optionval = ET.Element('{jabber:x:data}value')
optionval.text = option[0]
optionxml.append(optionval)
field.append(optionxml)
if self.required:
required = ET.Element('{jabber:x:data}required')
field.append(required)
if self.desc:
desc = ET.Element('{jabber:x:data}desc')
desc.text = self.desc
field.append(desc)
for value in self.value:
valuexml = ET.Element('{jabber:x:data}value')
if value is True or value is False:
if value:
valuexml.text = '1'
else:
valuexml.text = '0'
else:
valuexml.text = value
field.append(valuexml)
return field
def getXHTML(self):
field = ET.Element('div', {'class': 'xmpp-xforms-%s' % self.type})
if self.label:
label = ET.Element('p')
label.text = "%s: " % self.label
else:
label = ET.Element('p')
label.text = "%s: " % self.var
field.append(label)
if self.type == 'boolean':
formf = ET.Element('input', {'type': 'checkbox', 'name': self.var})
if len(self.value) and self.value[0] in (True, 'true', '1'):
formf.attrib['checked'] = 'checked'
elif self.type == 'fixed':
formf = ET.Element('p')
try:
formf.text = ', '.join(self.value)
except:
pass
field.append(formf)
formf = ET.Element('input', {'type': 'hidden', 'name': self.var})
try:
formf.text = ', '.join(self.value)
except:
pass
elif self.type == 'hidden':
formf = ET.Element('input', {'type': 'hidden', 'name': self.var})
try:
formf.text = ', '.join(self.value)
except:
pass
elif self.type in ('jid-multi', 'list-multi'):
formf = ET.Element('select', {'name': self.var})
for option in self.options:
optf = ET.Element('option', {'value': option[0], 'multiple': 'multiple'})
optf.text = option[1]
if option[1] in self.value:
optf.attrib['selected'] = 'selected'
formf.append(option)
elif self.type in ('jid-single', 'text-single'):
formf = ET.Element('input', {'type': 'text', 'name': self.var})
try:
formf.attrib['value'] = ', '.join(self.value)
except:
pass
elif self.type == 'list-single':
formf = ET.Element('select', {'name': self.var})
for option in self.options:
optf = ET.Element('option', {'value': option[0]})
optf.text = option[1]
if not optf.text:
optf.text = option[0]
if option[1] in self.value:
optf.attrib['selected'] = 'selected'
formf.append(optf)
elif self.type == 'text-multi':
formf = ET.Element('textarea', {'name': self.var})
try:
formf.text = ', '.join(self.value)
except:
pass
if not formf.text:
formf.text = ' '
elif self.type == 'text-private':
formf = ET.Element('input', {'type': 'password', 'name': self.var})
try:
formf.attrib['value'] = ', '.join(self.value)
except:
pass
label.append(formf)
return field

View File

@@ -1,4 +1,4 @@
from .. xmlstream.stanzabase import ElementBase, ET, JID
from .. xmlstream.stanzabase import registerStanzaPlugin, ElementBase, ET, JID
from .. stanza.iq import Iq
from .. stanza.message import Message
from .. basexmpp import basexmpp
@@ -6,9 +6,6 @@ from .. xmlstream.xmlstream import XMLStream
import logging
from . import xep_0004
def stanzaPlugin(stanza, plugin):
stanza.plugin_attrib_map[plugin.plugin_attrib] = plugin
stanza.plugin_tag_map["{%s}%s" % (plugin.namespace, plugin.name)] = plugin
class PubsubState(ElementBase):
namespace = 'http://jabber.org/protocol/psstate'
@@ -30,7 +27,7 @@ class PubsubState(ElementBase):
for child in self.xml.getchildren():
self.xml.remove(child)
stanzaPlugin(Iq, PubsubState)
registerStanzaPlugin(Iq, PubsubState)
class PubsubStateEvent(ElementBase):
namespace = 'http://jabber.org/protocol/psstate#event'
@@ -40,8 +37,8 @@ class PubsubStateEvent(ElementBase):
plugin_attrib_map = {}
plugin_tag_map = {}
stanzaPlugin(Message, PubsubStateEvent)
stanzaPlugin(PubsubStateEvent, PubsubState)
registerStanzaPlugin(Message, PubsubStateEvent)
registerStanzaPlugin(PubsubStateEvent, PubsubState)
class Pubsub(ElementBase):
namespace = 'http://jabber.org/protocol/pubsub'
@@ -51,7 +48,7 @@ class Pubsub(ElementBase):
plugin_attrib_map = {}
plugin_tag_map = {}
stanzaPlugin(Iq, Pubsub)
registerStanzaPlugin(Iq, Pubsub)
class PubsubOwner(ElementBase):
namespace = 'http://jabber.org/protocol/pubsub#owner'
@@ -61,7 +58,7 @@ class PubsubOwner(ElementBase):
plugin_attrib_map = {}
plugin_tag_map = {}
stanzaPlugin(Iq, PubsubOwner)
registerStanzaPlugin(Iq, PubsubOwner)
class Affiliation(ElementBase):
namespace = 'http://jabber.org/protocol/pubsub'
@@ -86,7 +83,7 @@ class Affiliations(ElementBase):
self.xml.append(affiliation.xml)
return self.iterables.append(affiliation)
stanzaPlugin(Pubsub, Affiliations)
registerStanzaPlugin(Pubsub, Affiliations)
class Subscription(ElementBase):
@@ -103,7 +100,7 @@ class Subscription(ElementBase):
def getjid(self):
return jid(self._getattr('jid'))
stanzaPlugin(Pubsub, Subscription)
registerStanzaPlugin(Pubsub, Subscription)
class Subscriptions(ElementBase):
namespace = 'http://jabber.org/protocol/pubsub'
@@ -114,7 +111,7 @@ class Subscriptions(ElementBase):
plugin_tag_map = {}
subitem = (Subscription,)
stanzaPlugin(Pubsub, Subscriptions)
registerStanzaPlugin(Pubsub, Subscriptions)
class OptionalSetting(object):
interfaces = set(('required',))
@@ -147,7 +144,7 @@ class SubscribeOptions(ElementBase, OptionalSetting):
plugin_tag_map = {}
interfaces = set(('required',))
stanzaPlugin(Subscription, SubscribeOptions)
registerStanzaPlugin(Subscription, SubscribeOptions)
class Item(ElementBase):
namespace = 'http://jabber.org/protocol/pubsub'
@@ -173,12 +170,12 @@ class Items(ElementBase):
namespace = 'http://jabber.org/protocol/pubsub'
name = 'items'
plugin_attrib = 'items'
interfaces = set(tuple())
interfaces = set(('node',))
plugin_attrib_map = {}
plugin_tag_map = {}
subitem = (Item,)
stanzaPlugin(Pubsub, Items)
registerStanzaPlugin(Pubsub, Items)
class Create(ElementBase):
namespace = 'http://jabber.org/protocol/pubsub'
@@ -188,7 +185,7 @@ class Create(ElementBase):
plugin_attrib_map = {}
plugin_tag_map = {}
stanzaPlugin(Pubsub, Create)
registerStanzaPlugin(Pubsub, Create)
#class Default(ElementBase):
# namespace = 'http://jabber.org/protocol/pubsub'
@@ -203,7 +200,7 @@ stanzaPlugin(Pubsub, Create)
# if not t: t == 'leaf'
# return t
#
#stanzaPlugin(Pubsub, Default)
#registerStanzaPlugin(Pubsub, Default)
class Publish(Items):
namespace = 'http://jabber.org/protocol/pubsub'
@@ -214,7 +211,7 @@ class Publish(Items):
plugin_tag_map = {}
subitem = (Item,)
stanzaPlugin(Pubsub, Publish)
registerStanzaPlugin(Pubsub, Publish)
class Retract(Items):
namespace = 'http://jabber.org/protocol/pubsub'
@@ -224,7 +221,7 @@ class Retract(Items):
plugin_attrib_map = {}
plugin_tag_map = {}
stanzaPlugin(Pubsub, Retract)
registerStanzaPlugin(Pubsub, Retract)
class Unsubscribe(ElementBase):
namespace = 'http://jabber.org/protocol/pubsub'
@@ -254,13 +251,13 @@ class Subscribe(ElementBase):
def getJid(self):
return JID(self._getAttr('jid'))
stanzaPlugin(Pubsub, Subscribe)
registerStanzaPlugin(Pubsub, Subscribe)
class Configure(ElementBase):
namespace = 'http://jabber.org/protocol/pubsub'
name = 'configure'
plugin_attrib = name
interfaces = set(('node', 'type', 'config'))
interfaces = set(('node', 'type'))
plugin_attrib_map = {}
plugin_tag_map = {}
@@ -269,22 +266,8 @@ class Configure(ElementBase):
if not t: t == 'leaf'
return t
def getConfig(self):
config = self.xml.find('{jabber:x:data}x')
form = xep_0004.Form()
if config is not None:
form.fromXML(config)
return form
def setConfig(self, value):
self.xml.append(value.getXML())
return self
def delConfig(self):
config = self.xml.find('{jabber:x:data}x')
self.xml.remove(config)
stanzaPlugin(Pubsub, Configure)
registerStanzaPlugin(Pubsub, Configure)
registerStanzaPlugin(Configure, xep_0004.Form)
class DefaultConfig(ElementBase):
namespace = 'http://jabber.org/protocol/pubsub#owner'
@@ -296,28 +279,21 @@ class DefaultConfig(ElementBase):
def __init__(self, *args, **kwargs):
ElementBase.__init__(self, *args, **kwargs)
def getConfig(self):
config = self.xml.find('{jabber:x:data}x')
form = xep_0004.Form()
if config is not None:
form.fromXML(config)
return form
def setConfig(self, value):
self.xml.append(value.getXML())
return self
def delConfig(self):
config = self.xml.find('{jabber:x:data}x')
self.xml.remove(config)
def getType(self):
t = self._getAttr('type')
if not t: t = 'leaf'
return t
def getConfig(self):
return self['form']
def setConfig(self, value):
self['form'].setStanzaValues(value.getStanzaValues())
return self
stanzaPlugin(PubsubOwner, DefaultConfig)
registerStanzaPlugin(PubsubOwner, DefaultConfig)
registerStanzaPlugin(DefaultConfig, xep_0004.Form)
class Options(ElementBase):
namespace = 'http://jabber.org/protocol/pubsub'
@@ -351,8 +327,8 @@ class Options(ElementBase):
def getJid(self):
return JID(self._getAttr('jid'))
stanzaPlugin(Pubsub, Options)
stanzaPlugin(Subscribe, Options)
registerStanzaPlugin(Pubsub, Options)
registerStanzaPlugin(Subscribe, Options)
class OwnerAffiliations(Affiliations):
namespace = 'http://jabber.org/protocol/pubsub#owner'
@@ -366,7 +342,7 @@ class OwnerAffiliations(Affiliations):
self.xml.append(affiliation.xml)
return self.affiliations.append(affiliation)
stanzaPlugin(PubsubOwner, OwnerAffiliations)
registerStanzaPlugin(PubsubOwner, OwnerAffiliations)
class OwnerAffiliation(Affiliation):
namespace = 'http://jabber.org/protocol/pubsub#owner'
@@ -380,15 +356,23 @@ class OwnerConfigure(Configure):
plugin_attrib_map = {}
plugin_tag_map = {}
stanzaPlugin(PubsubOwner, OwnerConfigure)
registerStanzaPlugin(PubsubOwner, OwnerConfigure)
class OwnerDefault(OwnerConfigure):
namespace = 'http://jabber.org/protocol/pubsub#owner'
interfaces = set(('node', 'config'))
plugin_attrib_map = {}
plugin_tag_map = {}
def getConfig(self):
return self['form']
def setConfig(self, value):
self['form'].setStanzaValues(value.getStanzaValues())
return self
stanzaPlugin(PubsubOwner, OwnerDefault)
registerStanzaPlugin(PubsubOwner, OwnerDefault)
registerStanzaPlugin(OwnerDefault, xep_0004.Form)
class OwnerDelete(ElementBase, OptionalSetting):
namespace = 'http://jabber.org/protocol/pubsub#owner'
@@ -398,7 +382,7 @@ class OwnerDelete(ElementBase, OptionalSetting):
plugin_tag_map = {}
interfaces = set(('node',))
stanzaPlugin(PubsubOwner, OwnerDelete)
registerStanzaPlugin(PubsubOwner, OwnerDelete)
class OwnerPurge(ElementBase, OptionalSetting):
namespace = 'http://jabber.org/protocol/pubsub#owner'
@@ -407,7 +391,7 @@ class OwnerPurge(ElementBase, OptionalSetting):
plugin_attrib_map = {}
plugin_tag_map = {}
stanzaPlugin(PubsubOwner, OwnerPurge)
registerStanzaPlugin(PubsubOwner, OwnerPurge)
class OwnerRedirect(ElementBase):
namespace = 'http://jabber.org/protocol/pubsub#owner'
@@ -423,7 +407,7 @@ class OwnerRedirect(ElementBase):
def getJid(self):
return JID(self._getAttr('jid'))
stanzaPlugin(OwnerDelete, OwnerRedirect)
registerStanzaPlugin(OwnerDelete, OwnerRedirect)
class OwnerSubscriptions(Subscriptions):
namespace = 'http://jabber.org/protocol/pubsub#owner'
@@ -437,7 +421,7 @@ class OwnerSubscriptions(Subscriptions):
self.xml.append(subscription.xml)
return self.subscriptions.append(subscription)
stanzaPlugin(PubsubOwner, OwnerSubscriptions)
registerStanzaPlugin(PubsubOwner, OwnerSubscriptions)
class OwnerSubscription(ElementBase):
namespace = 'http://jabber.org/protocol/pubsub#owner'
@@ -461,7 +445,7 @@ class Event(ElementBase):
plugin_attrib_map = {}
plugin_tag_map = {}
stanzaPlugin(Message, Event)
registerStanzaPlugin(Message, Event)
class EventItem(ElementBase):
namespace = 'http://jabber.org/protocol/pubsub#event'
@@ -501,7 +485,7 @@ class EventItems(ElementBase):
plugin_tag_map = {}
subitem = (EventItem, EventRetract)
stanzaPlugin(Event, EventItems)
registerStanzaPlugin(Event, EventItems)
class EventCollection(ElementBase):
namespace = 'http://jabber.org/protocol/pubsub#event'
@@ -511,7 +495,7 @@ class EventCollection(ElementBase):
plugin_attrib_map = {}
plugin_tag_map = {}
stanzaPlugin(Event, EventCollection)
registerStanzaPlugin(Event, EventCollection)
class EventAssociate(ElementBase):
namespace = 'http://jabber.org/protocol/pubsub#event'
@@ -521,7 +505,7 @@ class EventAssociate(ElementBase):
plugin_attrib_map = {}
plugin_tag_map = {}
stanzaPlugin(EventCollection, EventAssociate)
registerStanzaPlugin(EventCollection, EventAssociate)
class EventDisassociate(ElementBase):
namespace = 'http://jabber.org/protocol/pubsub#event'
@@ -531,7 +515,7 @@ class EventDisassociate(ElementBase):
plugin_attrib_map = {}
plugin_tag_map = {}
stanzaPlugin(EventCollection, EventDisassociate)
registerStanzaPlugin(EventCollection, EventDisassociate)
class EventConfiguration(ElementBase):
namespace = 'http://jabber.org/protocol/pubsub#event'
@@ -541,22 +525,8 @@ class EventConfiguration(ElementBase):
plugin_attrib_map = {}
plugin_tag_map = {}
def getConfig(self):
config = self.xml.find('{jabber:x:data}x')
form = xep_0004.Form()
if config is not None:
form.fromXML(config)
return form
def setConfig(self, value):
self.xml.append(value.getXML())
return self
def delConfig(self):
config = self.xml.find('{jabber:x:data}x')
self.xml.remove(config)
stanzaPlugin(Event, EventConfiguration)
registerStanzaPlugin(Event, EventConfiguration)
registerStanzaPlugin(EventConfiguration, xep_0004.Form)
class EventPurge(ElementBase):
namespace = 'http://jabber.org/protocol/pubsub#event'
@@ -566,7 +536,7 @@ class EventPurge(ElementBase):
plugin_attrib_map = {}
plugin_tag_map = {}
stanzaPlugin(Event, EventPurge)
registerStanzaPlugin(Event, EventPurge)
class EventSubscription(ElementBase):
namespace = 'http://jabber.org/protocol/pubsub#event'
@@ -582,4 +552,4 @@ class EventSubscription(ElementBase):
def getJid(self):
return JID(self._getAttr('jid'))
stanzaPlugin(Event, EventSubscription)
registerStanzaPlugin(Event, EventSubscription)

View File

@@ -1,427 +1,395 @@
"""
SleekXMPP: The Sleek XMPP Library
Copyright (C) 2007 Nathanael C. Fritz
Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout
This file is part of SleekXMPP.
SleekXMPP is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
SleekXMPP is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with SleekXMPP; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
See the file LICENSE for copying permission.
"""
from . import base
import logging
from xml.etree import cElementTree as ET
import copy
#TODO support item groups and results
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
import types
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'):
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) == types.DictType:
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.add_handler("<message><x xmlns='jabber:x:data' /></message>", self.handler_message_xform)
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 handler_message_xform(self, xml):
object = self.handle_form(xml)
self.xmpp.event("message_form", object)
def handler_presence_xform(self, xml):
object = self.handle_form(xml)
self.xmpp.event("presence_form", object)
def handle_form(self, xml):
xmlform = xml.find('{jabber:x:data}x')
object = self.buildForm(xmlform)
self.xmpp.event("message_xform", object)
return object
def handle_form(self, message):
self.xmpp.event("message_xform", message)
def buildForm(self, xml):
form = Form(ftype=xml.attrib['type'])
form.fromXML(xml)
return form
def makeForm(self, ftype='form', title='', instructions=''):
return Form(self.xmpp, ftype, title, instructions)
class FieldContainer(object):
def __init__(self, stanza = 'form'):
self.fields = []
self.field = {}
self.stanza = stanza
def addField(self, var, ftype='text-single', label='', desc='', required=False, value=None):
self.field[var] = FormField(var, ftype, label, desc, required, value)
self.fields.append(self.field[var])
return self.field[var]
def buildField(self, xml):
self.field[xml.get('var', '__unnamed__')] = FormField(xml.get('var', '__unnamed__'), xml.get('type', 'text-single'))
self.fields.append(self.field[xml.get('var', '__unnamed__')])
self.field[xml.get('var', '__unnamed__')].buildField(xml)
def buildContainer(self, xml):
self.stanza = xml.tag
for field in xml.findall('{jabber:x:data}field'):
self.buildField(field)
def getXML(self, ftype):
container = ET.Element(self.stanza)
for field in self.fields:
container.append(field.getXML(ftype))
return container
class Form(FieldContainer):
types = ('form', 'submit', 'cancel', 'result')
def __init__(self, xmpp=None, ftype='form', title='', instructions=''):
if not ftype in self.types:
raise ValueError("Invalid Form Type")
FieldContainer.__init__(self)
self.xmpp = xmpp
self.type = ftype
self.title = title
self.instructions = instructions
self.reported = []
self.items = []
def merge(self, form2):
form1 = Form(ftype=self.type)
form1.fromXML(self.getXML(self.type))
for field in form2.fields:
if not field.var in form1.field:
form1.addField(field.var, field.type, field.label, field.desc, field.required, field.value)
else:
form1.field[field.var].value = field.value
for option, label in field.options:
if (option, label) not in form1.field[field.var].options:
form1.fields[field.var].addOption(option, label)
return form1
def copy(self):
newform = Form(ftype=self.type)
newform.fromXML(self.getXML(self.type))
return newform
def update(self, form):
values = form.getValues()
for var in values:
if var in self.fields:
self.fields[var].setValue(self.fields[var])
def getValues(self):
result = {}
for field in self.fields:
value = field.value
if len(value) == 1:
value = value[0]
result[field.var] = value
return result
def setValues(self, values={}):
for field in values:
if field in self.field:
if isinstance(values[field], list) or isinstance(values[field], tuple):
for value in values[field]:
self.field[field].setValue(value)
else:
self.field[field].setValue(values[field])
def fromXML(self, xml):
self.buildForm(xml)
def addItem(self):
newitem = FieldContainer('item')
self.items.append(newitem)
return newitem
def buildItem(self, xml):
newitem = self.addItem()
newitem.buildContainer(xml)
def addReported(self):
reported = FieldContainer('reported')
self.reported.append(reported)
return reported
def buildReported(self, xml):
reported = self.addReported()
reported.buildContainer(xml)
def setTitle(self, title):
self.title = title
def setInstructions(self, instructions):
self.instructions = instructions
def setType(self, ftype):
self.type = ftype
def getXMLMessage(self, to):
msg = self.xmpp.makeMessage(to)
msg.append(self.getXML())
return msg
def buildForm(self, xml):
self.type = xml.get('type', 'form')
if xml.find('{jabber:x:data}title') is not None:
self.setTitle(xml.find('{jabber:x:data}title').text)
if xml.find('{jabber:x:data}instructions') is not None:
self.setInstructions(xml.find('{jabber:x:data}instructions').text)
for field in xml.findall('{jabber:x:data}field'):
self.buildField(field)
for reported in xml.findall('{jabber:x:data}reported'):
self.buildReported(reported)
for item in xml.findall('{jabber:x:data}item'):
self.buildItem(item)
#def getXML(self, tostring = False):
def getXML(self, ftype=None):
if ftype:
self.type = ftype
form = ET.Element('{jabber:x:data}x')
form.attrib['type'] = self.type
if self.title and self.type in ('form', 'result'):
title = ET.Element('{jabber:x:data}title')
title.text = self.title
form.append(title)
if self.instructions and self.type == 'form':
instructions = ET.Element('{jabber:x:data}instructions')
instructions.text = self.instructions
form.append(instructions)
for field in self.fields:
form.append(field.getXML(self.type))
for reported in self.reported:
form.append(reported.getXML('{jabber:x:data}reported'))
for item in self.items:
form.append(item.getXML(self.type))
#if tostring:
# form = self.xmpp.tostring(form)
return form
def getXHTML(self):
form = ET.Element('{http://www.w3.org/1999/xhtml}form')
if self.title:
title = ET.Element('h2')
title.text = self.title
form.append(title)
if self.instructions:
instructions = ET.Element('p')
instructions.text = self.instructions
form.append(instructions)
for field in self.fields:
form.append(field.getXHTML())
for field in self.reported:
form.append(field.getXHTML())
for field in self.items:
form.append(field.getXHTML())
return form
def makeSubmit(self):
self.setType('submit')
class FormField(object):
types = ('boolean', 'fixed', 'hidden', 'jid-multi', 'jid-single', 'list-multi', 'list-single', 'text-multi', 'text-private', 'text-single')
listtypes = ('jid-multi', 'jid-single', 'list-multi', 'list-single')
lbtypes = ('fixed', 'text-multi')
def __init__(self, var, ftype='text-single', label='', desc='', required=False, value=None):
if not ftype in self.types:
raise ValueError("Invalid Field Type")
self.type = ftype
self.var = var
self.label = label
self.desc = desc
self.options = []
self.required = False
self.value = []
if self.type in self.listtypes:
self.islist = True
else:
self.islist = False
if self.type in self.lbtypes:
self.islinebreak = True
else:
self.islinebreak = False
if value:
self.setValue(value)
def addOption(self, value, label):
if self.islist:
self.options.append((value, label))
else:
raise ValueError("Cannot add options to non-list type field.")
def setTrue(self):
if self.type == 'boolean':
self.value = [True]
def setFalse(self):
if self.type == 'boolean':
self.value = [False]
def require(self):
self.required = True
def setDescription(self, desc):
self.desc = desc
def setValue(self, value):
if self.type == 'boolean':
if value in ('1', 1, True, 'true', 'True', 'yes'):
value = True
else:
value = False
if self.islinebreak and value is not None:
self.value += value.split('\n')
else:
if len(self.value) and (not self.islist or self.type == 'list-single'):
self.value = [value]
else:
self.value.append(value)
def delValue(self, value):
if type(self.value) == type([]):
try:
idx = self.value.index(value)
if idx != -1:
self.value.pop(idx)
except ValueError:
pass
else:
self.value = ''
def setAnswer(self, value):
self.setValue(value)
def buildField(self, xml):
self.type = xml.get('type', 'text-single')
self.label = xml.get('label', '')
for option in xml.findall('{jabber:x:data}option'):
self.addOption(option.find('{jabber:x:data}value').text, option.get('label', ''))
for value in xml.findall('{jabber:x:data}value'):
self.setValue(value.text)
if xml.find('{jabber:x:data}required') is not None:
self.require()
if xml.find('{jabber:x:data}desc') is not None:
self.setDescription(xml.find('{jabber:x:data}desc').text)
def getXML(self, ftype):
field = ET.Element('{jabber:x:data}field')
if ftype != 'result':
field.attrib['type'] = self.type
if self.type != 'fixed':
if self.var:
field.attrib['var'] = self.var
if self.label:
field.attrib['label'] = self.label
if ftype == 'form':
for option in self.options:
optionxml = ET.Element('{jabber:x:data}option')
optionxml.attrib['label'] = option[1]
optionval = ET.Element('{jabber:x:data}value')
optionval.text = option[0]
optionxml.append(optionval)
field.append(optionxml)
if self.required:
required = ET.Element('{jabber:x:data}required')
field.append(required)
if self.desc:
desc = ET.Element('{jabber:x:data}desc')
desc.text = self.desc
field.append(desc)
for value in self.value:
valuexml = ET.Element('{jabber:x:data}value')
if value is True or value is False:
if value:
valuexml.text = '1'
else:
valuexml.text = '0'
else:
valuexml.text = value
field.append(valuexml)
return field
def getXHTML(self):
field = ET.Element('div', {'class': 'xmpp-xforms-%s' % self.type})
if self.label:
label = ET.Element('p')
label.text = "%s: " % self.label
else:
label = ET.Element('p')
label.text = "%s: " % self.var
field.append(label)
if self.type == 'boolean':
formf = ET.Element('input', {'type': 'checkbox', 'name': self.var})
if len(self.value) and self.value[0] in (True, 'true', '1'):
formf.attrib['checked'] = 'checked'
elif self.type == 'fixed':
formf = ET.Element('p')
try:
formf.text = ', '.join(self.value)
except:
pass
field.append(formf)
formf = ET.Element('input', {'type': 'hidden', 'name': self.var})
try:
formf.text = ', '.join(self.value)
except:
pass
elif self.type == 'hidden':
formf = ET.Element('input', {'type': 'hidden', 'name': self.var})
try:
formf.text = ', '.join(self.value)
except:
pass
elif self.type in ('jid-multi', 'list-multi'):
formf = ET.Element('select', {'name': self.var})
for option in self.options:
optf = ET.Element('option', {'value': option[0], 'multiple': 'multiple'})
optf.text = option[1]
if option[1] in self.value:
optf.attrib['selected'] = 'selected'
formf.append(option)
elif self.type in ('jid-single', 'text-single'):
formf = ET.Element('input', {'type': 'text', 'name': self.var})
try:
formf.attrib['value'] = ', '.join(self.value)
except:
pass
elif self.type == 'list-single':
formf = ET.Element('select', {'name': self.var})
for option in self.options:
optf = ET.Element('option', {'value': option[0]})
optf.text = option[1]
if not optf.text:
optf.text = option[0]
if option[1] in self.value:
optf.attrib['selected'] = 'selected'
formf.append(optf)
elif self.type == 'text-multi':
formf = ET.Element('textarea', {'name': self.var})
try:
formf.text = ', '.join(self.value)
except:
pass
if not formf.text:
formf.text = ' '
elif self.type == 'text-private':
formf = ET.Element('input', {'type': 'password', 'name': self.var})
try:
formf.attrib['value'] = ', '.join(self.value)
except:
pass
label.append(formf)
return field
return Form(xml=xml)

View File

@@ -178,9 +178,12 @@ class xep_0009(base.base_plugin):
def plugin_init(self):
self.xep = '0009'
self.description = 'Jabber-RPC'
self.xmpp.add_handler("<iq type='set'><query xmlns='jabber:iq:rpc' /></iq>", self._callMethod)
self.xmpp.add_handler("<iq type='result'><query xmlns='jabber:iq:rpc' /></iq>", self._callResult)
self.xmpp.add_handler("<iq type='error'><query xmlns='jabber:iq:rpc' /></iq>", self._callError)
self.xmpp.add_handler("<iq type='set'><query xmlns='jabber:iq:rpc' /></iq>",
self._callMethod, name='Jabber RPC Call')
self.xmpp.add_handler("<iq type='result'><query xmlns='jabber:iq:rpc' /></iq>",
self._callResult, name='Jabber RPC Result')
self.xmpp.add_handler("<iq type='error'><query xmlns='jabber:iq:rpc' /></iq>",
self._callError, name='Jabber RPC Error')
self.entries = {}
self.activeCalls = []

View File

@@ -0,0 +1,118 @@
"""
SleekXMPP: The Sleek XMPP Library
Copyright (C) 2010 Nathanael C. Fritz
This file is part of SleekXMPP.
See the file LICENSE for copying permission.
"""
from datetime import datetime
import logging
from . import base
from .. stanza.iq import Iq
from .. xmlstream.handler.callback import Callback
from .. xmlstream.matcher.xpath import MatchXPath
from .. xmlstream import ElementBase, ET, JID, register_stanza_plugin
log = logging.getLogger(__name__)
class LastActivity(ElementBase):
name = 'query'
namespace = 'jabber:iq:last'
plugin_attrib = 'last_activity'
interfaces = set(('seconds', 'status'))
def get_seconds(self):
return int(self._get_attr('seconds'))
def set_seconds(self, value):
self._set_attr('seconds', str(value))
def get_status(self):
return self.xml.text
def set_status(self, value):
self.xml.text = str(value)
def del_status(self):
self.xml.text = ''
class xep_0012(base.base_plugin):
"""
XEP-0012 Last Activity
"""
def plugin_init(self):
self.description = "Last Activity"
self.xep = "0012"
self.xmpp.registerHandler(
Callback('Last Activity',
MatchXPath('{%s}iq/{%s}query' % (self.xmpp.default_ns,
LastActivity.namespace)),
self.handle_last_activity_query))
register_stanza_plugin(Iq, LastActivity)
self.xmpp.add_event_handler('last_activity_request', self.handle_last_activity)
def post_init(self):
base.base_plugin.post_init(self)
if self.xmpp.is_component:
# We are a component, so we track the uptime
self.xmpp.add_event_handler("session_start", self._reset_uptime)
self._start_datetime = datetime.now()
self.xmpp.plugin['xep_0030'].add_feature('jabber:iq:last')
def _reset_uptime(self, event):
self._start_datetime = datetime.now()
def handle_last_activity_query(self, iq):
if iq['type'] == 'get':
log.debug("Last activity requested by %s" % iq['from'])
self.xmpp.event('last_activity_request', iq)
elif iq['type'] == 'result':
log.debug("Last activity result from %s" % iq['from'])
self.xmpp.event('last_activity', iq)
def handle_last_activity(self, iq):
jid = iq['from']
if self.xmpp.is_component:
# Send the uptime
result = LastActivity()
td = (datetime.now() - self._start_datetime)
result['seconds'] = td.seconds + td.days * 24 * 3600
reply = iq.reply().setPayload(result.xml).send()
else:
barejid = JID(jid).bare
if barejid in self.xmpp.roster and ( self.xmpp.roster[barejid]['subscription'] in ('from', 'both') or
barejid == self.xmpp.boundjid.bare ):
# We don't know how to calculate it
iq.reply().error().setPayload(iq['last_activity'].xml)
iq['error']['code'] = '503'
iq['error']['type'] = 'cancel'
iq['error']['condition'] = 'service-unavailable'
iq.send()
else:
iq.reply().error().setPayload(iq['last_activity'].xml)
iq['error']['code'] = '403'
iq['error']['type'] = 'auth'
iq['error']['condition'] = 'forbidden'
iq.send()
def get_last_activity(self, jid):
"""Query the LastActivity of jid and return it in seconds"""
iq = self.xmpp.makeIqGet()
query = LastActivity()
iq.append(query.xml)
iq.attrib['to'] = jid
iq.attrib['from'] = self.xmpp.boundjid.full
id = iq.get('id')
result = iq.send()
if result and result is not None and result.get('type', 'error') != 'error':
return result['last_activity']['seconds']
else:
return False

View File

@@ -3,322 +3,327 @@
Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout
This file is part of SleekXMPP.
See the file license.txt for copying permissio
See the file LICENSE for copying permission.
"""
import logging
from . import base
from .. xmlstream.handler.callback import Callback
from .. xmlstream.matcher.xpath import MatchXPath
from .. xmlstream.stanzabase import ElementBase, ET, JID
from .. xmlstream.stanzabase import registerStanzaPlugin, ElementBase, ET, JID
from .. stanza.iq import Iq
log = logging.getLogger(__name__)
class DiscoInfo(ElementBase):
namespace = 'http://jabber.org/protocol/disco#info'
name = 'query'
plugin_attrib = 'disco_info'
interfaces = set(('node', 'features', 'identities'))
namespace = 'http://jabber.org/protocol/disco#info'
name = 'query'
plugin_attrib = 'disco_info'
interfaces = set(('node', 'features', 'identities'))
def getFeatures(self):
features = []
featuresXML = self.xml.findall('{%s}feature' % self.namespace)
for feature in featuresXML:
features.append(feature.attrib['var'])
return features
def getFeatures(self):
features = []
featuresXML = self.xml.findall('{%s}feature' % self.namespace)
for feature in featuresXML:
features.append(feature.attrib['var'])
return features
def setFeatures(self, features):
self.delFeatures()
for name in features:
self.addFeature(name)
def setFeatures(self, features):
self.delFeatures()
for name in features:
self.addFeature(name)
def delFeatures(self):
featuresXML = self.xml.findall('{%s}feature' % self.namespace)
for feature in featuresXML:
self.xml.remove(feature)
def delFeatures(self):
featuresXML = self.xml.findall('{%s}feature' % self.namespace)
for feature in featuresXML:
self.xml.remove(feature)
def addFeature(self, feature):
featureXML = ET.Element('{%s}feature' % self.namespace,
{'var': feature})
self.xml.append(featureXML)
def addFeature(self, feature):
featureXML = ET.Element('{%s}feature' % self.namespace,
{'var': feature})
self.xml.append(featureXML)
def delFeature(self, feature):
featuresXML = self.xml.findall('{%s}feature' % self.namespace)
for featureXML in featuresXML:
if featureXML.attrib['var'] == feature:
self.xml.remove(featureXML)
def delFeature(self, feature):
featuresXML = self.xml.findall('{%s}feature' % self.namespace)
for featureXML in featuresXML:
if featureXML.attrib['var'] == feature:
self.xml.remove(featureXML)
def getIdentities(self):
ids = []
idsXML = self.xml.findall('{%s}identity' % self.namespace)
for idXML in idsXML:
idData = (idXML.attrib['category'],
idXML.attrib['type'],
idXML.attrib.get('name', ''))
ids.append(idData)
return ids
def getIdentities(self):
ids = []
idsXML = self.xml.findall('{%s}identity' % self.namespace)
for idXML in idsXML:
idData = (idXML.attrib['category'],
idXML.attrib['type'],
idXML.attrib.get('name', ''))
ids.append(idData)
return ids
def setIdentities(self, ids):
self.delIdentities()
for idData in ids:
self.addIdentity(*idData)
def setIdentities(self, ids):
self.delIdentities()
for idData in ids:
self.addIdentity(*idData)
def delIdentities(self):
idsXML = self.xml.findall('{%s}identity' % self.namespace)
for idXML in idsXML:
self.xml.remove(idXML)
def delIdentities(self):
idsXML = self.xml.findall('{%s}identity' % self.namespace)
for idXML in idsXML:
self.xml.remove(idXML)
def addIdentity(self, category, id_type, name=''):
idXML = ET.Element('{%s}identity' % self.namespace,
{'category': category,
'type': id_type,
'name': name})
self.xml.append(idXML)
def addIdentity(self, category, id_type, name=''):
idXML = ET.Element('{%s}identity' % self.namespace,
{'category': category,
'type': id_type,
'name': name})
self.xml.append(idXML)
def delIdentity(self, category, id_type, name=''):
idsXML = self.xml.findall('{%s}identity' % self.namespace)
for idXML in idsXML:
idData = (idXML.attrib['category'],
idXML.attrib['type'])
delId = (category, id_type)
if idData == delId:
self.xml.remove(idXML)
def delIdentity(self, category, id_type, name=''):
idsXML = self.xml.findall('{%s}identity' % self.namespace)
for idXML in idsXML:
idData = (idXML.attrib['category'],
idXML.attrib['type'])
delId = (category, id_type)
if idData == delId:
self.xml.remove(idXML)
class DiscoItems(ElementBase):
namespace = 'http://jabber.org/protocol/disco#items'
name = 'query'
plugin_attrib = 'disco_items'
interfaces = set(('node', 'items'))
namespace = 'http://jabber.org/protocol/disco#items'
name = 'query'
plugin_attrib = 'disco_items'
interfaces = set(('node', 'items'))
def getItems(self):
items = []
itemsXML = self.xml.findall('{%s}item' % self.namespace)
for item in itemsXML:
itemData = (item.attrib['jid'],
item.attrib.get('node'),
item.attrib.get('name'))
items.append(itemData)
return items
def getItems(self):
items = []
itemsXML = self.xml.findall('{%s}item' % self.namespace)
for item in itemsXML:
itemData = (item.attrib['jid'],
item.attrib.get('node'),
item.attrib.get('name'))
items.append(itemData)
return items
def setItems(self, items):
self.delItems()
for item in items:
self.addItem(*item)
def setItems(self, items):
self.delItems()
for item in items:
self.addItem(*item)
def delItems(self):
itemsXML = self.xml.findall('{%s}item' % self.namespace)
for item in itemsXML:
self.xml.remove(item)
def delItems(self):
itemsXML = self.xml.findall('{%s}item' % self.namespace)
for item in itemsXML:
self.xml.remove(item)
def addItem(self, jid, node='', name=''):
itemXML = ET.Element('{%s}item' % self.namespace, {'jid': jid})
if name:
itemXML.attrib['name'] = name
if node:
itemXML.attrib['node'] = node
self.xml.append(itemXML)
def addItem(self, jid, node='', name=''):
itemXML = ET.Element('{%s}item' % self.namespace, {'jid': jid})
if name:
itemXML.attrib['name'] = name
if node:
itemXML.attrib['node'] = node
self.xml.append(itemXML)
def delItem(self, jid, node=''):
itemsXML = self.xml.findall('{%s}item' % self.namespace)
for itemXML in itemsXML:
itemData = (itemXML.attrib['jid'],
itemXML.attrib.get('node', ''))
itemDel = (jid, node)
if itemData == itemDel:
self.xml.remove(itemXML)
def delItem(self, jid, node=''):
itemsXML = self.xml.findall('{%s}item' % self.namespace)
for itemXML in itemsXML:
itemData = (itemXML.attrib['jid'],
itemXML.attrib.get('node', ''))
itemDel = (jid, node)
if itemData == itemDel:
self.xml.remove(itemXML)
class DiscoNode(object):
"""
Collection object for grouping info and item information
into nodes.
"""
def __init__(self, name):
self.name = name
self.info = DiscoInfo()
self.items = DiscoItems()
"""
Collection object for grouping info and item information
into nodes.
"""
def __init__(self, name):
self.name = name
self.info = DiscoInfo()
self.items = DiscoItems()
# This is a bit like poor man's inheritance, but
# to simplify adding information to the node we
# map node functions to either the info or items
# stanza objects.
#
# We don't want to make DiscoNode inherit from
# DiscoInfo and DiscoItems because DiscoNode is
# not an actual stanza, and doing so would create
# confusion and potential bugs.
self.info['node'] = name
self.items['node'] = name
self._map(self.items, 'items', ['get', 'set', 'del'])
self._map(self.items, 'item', ['add', 'del'])
self._map(self.info, 'identities', ['get', 'set', 'del'])
self._map(self.info, 'identity', ['add', 'del'])
self._map(self.info, 'features', ['get', 'set', 'del'])
self._map(self.info, 'feature', ['add', 'del'])
# This is a bit like poor man's inheritance, but
# to simplify adding information to the node we
# map node functions to either the info or items
# stanza objects.
#
# We don't want to make DiscoNode inherit from
# DiscoInfo and DiscoItems because DiscoNode is
# not an actual stanza, and doing so would create
# confusion and potential bugs.
def isEmpty(self):
"""
Test if the node contains any information. Useful for
determining if a node can be deleted.
"""
ids = self.getIdentities()
features = self.getFeatures()
items = self.getItems()
self._map(self.items, 'items', ['get', 'set', 'del'])
self._map(self.items, 'item', ['add', 'del'])
self._map(self.info, 'identities', ['get', 'set', 'del'])
self._map(self.info, 'identity', ['add', 'del'])
self._map(self.info, 'features', ['get', 'set', 'del'])
self._map(self.info, 'feature', ['add', 'del'])
if not ids and not features and not items:
return True
return False
def isEmpty(self):
"""
Test if the node contains any information. Useful for
determining if a node can be deleted.
"""
ids = self.getIdentities()
features = self.getFeatures()
items = self.getItems()
def _map(self, obj, interface, access):
"""
Map functions of the form obj.accessInterface
to self.accessInterface for each given access type.
"""
interface = interface.title()
for access_type in access:
method = access_type + interface
if hasattr(obj, method):
setattr(self, method, getattr(obj, method))
if not ids and not features and not items:
return True
return False
def _map(self, obj, interface, access):
"""
Map functions of the form obj.accessInterface
to self.accessInterface for each given access type.
"""
interface = interface.title()
for access_type in access:
method = access_type + interface
if hasattr(obj, method):
setattr(self, method, getattr(obj, method))
class xep_0030(base.base_plugin):
"""
XEP-0030 Service Discovery
"""
def plugin_init(self):
self.xep = '0030'
self.description = 'Service Discovery'
"""
XEP-0030 Service Discovery
"""
self.xmpp.registerHandler(
Callback('Disco Items',
MatchXPath('{%s}iq/{%s}query' % (self.xmpp.default_ns,
DiscoItems.namespace)),
self.handle_item_query))
def plugin_init(self):
self.xep = '0030'
self.description = 'Service Discovery'
self.xmpp.registerHandler(
Callback('Disco Info',
MatchXPath('{%s}iq/{%s}query' % (self.xmpp.default_ns,
DiscoInfo.namespace)),
self.handle_info_query))
self.xmpp.registerHandler(
Callback('Disco Items',
MatchXPath('{%s}iq/{%s}query' % (self.xmpp.default_ns,
DiscoItems.namespace)),
self.handle_item_query))
self.xmpp.stanzaPlugin(Iq, DiscoInfo)
self.xmpp.stanzaPlugin(Iq, DiscoItems)
self.xmpp.registerHandler(
Callback('Disco Info',
MatchXPath('{%s}iq/{%s}query' % (self.xmpp.default_ns,
DiscoInfo.namespace)),
self.handle_info_query))
self.xmpp.add_event_handler('disco_items_request', self.handle_disco_items)
self.xmpp.add_event_handler('disco_info_request', self.handle_disco_info)
registerStanzaPlugin(Iq, DiscoInfo)
registerStanzaPlugin(Iq, DiscoItems)
self.nodes = {'main': DiscoNode('main')}
self.xmpp.add_event_handler('disco_items_request', self.handle_disco_items)
self.xmpp.add_event_handler('disco_info_request', self.handle_disco_info)
def add_node(self, node):
if node not in self.nodes:
self.nodes[node] = DiscoNode(node)
self.nodes = {'main': DiscoNode('main')}
def del_node(self, node):
if node in self.nodes:
del self.nodes[node]
def add_node(self, node):
if node not in self.nodes:
self.nodes[node] = DiscoNode(node)
def handle_item_query(self, iq):
if iq['type'] == 'get':
logging.debug("Items requested by %s" % iq['from'])
self.xmpp.event('disco_items_request', iq)
elif iq['type'] == 'result':
logging.debug("Items result from %s" % iq['from'])
self.xmpp.event('disco_items', iq)
def del_node(self, node):
if node in self.nodes:
del self.nodes[node]
def handle_info_query(self, iq):
if iq['type'] == 'get':
logging.debug("Info requested by %s" % iq['from'])
self.xmpp.event('disco_info_request', iq)
elif iq['type'] == 'result':
logging.debug("Info result from %s" % iq['from'])
self.xmpp.event('disco_info', iq)
def handle_item_query(self, iq):
if iq['type'] == 'get':
log.debug("Items requested by %s" % iq['from'])
self.xmpp.event('disco_items_request', iq)
elif iq['type'] == 'result':
log.debug("Items result from %s" % iq['from'])
self.xmpp.event('disco_items', iq)
def handle_disco_info(self, iq, forwarded=False):
"""
A default handler for disco#info requests. If another
handler is registered, this one will defer and not run.
"""
handlers = self.xmpp.event_handlers['disco_info_request']
if not forwarded and len(handlers) > 1:
return
def handle_info_query(self, iq):
if iq['type'] == 'get':
log.debug("Info requested by %s" % iq['from'])
self.xmpp.event('disco_info_request', iq)
elif iq['type'] == 'result':
log.debug("Info result from %s" % iq['from'])
self.xmpp.event('disco_info', iq)
node_name = iq['disco_info']['node']
if not node_name:
node_name = 'main'
def handle_disco_info(self, iq, forwarded=False):
"""
A default handler for disco#info requests. If another
handler is registered, this one will defer and not run.
"""
if not forwarded and self.xmpp.event_handled('disco_info_request'):
return
logging.debug("Using default handler for disco#info on node '%s'." % node_name)
node_name = iq['disco_info']['node']
if not node_name:
node_name = 'main'
if node_name in self.nodes:
node = self.nodes[node_name]
iq.reply().setPayload(node.info.xml).send()
else:
logging.debug("Node %s requested, but does not exist." % node_name)
iq.reply().error().setPayload(iq['disco_info'].xml)
iq['error']['code'] = '404'
iq['error']['type'] = 'cancel'
iq['error']['condition'] = 'item-not-found'
iq.send()
def handle_disco_items(self, iq, forwarded=False):
"""
A default handler for disco#items requests. If another
handler is registered, this one will defer and not run.
log.debug("Using default handler for disco#info on node '%s'." % node_name)
If this handler is called by your own custom handler with
forwarded set to True, then it will run as normal.
"""
handlers = self.xmpp.event_handlers['disco_items_request']
if not forwarded and len(handlers) > 1:
return
if node_name in self.nodes:
node = self.nodes[node_name]
iq.reply().setPayload(node.info.xml).send()
else:
log.debug("Node %s requested, but does not exist." % node_name)
iq.reply().error().setPayload(iq['disco_info'].xml)
iq['error']['code'] = '404'
iq['error']['type'] = 'cancel'
iq['error']['condition'] = 'item-not-found'
iq.send()
node_name = iq['disco_items']['node']
if not node_name:
node_name = 'main'
def handle_disco_items(self, iq, forwarded=False):
"""
A default handler for disco#items requests. If another
handler is registered, this one will defer and not run.
logging.debug("Using default handler for disco#items on node '%s'." % node_name)
If this handler is called by your own custom handler with
forwarded set to True, then it will run as normal.
"""
if not forwarded and self.xmpp.event_handled('disco_items_request'):
return
if node_name in self.nodes:
node = self.nodes[node_name]
iq.reply().setPayload(node.items.xml).send()
else:
logging.debug("Node %s requested, but does not exist." % node_name)
iq.reply().error().setPayload(iq['disco_items'].xml)
iq['error']['code'] = '404'
iq['error']['type'] = 'cancel'
iq['error']['condition'] = 'item-not-found'
iq.send()
node_name = iq['disco_items']['node']
if not node_name:
node_name = 'main'
# Older interface methods for backwards compatibility
log.debug("Using default handler for disco#items on node '%s'." % node_name)
def getInfo(self, jid, node=''):
iq = self.xmpp.Iq()
iq['type'] = 'get'
iq['to'] = jid
iq['from'] = self.xmpp.fulljid
iq['disco_info']['node'] = node
iq.send()
if node_name in self.nodes:
node = self.nodes[node_name]
iq.reply().setPayload(node.items.xml).send()
else:
log.debug("Node %s requested, but does not exist." % node_name)
iq.reply().error().setPayload(iq['disco_items'].xml)
iq['error']['code'] = '404'
iq['error']['type'] = 'cancel'
iq['error']['condition'] = 'item-not-found'
iq.send()
def getItems(self, jid, node=''):
iq = self.xmpp.Iq()
iq['type'] = 'get'
iq['to'] = jid
iq['from'] = self.xmpp.fulljid
iq['disco_items']['node'] = node
iq.send()
def add_feature(self, feature, node='main'):
self.add_node(node)
self.nodes[node].addFeature(feature)
def add_identity(self, category='', itype='', name='', node='main'):
self.add_node(node)
self.nodes[node].addIdentity(category=category,
id_type=itype,
name=name)
def add_item(self, jid=None, name='', node='main', subnode=''):
self.add_node(node)
self.add_node(subnode)
if jid is None:
jid = self.xmpp.fulljid
self.nodes[node].addItem(jid=jid, name=name, node=subnode)
# Older interface methods for backwards compatibility
def getInfo(self, jid, node='', dfrom=None):
iq = self.xmpp.Iq()
iq['type'] = 'get'
iq['to'] = jid
iq['from'] = dfrom
iq['disco_info']['node'] = node
return iq.send()
def getItems(self, jid, node='', dfrom=None):
iq = self.xmpp.Iq()
iq['type'] = 'get'
iq['to'] = jid
iq['from'] = dfrom
iq['disco_items']['node'] = node
return iq.send()
def add_feature(self, feature, node='main'):
self.add_node(node)
self.nodes[node].addFeature(feature)
def add_identity(self, category='', itype='', name='', node='main'):
self.add_node(node)
self.nodes[node].addIdentity(category=category,
id_type=itype,
name=name)
def add_item(self, jid=None, name='', node='main', subnode=''):
self.add_node(node)
self.add_node(subnode)
if jid is None:
jid = self.xmpp.fulljid
self.nodes[node].addItem(jid=jid, name=name, node=subnode)

View File

@@ -0,0 +1,161 @@
"""
SleekXMPP: The Sleek XMPP Library
Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout
This file is part of SleekXMPP.
See the file LICENSE for copying permission.
"""
import logging
from . import base
from .. xmlstream.handler.callback import Callback
from .. xmlstream.matcher.xpath import MatchXPath
from .. xmlstream.stanzabase import registerStanzaPlugin, ElementBase, ET, JID
from .. stanza.message import Message
class Addresses(ElementBase):
namespace = 'http://jabber.org/protocol/address'
name = 'addresses'
plugin_attrib = 'addresses'
interfaces = set(('addresses', 'bcc', 'cc', 'noreply', 'replyroom', 'replyto', 'to'))
def addAddress(self, atype='to', jid='', node='', uri='', desc='', delivered=False):
address = Address(parent=self)
address['type'] = atype
address['jid'] = jid
address['node'] = node
address['uri'] = uri
address['desc'] = desc
address['delivered'] = delivered
return address
def getAddresses(self, atype=None):
addresses = []
for addrXML in self.xml.findall('{%s}address' % Address.namespace):
# ElementTree 1.2.6 does not support [@attr='value'] in findall
if atype is None or addrXML.attrib.get('type') == atype:
addresses.append(Address(xml=addrXML, parent=None))
return addresses
def setAddresses(self, addresses, set_type=None):
self.delAddresses(set_type)
for addr in addresses:
addr = dict(addr)
# Remap 'type' to 'atype' to match the add method
if set_type is not None:
addr['type'] = set_type
curr_type = addr.get('type', None)
if curr_type is not None:
del addr['type']
addr['atype'] = curr_type
self.addAddress(**addr)
def delAddresses(self, atype=None):
if atype is None:
return
for addrXML in self.xml.findall('{%s}address' % Address.namespace):
# ElementTree 1.2.6 does not support [@attr='value'] in findall
if addrXML.attrib.get('type') == atype:
self.xml.remove(addrXML)
# --------------------------------------------------------------
def delBcc(self):
self.delAddresses('bcc')
def delCc(self):
self.delAddresses('cc')
def delNoreply(self):
self.delAddresses('noreply')
def delReplyroom(self):
self.delAddresses('replyroom')
def delReplyto(self):
self.delAddresses('replyto')
def delTo(self):
self.delAddresses('to')
# --------------------------------------------------------------
def getBcc(self):
return self.getAddresses('bcc')
def getCc(self):
return self.getAddresses('cc')
def getNoreply(self):
return self.getAddresses('noreply')
def getReplyroom(self):
return self.getAddresses('replyroom')
def getReplyto(self):
return self.getAddresses('replyto')
def getTo(self):
return self.getAddresses('to')
# --------------------------------------------------------------
def setBcc(self, addresses):
self.setAddresses(addresses, 'bcc')
def setCc(self, addresses):
self.setAddresses(addresses, 'cc')
def setNoreply(self, addresses):
self.setAddresses(addresses, 'noreply')
def setReplyroom(self, addresses):
self.setAddresses(addresses, 'replyroom')
def setReplyto(self, addresses):
self.setAddresses(addresses, 'replyto')
def setTo(self, addresses):
self.setAddresses(addresses, 'to')
class Address(ElementBase):
namespace = 'http://jabber.org/protocol/address'
name = 'address'
plugin_attrib = 'address'
interfaces = set(('delivered', 'desc', 'jid', 'node', 'type', 'uri'))
address_types = set(('bcc', 'cc', 'noreply', 'replyroom', 'replyto', 'to'))
def getDelivered(self):
return self.xml.attrib.get('delivered', False)
def setDelivered(self, delivered):
if delivered:
self.xml.attrib['delivered'] = "true"
else:
del self['delivered']
def setUri(self, uri):
if uri:
del self['jid']
del self['node']
self.xml.attrib['uri'] = uri
elif 'uri' in self.xml.attrib:
del self.xml.attrib['uri']
class xep_0033(base.base_plugin):
"""
XEP-0033: Extended Stanza Addressing
"""
def plugin_init(self):
self.xep = '0033'
self.description = 'Extended Stanza Addressing'
registerStanzaPlugin(Message, Addresses)
def post_init(self):
base.base_plugin.post_init(self)
self.xmpp.plugin['xep_0030'].add_feature(Addresses.namespace)

View File

@@ -1,32 +1,24 @@
"""
SleekXMPP: The Sleek XMPP Library
Copyright (C) 2007 Nathanael C. Fritz
This file is part of SleekXMPP.
SleekXMPP: The Sleek XMPP Library
Copyright (C) 2010 Nathanael C. Fritz
This file is part of SleekXMPP.
SleekXMPP is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
SleekXMPP is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with SleekXMPP; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
See the file LICENSE for copying permission.
"""
from __future__ import with_statement
from . import base
import logging
from xml.etree import cElementTree as ET
from .. xmlstream.stanzabase import ElementBase, JID
from .. xmlstream.stanzabase import registerStanzaPlugin, ElementBase, JID
from .. stanza.presence import Presence
from .. xmlstream.handler.callback import Callback
from .. xmlstream.matcher.xpath import MatchXPath
from .. xmlstream.matcher.xmlmask import MatchXMLMask
log = logging.getLogger(__name__)
class MUCPresence(ElementBase):
name = 'x'
namespace = 'http://jabber.org/protocol/muc#user'
@@ -46,107 +38,128 @@ class MUCPresence(ElementBase):
#TODO if no affilation, set it to the default and return default
item = self.getXMLItem()
return item.get('affiliation', '')
def setAffiliation(self, value):
item = self.getXMLItem()
#TODO check for valid affiliation
item.attrib['affiliation'] = value
return self
def delAffiliation(self):
item = self.getXMLItem()
#TODO set default affiliation
if 'affiliation' in item.attrib: del item.attrib['affiliation']
return self
def getJid(self):
item = self.getXMLItem()
return JID(item.get('jid', ''))
def setJid(self, value):
item = self.getXMLItem()
if not isinstance(value, str):
value = str(value)
item.attrib['jid'] = value
return self
def delJid(self):
item = self.getXMLItem()
if 'jid' in item.attrib: del item.attrib['jid']
return self
def getRole(self):
item = self.getXMLItem()
#TODO get default role, set default role if none
return item.get('role', '')
def setRole(self, value):
item = self.getXMLItem()
#TODO check for valid role
item.attrib['role'] = value
return self
def delRole(self):
item = self.getXMLItem()
#TODO set default role
if 'role' in item.attrib: del item.attrib['role']
return self
def getNick(self):
return self.parent()['from'].resource
def getRoom(self):
return self.parent()['from'].bare
def setNick(self, value):
logging.warning("Cannot set nick through mucpresence plugin.")
log.warning("Cannot set nick through mucpresence plugin.")
return self
def setRoom(self, value):
logging.warning("Cannot set room through mucpresence plugin.")
log.warning("Cannot set room through mucpresence plugin.")
return self
def delNick(self):
logging.warning("Cannot delete nick through mucpresence plugin.")
log.warning("Cannot delete nick through mucpresence plugin.")
return self
def delRoom(self):
logging.warning("Cannot delete room through mucpresence plugin.")
log.warning("Cannot delete room through mucpresence plugin.")
return self
class xep_0045(base.base_plugin):
"""
Impliments XEP-0045 Multi User Chat
"""
def plugin_init(self):
self.rooms = {}
self.ourNicks = {}
self.xep = '0045'
self.description = 'Multi User Chat'
# load MUC support in presence stanzas
self.xmpp.stanzaPlugin(Presence, MUCPresence)
registerStanzaPlugin(Presence, MUCPresence)
self.xmpp.registerHandler(Callback('MUCPresence', MatchXMLMask("<presence xmlns='%s' />" % self.xmpp.default_ns), self.handle_groupchat_presence))
self.xmpp.registerHandler(Callback('MUCMessage', MatchXMLMask("<message xmlns='%s' type='groupchat'><body/></message>" % self.xmpp.default_ns), self.handle_groupchat_message))
self.xmpp.registerHandler(Callback('MUCSubject', MatchXMLMask("<message xmlns='%s' type='groupchat'><subject/></message>" % self.xmpp.default_ns), self.handle_groupchat_subject))
def handle_groupchat_presence(self, pr):
""" Handle a presence in a muc.
"""
got_offline = False
got_online = False
if pr['muc']['room'] not in self.rooms.keys():
return
entry = pr['muc'].getValues()
entry = pr['muc'].getStanzaValues()
entry['show'] = pr['show']
entry['status'] = pr['status']
if pr['type'] == 'unavailable':
del self.rooms[entry['room']][entry['nick']]
if entry['nick'] in self.rooms[entry['room']]:
del self.rooms[entry['room']][entry['nick']]
got_offline = True
else:
if entry['nick'] not in self.rooms[entry['room']]:
got_online = True
self.rooms[entry['room']][entry['nick']] = entry
logging.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:
self.xmpp.event("muc::%s::got_offline" % entry['room'], pr)
if got_online:
self.xmpp.event("muc::%s::got_online" % entry['room'], pr)
def handle_groupchat_message(self, msg):
""" Handle a message event in a muc.
"""
self.xmpp.event('groupchat_message', msg)
self.xmpp.event("muc::%s::message" % msg['from'].bare, msg)
def handle_groupchat_subject(self, msg):
""" Handle a message coming from a muc indicating
a change of subject (or announcing it when joining the room)
"""
self.xmpp.event('groupchat_subject', msg)
def jidInRoom(self, room, jid):
for nick in self.rooms[room]:
entry = self.rooms[room][nick]
@@ -154,6 +167,12 @@ class xep_0045(base.base_plugin):
return True
return False
def getNick(self, room, jid):
for nick in self.rooms[room]:
entry = self.rooms[room][nick]
if entry is not None and entry['jid'].full == jid:
return nick
def getRoomForm(self, room, ifrom=None):
iq = self.xmpp.makeIqGet()
iq['to'] = room
@@ -166,14 +185,14 @@ class xep_0045(base.base_plugin):
return False
xform = result.xml.find('{http://jabber.org/protocol/muc#owner}query/{jabber:x:data}x')
if xform is None: return False
form = self.xmpp.plugin['xep_0004'].buildForm(xform)
form = self.xmpp.plugin['old_0004'].buildForm(xform)
return form
def configureRoom(self, room, form=None, ifrom=None):
if form is None:
form = self.getRoomForm(room, ifrom=ifrom)
#form = self.xmpp.plugin['xep_0004'].makeForm(ftype='submit')
#form.addField('FORM_TYPE', value='http://jabber.org/protocol/muc#roomconfig')
#form = self.xmpp.plugin['old_0004'].makeForm(ftype='submit')
#form.addField('FORM_TYPE', value='http://jabber.org/protocol/muc#roomconfig')
iq = self.xmpp.makeIqSet()
iq['to'] = room
if ifrom is not None:
@@ -186,7 +205,7 @@ class xep_0045(base.base_plugin):
if result['type'] == 'error':
return False
return True
def joinMUC(self, room, nick, maxhistory="0", password='', wait=False, pstatus=None, pshow=None):
""" Join the specified room, requesting 'maxhistory' lines of history.
"""
@@ -196,9 +215,13 @@ class xep_0045(base.base_plugin):
passelement = ET.Element('password')
passelement.text = password
x.append(passelement)
history = ET.Element('history')
history.attrib['maxstanzas'] = maxhistory
x.append(history)
if maxhistory:
history = ET.Element('history')
if maxhistory == "0":
history.attrib['maxchars'] = maxhistory
else:
history.attrib['maxstanzas'] = maxhistory
x.append(history)
stanza.append(x)
if not wait:
self.xmpp.send(stanza)
@@ -208,7 +231,7 @@ class xep_0045(base.base_plugin):
self.xmpp.send(stanza, expect)
self.rooms[room] = {}
self.ourNicks[room] = nick
def destroy(self, room, reason='', altroom = '', ifrom=None):
iq = self.xmpp.makeIqSet()
if ifrom is not None:
@@ -234,9 +257,9 @@ class xep_0045(base.base_plugin):
raise TypeError
query = ET.Element('{http://jabber.org/protocol/muc#admin}query')
if nick is not None:
item = ET.Element('item', {'affiliation':affiliation, 'nick':nick})
item = ET.Element('item', {'affiliation':affiliation, 'nick':nick})
else:
item = ET.Element('item', {'affiliation':affiliation, 'jid':jid})
item = ET.Element('item', {'affiliation':affiliation, 'jid':jid})
query.append(item)
iq = self.xmpp.makeIqSet(query)
iq['to'] = room
@@ -244,7 +267,7 @@ class xep_0045(base.base_plugin):
if result is False or result['type'] != 'result':
raise ValueError
return True
def invite(self, room, jid, reason=''):
""" Invite a jid to a room."""
msg = self.xmpp.makeMessage(room)
@@ -259,15 +282,19 @@ class xep_0045(base.base_plugin):
msg.append(x)
self.xmpp.send(msg)
def leaveMUC(self, room, nick):
def leaveMUC(self, room, nick, msg=''):
""" Leave the specified room.
"""
self.xmpp.sendPresence(pshow='unavailable', pto="%s/%s" % (room, nick))
if msg:
self.xmpp.sendPresence(pshow='unavailable', pto="%s/%s" % (room, nick), pstatus=msg)
else:
self.xmpp.sendPresence(pshow='unavailable', pto="%s/%s" % (room, nick))
del self.rooms[room]
def getRoomConfig(self, room):
iq = self.xmpp.makeIqGet('http://jabber.org/protocol/muc#owner')
iq['to'] = room
iq['from'] = self.xmpp.jid
result = iq.send()
if result is None or result['type'] != 'result':
raise ValueError
@@ -275,30 +302,31 @@ class xep_0045(base.base_plugin):
if form is None:
raise ValueError
return self.xmpp.plugin['xep_0004'].buildForm(form)
def cancelConfig(self, room):
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.send()
def setRoomConfig(self, room, config):
query = ET.Element('{http://jabber.org/protocol/muc#owner}query')
x = config.getXML('submit')
query.append(x)
iq = self.xmpp.makeIqSet(query)
iq['to'] = room
iq['from'] = self.xmpp.jid
iq.send()
def getJoinedRooms(self):
return self.rooms.keys()
def getOurJidInRoom(self, roomJid):
""" Return the jid we're using in a room.
"""
return "%s/%s" % (roomJid, self.ourNicks[roomJid])
def getJidProperty(self, room, nick, jidProperty):
""" Get the property of a nick in a room, such as its 'jid' or 'affiliation'
If not found, return None.
@@ -307,7 +335,7 @@ class xep_0045(base.base_plugin):
return self.rooms[room][nick][jidProperty]
else:
return None
def getRoster(self, room):
""" Get the list of nicks in a room.
"""

View File

@@ -1,27 +1,14 @@
"""
SleekXMPP: The Sleek XMPP Library
Copyright (C) 2007 Nathanael C. Fritz
This file is part of SleekXMPP.
SleekXMPP is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
SleekXMPP is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with SleekXMPP; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
SleekXMPP: The Sleek XMPP Library
Copyright (C) 2010 Nathanael C. Fritz
This file is part of SleekXMPP.
See the file LICENSE for copying permission.
"""
from __future__ import with_statement
from . import base
import logging
from xml.etree import cElementTree as ET
import traceback
import time
class xep_0050(base.base_plugin):
@@ -32,11 +19,11 @@ class xep_0050(base.base_plugin):
def plugin_init(self):
self.xep = '0050'
self.description = 'Ad-Hoc Commands'
self.xmpp.add_handler("<iq type='set' xmlns='%s'><command xmlns='http://jabber.org/protocol/commands' action='__None__'/></iq>" % self.xmpp.default_ns, self.handler_command)
self.xmpp.add_handler("<iq type='set' xmlns='%s'><command xmlns='http://jabber.org/protocol/commands' action='execute'/></iq>" % self.xmpp.default_ns, self.handler_command)
self.xmpp.add_handler("<iq type='set' xmlns='%s'><command xmlns='http://jabber.org/protocol/commands' action='next'/></iq>" % self.xmpp.default_ns, self.handler_command_next, threaded=True)
self.xmpp.add_handler("<iq type='set' xmlns='%s'><command xmlns='http://jabber.org/protocol/commands' action='cancel'/></iq>" % self.xmpp.default_ns, self.handler_command_cancel)
self.xmpp.add_handler("<iq type='set' xmlns='%s'><command xmlns='http://jabber.org/protocol/commands' action='complete'/></iq>" % self.xmpp.default_ns, self.handler_command_complete)
self.xmpp.add_handler("<iq type='set' xmlns='%s'><command xmlns='http://jabber.org/protocol/commands' action='__None__'/></iq>" % self.xmpp.default_ns, self.handler_command, name='Ad-Hoc None')
self.xmpp.add_handler("<iq type='set' xmlns='%s'><command xmlns='http://jabber.org/protocol/commands' action='execute'/></iq>" % self.xmpp.default_ns, self.handler_command, name='Ad-Hoc Execute')
self.xmpp.add_handler("<iq type='set' xmlns='%s'><command xmlns='http://jabber.org/protocol/commands' action='next'/></iq>" % self.xmpp.default_ns, self.handler_command_next, name='Ad-Hoc Next', threaded=True)
self.xmpp.add_handler("<iq type='set' xmlns='%s'><command xmlns='http://jabber.org/protocol/commands' action='cancel'/></iq>" % self.xmpp.default_ns, self.handler_command_cancel, name='Ad-Hoc Cancel')
self.xmpp.add_handler("<iq type='set' xmlns='%s'><command xmlns='http://jabber.org/protocol/commands' action='complete'/></iq>" % self.xmpp.default_ns, self.handler_command_complete, name='Ad-Hoc Complete')
self.commands = {}
self.sessions = {}
self.sd = self.xmpp.plugin['xep_0030']
@@ -83,7 +70,7 @@ class xep_0050(base.base_plugin):
in_command = xml.find('{http://jabber.org/protocol/commands}command')
sessionid = in_command.get('sessionid', None)
pointer = self.sessions[sessionid]['next']
results = self.xmpp.plugin['xep_0004'].makeForm('result')
results = self.xmpp.plugin['old_0004'].makeForm('result')
results.fromXML(in_command.find('{jabber:x:data}x'))
pointer(results,sessionid)
self.xmpp.send(self.makeCommand(xml.attrib['from'], in_command.attrib['node'], form=None, id=xml.attrib['id'], sessionid=sessionid, status='completed', actions=[]))
@@ -94,7 +81,7 @@ class xep_0050(base.base_plugin):
in_command = xml.find('{http://jabber.org/protocol/commands}command')
sessionid = in_command.get('sessionid', None)
pointer = self.sessions[sessionid]['next']
results = self.xmpp.plugin['xep_0004'].makeForm('result')
results = self.xmpp.plugin['old_0004'].makeForm('result')
results.fromXML(in_command.find('{jabber:x:data}x'))
form, npointer, next = pointer(results,sessionid)
self.sessions[sessionid]['next'] = npointer

View File

@@ -2,8 +2,13 @@ from __future__ import with_statement
from . import base
import logging
#from xml.etree import cElementTree as ET
from .. xmlstream.stanzabase import ElementBase, ET
from .. xmlstream.stanzabase import registerStanzaPlugin, ElementBase, ET
from . import stanza_pubsub
from . xep_0004 import Form
log = logging.getLogger(__name__)
class xep_0060(base.base_plugin):
"""
@@ -13,7 +18,7 @@ class xep_0060(base.base_plugin):
def plugin_init(self):
self.xep = '0060'
self.description = 'Publish-Subscribe'
def create_node(self, jid, node, config=None, collection=False, ntype=None):
pubsub = ET.Element('{http://jabber.org/protocol/pubsub}pubsub')
create = ET.Element('create')
@@ -41,7 +46,8 @@ class xep_0060(base.base_plugin):
submitform.field['pubsub#node_type'].setValue('leaf')
else:
submitform.addField('pubsub#node_type', value='leaf')
configure.append(submitform.getXML('submit'))
submitform['type'] = 'submit'
configure.append(submitform.xml)
pubsub.append(configure)
iq = self.xmpp.makeIqSet(pubsub)
iq.attrib['to'] = jid
@@ -50,7 +56,7 @@ class xep_0060(base.base_plugin):
result = iq.send()
if result is False or result is None or result['type'] == 'error': return False
return True
def subscribe(self, jid, node, bare=True, subscribee=None):
pubsub = ET.Element('{http://jabber.org/protocol/pubsub}pubsub')
subscribe = ET.Element('subscribe')
@@ -70,7 +76,7 @@ class xep_0060(base.base_plugin):
result = iq.send()
if result is False or result is None or result['type'] == 'error': return False
return True
def unsubscribe(self, jid, node, bare=True, subscribee=None):
pubsub = ET.Element('{http://jabber.org/protocol/pubsub}pubsub')
unsubscribe = ET.Element('unsubscribe')
@@ -90,7 +96,7 @@ class xep_0060(base.base_plugin):
result = iq.send()
if result is False or result is None or result['type'] == 'error': return False
return True
def getNodeConfig(self, jid, node=None): # if no node, then grab default
pubsub = ET.Element('{http://jabber.org/protocol/pubsub#owner}pubsub')
if node is not None:
@@ -108,17 +114,17 @@ class xep_0060(base.base_plugin):
#self.xmpp.add_handler("<iq id='%s'/>" % id, self.handlerCreateNodeResponse)
result = iq.send()
if result is None or result == False or result['type'] == 'error':
logging.warning("got error instead of config")
log.warning("got error instead of config")
return False
if node is not None:
form = result.find('{http://jabber.org/protocol/pubsub#owner}pubsub/{http://jabber.org/protocol/pubsub#owner}configure/{jabber:x:data}x')
else:
form = result.find('{http://jabber.org/protocol/pubsub#owner}pubsub/{http://jabber.org/protocol/pubsub#owner}default/{jabber:x:data}x')
if not form or form is None:
logging.error("No form found.")
log.error("No form found.")
return False
return self.xmpp.plugin['xep_0004'].buildForm(form)
return Form(xml=form)
def getNodeSubscriptions(self, jid, node):
pubsub = ET.Element('{http://jabber.org/protocol/pubsub#owner}pubsub')
subscriptions = ET.Element('subscriptions')
@@ -131,7 +137,7 @@ class xep_0060(base.base_plugin):
id = iq['id']
result = iq.send()
if result is None or result == False or result['type'] == 'error':
logging.warning("got error instead of config")
log.warning("got error instead of config")
return False
else:
results = result.findall('{http://jabber.org/protocol/pubsub#owner}pubsub/{http://jabber.org/protocol/pubsub#owner}subscriptions/{http://jabber.org/protocol/pubsub#owner}subscription')
@@ -154,7 +160,7 @@ class xep_0060(base.base_plugin):
id = iq['id']
result = iq.send()
if result is None or result == False or result['type'] == 'error':
logging.warning("got error instead of config")
log.warning("got error instead of config")
return False
else:
results = result.findall('{http://jabber.org/protocol/pubsub#owner}pubsub/{http://jabber.org/protocol/pubsub#owner}affiliations/{http://jabber.org/protocol/pubsub#owner}affiliation')
@@ -179,8 +185,8 @@ class xep_0060(base.base_plugin):
return True
else:
return False
def setNodeConfig(self, jid, node, config):
pubsub = ET.Element('{http://jabber.org/protocol/pubsub#owner}pubsub')
configure = ET.Element('configure')
@@ -193,10 +199,10 @@ class xep_0060(base.base_plugin):
iq.attrib['from'] = self.xmpp.fulljid
id = iq['id']
result = iq.send()
if result is None or result['type'] == 'error':
if result is None or result['type'] == 'error':
return False
return True
def setItem(self, jid, node, items=[]):
pubsub = ET.Element('{http://jabber.org/protocol/pubsub}pubsub')
publish = ET.Element('publish')
@@ -216,7 +222,7 @@ class xep_0060(base.base_plugin):
result = iq.send()
if result is None or result is False or result['type'] == 'error': return False
return True
def addItem(self, jid, node, items=[]):
return self.setItem(jid, node, items)
@@ -235,7 +241,7 @@ class xep_0060(base.base_plugin):
result = iq.send()
if result is None or result is False or result['type'] == 'error': return False
return True
def getNodes(self, jid):
response = self.xmpp.plugin['xep_0030'].getItems(jid)
items = response.findall('{http://jabber.org/protocol/disco#items}query/{http://jabber.org/protocol/disco#items}item')
@@ -244,7 +250,7 @@ class xep_0060(base.base_plugin):
for item in items:
nodes[item.get('node')] = item.get('name')
return nodes
def getItems(self, jid, node):
response = self.xmpp.plugin['xep_0030'].getItems(jid, node)
items = response.findall('{http://jabber.org/protocol/disco#items}query/{http://jabber.org/protocol/disco#items}item')
@@ -262,7 +268,7 @@ class xep_0060(base.base_plugin):
try:
config.field['pubsub#collection'].setValue(parent)
except KeyError:
logging.warning("pubsub#collection doesn't exist in config, trying to add it")
log.warning("pubsub#collection doesn't exist in config, trying to add it")
config.addField('pubsub#collection', value=parent)
if not self.setNodeConfig(jid, child, config):
return False
@@ -296,7 +302,7 @@ class xep_0060(base.base_plugin):
try:
config.field['pubsub#collection'].setValue(parent)
except KeyError:
logging.warning("pubsub#collection doesn't exist in config, trying to add it")
log.warning("pubsub#collection doesn't exist in config, trying to add it")
config.addField('pubsub#collection', value=parent)
if not self.setNodeConfig(jid, child, config):
return False

View File

@@ -1,21 +1,9 @@
"""
SleekXMPP: The Sleek XMPP Library
Copyright (C) 2007 Nathanael C. Fritz
This file is part of SleekXMPP.
SleekXMPP: The Sleek XMPP Library
Copyright (C) 2010 Nathanael C. Fritz
This file is part of SleekXMPP.
SleekXMPP is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
SleekXMPP is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with SleekXMPP; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
See the file LICENSE for copying permission.
"""
from __future__ import with_statement
from xml.etree import cElementTree as ET
@@ -24,6 +12,9 @@ import hashlib
from . import base
log = logging.getLogger(__name__)
class xep_0078(base.base_plugin):
"""
XEP-0078 NON-SASL Authentication
@@ -35,14 +26,14 @@ class xep_0078(base.base_plugin):
#disabling until I fix conflict with PLAIN
#self.xmpp.registerFeature("<auth xmlns='http://jabber.org/features/iq-auth'/>", self.auth)
self.streamid = ''
def check_stream(self, xml):
self.streamid = xml.attrib['id']
if xml.get('version', '0') != '1.0':
self.auth()
def auth(self, xml=None):
logging.debug("Starting jabber:iq:auth Authentication")
log.debug("Starting jabber:iq:auth Authentication")
auth_request = self.xmpp.makeIqGet()
auth_request_query = ET.Element('{jabber:iq:auth}query')
auth_request.attrib['to'] = self.xmpp.server
@@ -59,12 +50,12 @@ class xep_0078(base.base_plugin):
query.append(username)
query.append(resource)
if rquery.find('{jabber:iq:auth}digest') is None:
logging.warning("Authenticating via jabber:iq:auth Plain.")
log.warning("Authenticating via jabber:iq:auth Plain.")
password = ET.Element('password')
password.text = self.xmpp.password
query.append(password)
else:
logging.debug("Authenticating via jabber:iq:auth Digest")
log.debug("Authenticating via jabber:iq:auth Digest")
digest = ET.Element('digest')
digest.text = hashlib.sha1(b"%s%s" % (self.streamid, self.xmpp.password)).hexdigest()
query.append(digest)
@@ -76,6 +67,6 @@ class xep_0078(base.base_plugin):
self.xmpp.sessionstarted = True
self.xmpp.event("session_start")
else:
logging.info("Authentication failed")
log.info("Authentication failed")
self.xmpp.disconnect()
self.xmpp.event("failed_auth")

View File

@@ -0,0 +1,104 @@
"""
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 permissio
"""
import logging
from . import base
from .. xmlstream.handler.callback import Callback
from .. xmlstream.matcher.xpath import MatchXPath
from .. xmlstream.stanzabase import registerStanzaPlugin, ElementBase, ET, JID
from .. stanza.message import Message
log = logging.getLogger(__name__)
class ChatState(ElementBase):
namespace = 'http://jabber.org/protocol/chatstates'
plugin_attrib = 'chat_state'
interface = set(('state',))
states = set(('active', 'composing', 'gone', 'inactive', 'paused'))
def active(self):
self.setState('active')
def composing(self):
self.setState('composing')
def gone(self):
self.setState('gone')
def inactive(self):
self.setState('inactive')
def paused(self):
self.setState('paused')
def setState(self, state):
if state in self.states:
self.name = state
self.xml.tag = '{%s}%s' % (self.namespace, state)
else:
raise ValueError('Invalid chat state')
def getState(self):
return self.name
# In order to match the various chat state elements,
# we need one stanza object per state, even though
# they are all the same except for the initial name
# value. Do not depend on the type of the chat state
# stanza object for the actual state.
class Active(ChatState):
name = 'active'
class Composing(ChatState):
name = 'composing'
class Gone(ChatState):
name = 'gone'
class Inactive(ChatState):
name = 'inactive'
class Paused(ChatState):
name = 'paused'
class xep_0085(base.base_plugin):
"""
XEP-0085 Chat State Notifications
"""
def plugin_init(self):
self.xep = '0085'
self.description = 'Chat State Notifications'
handlers = [('Active Chat State', 'active'),
('Composing Chat State', 'composing'),
('Gone Chat State', 'gone'),
('Inactive Chat State', 'inactive'),
('Paused Chat State', 'paused')]
for handler in handlers:
self.xmpp.registerHandler(
Callback(handler[0],
MatchXPath("{%s}message/{%s}%s" % (self.xmpp.default_ns,
ChatState.namespace,
handler[1])),
self._handleChatState))
registerStanzaPlugin(Message, Active)
registerStanzaPlugin(Message, Composing)
registerStanzaPlugin(Message, Gone)
registerStanzaPlugin(Message, Inactive)
registerStanzaPlugin(Message, Paused)
def post_init(self):
base.base_plugin.post_init(self)
self.xmpp.plugin['xep_0030'].add_feature('http://jabber.org/protocol/chatstates')
def _handleChatState(self, msg):
state = msg['chat_state'].name
log.debug("Chat State: %s, %s" % (state, msg['from'].jid))
self.xmpp.event('chatstate_%s' % state, msg)

View File

@@ -1,21 +1,9 @@
"""
SleekXMPP: The Sleek XMPP Library
Copyright (C) 2007 Nathanael C. Fritz
This file is part of SleekXMPP.
SleekXMPP is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
SleekXMPP is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with SleekXMPP; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
SleekXMPP: The Sleek XMPP Library
Copyright (C) 2010 Nathanael C. Fritz
This file is part of SleekXMPP.
See the file LICENSE for copying permission.
"""
from xml.etree import cElementTree as ET
from . import base
@@ -30,7 +18,7 @@ class xep_0092(base.base_plugin):
self.xep = "0092"
self.name = self.config.get('name', 'SleekXMPP')
self.version = self.config.get('version', '0.1-dev')
self.xmpp.add_handler("<iq type='get' xmlns='%s'><query xmlns='jabber:iq:version' /></iq>" % self.xmpp.default_ns, self.report_version)
self.xmpp.add_handler("<iq type='get' xmlns='%s'><query xmlns='jabber:iq:version' /></iq>" % self.xmpp.default_ns, self.report_version, name='Sofware Version')
def post_init(self):
base.base_plugin.post_init(self)

View File

@@ -0,0 +1,51 @@
"""
SleekXMPP: The Sleek XMPP Library
Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout
This file is part of SleekXMPP.
See the file LICENSE for copying permission.
"""
import logging
from . import base
from .. xmlstream.handler.callback import Callback
from .. xmlstream.matcher.xpath import MatchXPath
from .. xmlstream.stanzabase import registerStanzaPlugin, ElementBase, ET, JID
from .. stanza.iq import Iq
from . xep_0030 import DiscoInfo, DiscoItems
from . xep_0004 import Form
class xep_0128(base.base_plugin):
"""
XEP-0128 Service Discovery Extensions
"""
def plugin_init(self):
self.xep = '0128'
self.description = 'Service Discovery Extensions'
registerStanzaPlugin(DiscoInfo, Form)
registerStanzaPlugin(DiscoItems, Form)
def extend_info(self, node, data=None):
if data is None:
data = {}
node = self.xmpp['xep_0030'].nodes.get(node, None)
if node is None:
self.xmpp['xep_0030'].add_node(node)
info = node.info
info['form']['type'] = 'result'
info['form'].setFields(data, default=None)
def extend_items(self, node, data=None):
if data is None:
data = {}
node = self.xmpp['xep_0030'].nodes.get(node, None)
if node is None:
self.xmpp['xep_0030'].add_node(node)
items = node.items
items['form']['type'] = 'result'
items['form'].setFields(data, default=None)

View File

@@ -1,72 +1,63 @@
"""
SleekXMPP: The Sleek XMPP Library
XEP-0199 (Ping) support
Copyright (C) 2007 Kevin Smith
This file is part of SleekXMPP.
SleekXMPP: The Sleek XMPP Library
Copyright (C) 2010 Nathanael C. Fritz
This file is part of SleekXMPP.
SleekXMPP is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
SleekXMPP is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with SleekXMPP; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
See the file LICENSE for copying permission.
"""
from xml.etree import cElementTree as ET
from . import base
import time
import logging
log = logging.getLogger(__name__)
class xep_0199(base.base_plugin):
"""XEP-0199 XMPP Ping"""
"""XEP-0199 XMPP Ping"""
def plugin_init(self):
self.description = "XMPP Ping"
self.xep = "0199"
self.xmpp.add_handler("<iq type='get' xmlns='%s'><ping xmlns='http://www.xmpp.org/extensions/xep-0199.html#ns'/></iq>" % self.xmpp.default_ns, self.handler_ping)
self.running = False
#if self.config.get('keepalive', True):
#self.xmpp.add_event_handler('session_start', self.handler_pingserver, threaded=True)
def post_init(self):
base.base_plugin.post_init(self)
self.xmpp.plugin['xep_0030'].add_feature('http://www.xmpp.org/extensions/xep-0199.html#ns')
def handler_pingserver(self, xml):
if not self.running:
time.sleep(self.config.get('frequency', 300))
while self.sendPing(self.xmpp.server, self.config.get('timeout', 30)) is not False:
time.sleep(self.config.get('frequency', 300))
logging.debug("Did not recieve ping back in time. Requesting Reconnect.")
self.xmpp.disconnect(reconnect=True)
def handler_ping(self, xml):
iq = self.xmpp.makeIqResult(xml.get('id', 'unknown'))
iq.attrib['to'] = xml.get('from', self.xmpp.server)
self.xmpp.send(iq)
def plugin_init(self):
self.description = "XMPP Ping"
self.xep = "0199"
self.xmpp.add_handler("<iq type='get' xmlns='%s'><ping xmlns='urn:xmpp:ping'/></iq>" % self.xmpp.default_ns, self.handler_ping, name='XMPP Ping')
if self.config.get('keepalive', True):
self.xmpp.add_event_handler('session_start', self.handler_pingserver, threaded=True)
def sendPing(self, jid, timeout = 30):
""" sendPing(jid, timeout)
Sends a ping to the specified jid, returning the time (in seconds)
to receive a reply, or None if no reply is received in timeout seconds.
"""
id = self.xmpp.getNewId()
iq = self.xmpp.makeIq(id)
iq.attrib['type'] = 'get'
iq.attrib['to'] = jid
ping = ET.Element('{http://www.xmpp.org/extensions/xep-0199.html#ns}ping')
iq.append(ping)
startTime = time.clock()
#pingresult = self.xmpp.send(iq, self.xmpp.makeIq(id), timeout)
pingresult = iq.send()
endTime = time.clock()
if pingresult == False:
#self.xmpp.disconnect(reconnect=True)
return False
return endTime - startTime
def post_init(self):
base.base_plugin.post_init(self)
self.xmpp.plugin['xep_0030'].add_feature('urn:xmpp:ping')
def handler_pingserver(self, xml):
self.xmpp.schedule("xep-0119 ping", float(self.config.get('frequency', 300)), self.scheduled_ping, repeat=True)
def scheduled_ping(self):
log.debug("pinging...")
if self.sendPing(self.xmpp.server, self.config.get('timeout', 30)) is False:
log.debug("Did not recieve ping back in time. Requesting Reconnect.")
self.xmpp.reconnect()
def handler_ping(self, xml):
iq = self.xmpp.makeIqResult(xml.get('id', 'unknown'))
iq.attrib['to'] = xml.get('from', self.xmpp.boundjid.domain)
self.xmpp.send(iq)
def sendPing(self, jid, timeout = 30):
""" sendPing(jid, timeout)
Sends a ping to the specified jid, returning the time (in seconds)
to receive a reply, or None if no reply is received in timeout seconds.
"""
id = self.xmpp.getNewId()
iq = self.xmpp.makeIq(id)
iq.attrib['type'] = 'get'
iq.attrib['to'] = jid
ping = ET.Element('{urn:xmpp:ping}ping')
iq.append(ping)
startTime = time.clock()
#pingresult = self.xmpp.send(iq, self.xmpp.makeIq(id), timeout)
pingresult = iq.send()
endTime = time.clock()
if pingresult == False:
#self.xmpp.disconnect(reconnect=True)
return False
return endTime - startTime

View File

@@ -0,0 +1,115 @@
"""
SleekXMPP: The Sleek XMPP Library
Copyright (C) 2010 Nathanael C. Fritz
This file is part of SleekXMPP.
See the file LICENSE for copying permission.
"""
from datetime import datetime, tzinfo
import logging
import time
from . import base
from .. stanza.iq import Iq
from .. xmlstream.handler.callback import Callback
from .. xmlstream.matcher.xpath import MatchXPath
from .. xmlstream import ElementBase, ET, JID, register_stanza_plugin
log = logging.getLogger(__name__)
class EntityTime(ElementBase):
name = 'time'
namespace = 'urn:xmpp:time'
plugin_attrib = 'entity_time'
interfaces = set(('tzo', 'utc'))
sub_interfaces = set(('tzo', 'utc'))
#def get_utc(self): # TODO: return a datetime.tzinfo object?
#pass
def set_tzo(self, tzo): # TODO: support datetime.tzinfo objects?
if isinstance(tzo, tzinfo):
td = datetime.now(tzo).utcoffset() # What if we are faking the time? datetime.now() shouldn't be used here'
seconds = td.seconds + td.days * 24 * 3600
sign = ('+' if seconds >= 0 else '-')
minutes = abs(seconds // 60)
tzo = '{sign}{hours:02d}:{minutes:02d}'.format(sign=sign, hours=minutes//60, minutes=minutes%60)
elif not isinstance(tzo, str):
raise TypeError('The time should be a string or a datetime.tzinfo object.')
self._set_sub_text('tzo', tzo)
def get_utc(self):
# Returns a datetime object instead the string. Is this a good idea?
value = self._get_sub_text('utc')
if '.' in value:
return datetime.strptime(value, '%Y-%m-%d.%fT%H:%M:%SZ')
else:
return datetime.strptime(value, '%Y-%m-%dT%H:%M:%SZ')
def set_utc(self, tim=None):
if isinstance(tim, datetime):
if tim.utcoffset():
tim = tim - tim.utcoffset()
tim = tim.strftime('%Y-%m-%dT%H:%M:%SZ')
elif isinstance(tim, time.struct_time):
tim = time.strftime('%Y-%m-%dT%H:%M:%SZ', tim)
elif not isinstance(tim, str):
raise TypeError('The time should be a string or a datetime.datetime or time.struct_time object.')
self._set_sub_text('utc', tim)
class xep_0202(base.base_plugin):
"""
XEP-0202 Entity Time
"""
def plugin_init(self):
self.description = "Entity Time"
self.xep = "0202"
self.xmpp.registerHandler(
Callback('Time Request',
MatchXPath('{%s}iq/{%s}time' % (self.xmpp.default_ns,
EntityTime.namespace)),
self.handle_entity_time_query))
register_stanza_plugin(Iq, EntityTime)
self.xmpp.add_event_handler('entity_time_request', self.handle_entity_time)
def post_init(self):
base.base_plugin.post_init(self)
self.xmpp.plugin['xep_0030'].add_feature('urn:xmpp:time')
def handle_entity_time_query(self, iq):
if iq['type'] == 'get':
log.debug("Entity time requested by %s" % iq['from'])
self.xmpp.event('entity_time_request', iq)
elif iq['type'] == 'result':
log.debug("Entity time result from %s" % iq['from'])
self.xmpp.event('entity_time', iq)
def handle_entity_time(self, iq):
iq = iq.reply()
iq.enable('entity_time')
tzo = time.strftime('%z') # %z is not on all ANSI C libraries
tzo = tzo[:3] + ':' + tzo[3:]
iq['entity_time']['tzo'] = tzo
iq['entity_time']['utc'] = datetime.utcnow()
iq.send()
def get_entity_time(self, jid):
iq = self.xmpp.makeIqGet()
iq.enable('entity_time')
iq.attrib['to'] = jid
iq.attrib['from'] = self.xmpp.boundjid.full
id = iq.get('id')
result = iq.send()
if result and result is not None and result.get('type', 'error') != 'error':
return {'utc': result['entity_time']['utc'], 'tzo': result['entity_time']['tzo']}
else:
return False

View File

@@ -3,6 +3,11 @@
Copyright (C) 2010 Nathanael C. Fritz
This file is part of SleekXMPP.
See the file license.txt for copying permission.
See the file LICENSE for copying permission.
"""
__all__ = ['presence']
from sleekxmpp.stanza.error import Error
from sleekxmpp.stanza.iq import Iq
from sleekxmpp.stanza.message import Message
from sleekxmpp.stanza.presence import Presence

26
sleekxmpp/stanza/atom.py Normal file
View File

@@ -0,0 +1,26 @@
"""
SleekXMPP: The Sleek XMPP Library
Copyright (C) 2010 Nathanael C. Fritz
This file is part of SleekXMPP.
See the file LICENSE for copying permission.
"""
from sleekxmpp.xmlstream import ElementBase
class AtomEntry(ElementBase):
"""
A simple Atom feed entry.
Stanza Interface:
title -- The title of the Atom feed entry.
summary -- The summary of the Atom feed entry.
"""
namespace = 'http://www.w3.org/2005/Atom'
name = 'entry'
plugin_attrib = 'entry'
interfaces = set(('title', 'summary'))
sub_interfaces = set(('title', 'summary'))

View File

@@ -3,60 +3,139 @@
Copyright (C) 2010 Nathanael C. Fritz
This file is part of SleekXMPP.
See the file license.txt for copying permission.
See the file LICENSE for copying permission.
"""
from .. xmlstream.stanzabase import ElementBase, ET
from sleekxmpp.xmlstream import ElementBase, ET, register_stanza_plugin
class Error(ElementBase):
namespace = 'jabber:client'
name = 'error'
plugin_attrib = 'error'
conditions = set(('bad-request', 'conflict', 'feature-not-implemented', 'forbidden', 'gone', 'internal-server-error', 'item-not-found', 'jid-malformed', 'not-acceptable', 'not-allowed', 'not-authorized', 'payment-required', 'recipient-unavailable', 'redirect', 'registration-required', 'remote-server-not-found', 'remote-server-timeout', 'resource-constraint', 'service-unavailable', 'subscription-required', 'undefined-condition', 'unexpected-request'))
interfaces = set(('code', 'condition', 'text', 'type'))
types = set(('cancel', 'continue', 'modify', 'auth', 'wait'))
sub_interfaces = set(('text',))
condition_ns = 'urn:ietf:params:xml:ns:xmpp-stanzas'
def setup(self, xml=None):
if ElementBase.setup(self, xml): #if we had to generate xml
self['type'] = 'cancel'
self['condition'] = 'feature-not-implemented'
if self.parent is not None:
self.parent()['type'] = 'error'
def getCondition(self):
for child in self.xml.getchildren():
if "{%s}" % self.condition_ns in child.tag:
return child.tag.split('}', 1)[-1]
return ''
def setCondition(self, value):
if value in self.conditions:
for child in self.xml.getchildren():
if "{%s}" % self.condition_ns in child.tag:
self.xml.remove(child)
condition = ET.Element("{%s}%s" % (self.condition_ns, value))
self.xml.append(condition)
return self
def delCondition(self):
return self
def getText(self):
text = ''
textxml = self.xml.find("{urn:ietf:params:xml:ns:xmpp-stanzas}text")
if textxml is not None:
text = textxml.text
return text
def setText(self, value):
self.delText()
textxml = ET.Element('{urn:ietf:params:xml:ns:xmpp-stanzas}text')
textxml.text = value
self.xml.append(textxml)
return self
def delText(self):
textxml = self.xml.find("{urn:ietf:params:xml:ns:xmpp-stanzas}text")
if textxml is not None:
self.xml.remove(textxml)
"""
XMPP stanzas of type 'error' should include an <error> stanza that
describes the nature of the error and how it should be handled.
Use the 'XEP-0086: Error Condition Mappings' plugin to include error
codes used in older XMPP versions.
Example error stanza:
<error type="cancel" code="404">
<item-not-found xmlns="urn:ietf:params:xml:ns:xmpp-stanzas" />
<text xmlns="urn:ietf:params:xml:ns:xmpp-stanzas">
The item was not found.
</text>
</error>
Stanza Interface:
code -- The error code used in older XMPP versions.
condition -- The name of the condition element.
text -- Human readable description of the error.
type -- Error type indicating how the error should be handled.
Attributes:
conditions -- The set of allowable error condition elements.
condition_ns -- The namespace for the condition element.
types -- A set of values indicating how the error
should be treated.
Methods:
setup -- Overrides ElementBase.setup.
get_condition -- Retrieve the name of the condition element.
set_condition -- Add a condition element.
del_condition -- Remove the condition element.
get_text -- Retrieve the contents of the <text> element.
set_text -- Set the contents of the <text> element.
del_text -- Remove the <text> element.
"""
namespace = 'jabber:client'
name = 'error'
plugin_attrib = 'error'
interfaces = set(('code', 'condition', 'text', 'type'))
sub_interfaces = set(('text',))
conditions = set(('bad-request', 'conflict', 'feature-not-implemented',
'forbidden', 'gone', 'internal-server-error',
'item-not-found', 'jid-malformed', 'not-acceptable',
'not-allowed', 'not-authorized', 'payment-required',
'recipient-unavailable', 'redirect',
'registration-required', 'remote-server-not-found',
'remote-server-timeout', 'resource-constraint',
'service-unavailable', 'subscription-required',
'undefined-condition', 'unexpected-request'))
condition_ns = 'urn:ietf:params:xml:ns:xmpp-stanzas'
types = set(('cancel', 'continue', 'modify', 'auth', 'wait'))
def setup(self, xml=None):
"""
Populate the stanza object using an optional XML object.
Overrides ElementBase.setup.
Sets a default error type and condition, and changes the
parent stanza's type to 'error'.
Arguments:
xml -- Use an existing XML object for the stanza's values.
"""
# To comply with PEP8, method names now use underscores.
# Deprecated method names are re-mapped for backwards compatibility.
self.getCondition = self.get_condition
self.setCondition = self.set_condition
self.delCondition = self.del_condition
self.getText = self.get_text
self.setText = self.set_text
self.delText = self.del_text
if ElementBase.setup(self, xml):
#If we had to generate XML then set default values.
self['type'] = 'cancel'
self['condition'] = 'feature-not-implemented'
if self.parent is not None:
self.parent()['type'] = 'error'
def get_condition(self):
"""Return the condition element's name."""
for child in self.xml.getchildren():
if "{%s}" % self.condition_ns in child.tag:
return child.tag.split('}', 1)[-1]
return ''
def set_condition(self, value):
"""
Set the tag name of the condition element.
Arguments:
value -- The tag name of the condition element.
"""
if value in self.conditions:
del self['condition']
self.xml.append(ET.Element("{%s}%s" % (self.condition_ns, value)))
return self
def del_condition(self):
"""Remove the condition element."""
for child in self.xml.getchildren():
if "{%s}" % self.condition_ns in child.tag:
tag = child.tag.split('}', 1)[-1]
if tag in self.conditions:
self.xml.remove(child)
return self
def get_text(self):
"""Retrieve the contents of the <text> element."""
return self._get_sub_text('{%s}text' % self.condition_ns)
def set_text(self, value):
"""
Set the contents of the <text> element.
Arguments:
value -- The new contents for the <text> element.
"""
self._set_sub_text('{%s}text' % self.condition_ns, text=value)
return self
def del_text(self):
"""Remove the <text> element."""
self._del_sub('{%s}text' % self.condition_ns)
return self

View File

@@ -3,33 +3,95 @@
Copyright (C) 2010 Nathanael C. Fritz
This file is part of SleekXMPP.
See the file license.txt for copying permission.
See the file LICENSE for copying permission.
"""
from .. xmlstream.stanzabase import ElementBase, ET
from sleekxmpp.stanza import Message
from sleekxmpp.xmlstream import ElementBase, ET, register_stanza_plugin
class HTMLIM(ElementBase):
namespace = 'http://jabber.org/protocol/xhtml-im'
name = 'html'
plugin_attrib = 'html'
interfaces = set(('html',))
plugin_attrib_map = set()
plugin_xml_map = set()
def setHtml(self, html):
if isinstance(html, str):
html = ET.XML(html)
if html.tag != '{http://www.w3.org/1999/xhtml}body':
body = ET.Element('{http://www.w3.org/1999/xhtml}body')
body.append(html)
self.xml.append(body)
else:
self.xml.append(html)
def getHtml(self):
html = self.xml.find('{http://www.w3.org/1999/xhtml}body')
if html is None: return ''
return html
def delHtml(self):
if self.parent is not None:
self.parent().xml.remove(self.xml)
"""
XEP-0071: XHTML-IM defines a method for embedding XHTML content
within a <message> stanza so that lightweight markup can be used
to format the message contents and to create links.
Only a subset of XHTML is recommended for use with XHTML-IM.
See the full spec at 'http://xmpp.org/extensions/xep-0071.html'
for more information.
Example stanza:
<message to="user@example.com">
<body>Non-html message content.</body>
<html xmlns="http://jabber.org/protocol/xhtml-im">
<body xmlns="http://www.w3.org/1999/xhtml">
<p><b>HTML!</b></p>
</body>
</html>
</message>
Stanza Interface:
body -- The contents of the HTML body tag.
Methods:
setup -- Overrides ElementBase.setup.
get_body -- Return the HTML body contents.
set_body -- Set the HTML body contents.
del_body -- Remove the HTML body contents.
"""
namespace = 'http://jabber.org/protocol/xhtml-im'
name = 'html'
interfaces = set(('body',))
plugin_attrib = name
def setup(self, xml=None):
"""
Populate the stanza object using an optional XML object.
Overrides StanzaBase.setup.
Arguments:
xml -- Use an existing XML object for the stanza's values.
"""
# To comply with PEP8, method names now use underscores.
# Deprecated method names are re-mapped for backwards compatibility.
self.setBody = self.set_body
self.getBody = self.get_body
self.delBody = self.del_body
return ElementBase.setup(self, xml)
def set_body(self, html):
"""
Set the contents of the HTML body.
Arguments:
html -- Either a string or XML object. If the top level
element is not <body> with a namespace of
'http://www.w3.org/1999/xhtml', it will be wrapped.
"""
if isinstance(html, str):
html = ET.XML(html)
if html.tag != '{http://www.w3.org/1999/xhtml}body':
body = ET.Element('{http://www.w3.org/1999/xhtml}body')
body.append(html)
self.xml.append(body)
else:
self.xml.append(html)
def get_body(self):
"""Return the contents of the HTML body."""
html = self.xml.find('{http://www.w3.org/1999/xhtml}body')
if html is None:
return ''
return html
def del_body(self):
"""Remove the HTML body contents."""
if self.parent is not None:
self.parent().xml.remove(self.xml)
register_stanza_plugin(Message, HTMLIM)

View File

@@ -3,75 +3,181 @@
Copyright (C) 2010 Nathanael C. Fritz
This file is part of SleekXMPP.
See the file license.txt for copying permission.
See the file LICENSE for copying permission.
"""
from .. xmlstream.stanzabase import StanzaBase
from xml.etree import cElementTree as ET
from . error import Error
from .. xmlstream.handler.waiter import Waiter
from .. xmlstream.matcher.id import MatcherId
from . rootstanza import RootStanza
from sleekxmpp.stanza import Error
from sleekxmpp.stanza.rootstanza import RootStanza
from sleekxmpp.xmlstream import RESPONSE_TIMEOUT, StanzaBase, ET
from sleekxmpp.xmlstream.handler import Waiter
from sleekxmpp.xmlstream.matcher import MatcherId
class Iq(RootStanza):
interfaces = set(('type', 'to', 'from', 'id','query'))
types = set(('get', 'result', 'set', 'error'))
name = 'iq'
plugin_attrib = name
namespace = 'jabber:client'
def __init__(self, *args, **kwargs):
StanzaBase.__init__(self, *args, **kwargs)
if self['id'] == '':
if self.stream is not None:
self['id'] = self.stream.getNewId()
else:
self['id'] = '0'
def unhandled(self):
if self['type'] in ('get', 'set'):
self.reply()
self['error']['condition'] = 'feature-not-implemented'
self['error']['text'] = 'No handlers registered for this request.'
self.send()
def setPayload(self, value):
self.clear()
StanzaBase.setPayload(self, value)
return self
def setQuery(self, value):
query = self.xml.find("{%s}query" % value)
if query is None and value:
self.clear()
query = ET.Element("{%s}query" % value)
self.xml.append(query)
return self
def getQuery(self):
for child in self.xml.getchildren():
if child.tag.endswith('query'):
ns =child.tag.split('}')[0]
if '{' in ns:
ns = ns[1:]
return ns
return ''
def reply(self):
self['type'] = 'result'
StanzaBase.reply(self)
return self
def delQuery(self):
for child in self.getchildren():
if child.tag.endswith('query'):
self.xml.remove(child)
return self
def send(self, block=True, timeout=10):
if block and self['type'] in ('get', 'set'):
waitfor = Waiter('IqWait_%s' % self['id'], MatcherId(self['id']))
self.stream.registerHandler(waitfor)
StanzaBase.send(self)
return waitfor.wait(timeout)
else:
return StanzaBase.send(self)
"""
XMPP <iq> stanzas, or info/query stanzas, are XMPP's method of
requesting and modifying information, similar to HTTP's GET and
POST methods.
Each <iq> stanza must have an 'id' value which associates the
stanza with the response stanza. XMPP entities must always
be given a response <iq> stanza with a type of 'result' after
sending a stanza of type 'get' or 'set'.
Most uses cases for <iq> stanzas will involve adding a <query>
element whose namespace indicates the type of information
desired. However, some custom XMPP applications use <iq> stanzas
as a carrier stanza for an application-specific protocol instead.
Example <iq> Stanzas:
<iq to="user@example.com" type="get" id="314">
<query xmlns="http://jabber.org/protocol/disco#items" />
</iq>
<iq to="user@localhost" type="result" id="17">
<query xmlns='jabber:iq:roster'>
<item jid='otheruser@example.net'
name='John Doe'
subscription='both'>
<group>Friends</group>
</item>
</query>
</iq>
Stanza Interface:
query -- The namespace of the <query> element if one exists.
Attributes:
types -- May be one of: get, set, result, or error.
Methods:
__init__ -- Overrides StanzaBase.__init__.
unhandled -- Send error if there are no handlers.
set_payload -- Overrides StanzaBase.set_payload.
set_query -- Add or modify a <query> element.
get_query -- Return the namespace of the <query> element.
del_query -- Remove the <query> element.
reply -- Overrides StanzaBase.reply
send -- Overrides StanzaBase.send
"""
namespace = 'jabber:client'
name = 'iq'
interfaces = set(('type', 'to', 'from', 'id', 'query'))
types = set(('get', 'result', 'set', 'error'))
plugin_attrib = name
def __init__(self, *args, **kwargs):
"""
Initialize a new <iq> stanza with an 'id' value.
Overrides StanzaBase.__init__.
"""
StanzaBase.__init__(self, *args, **kwargs)
# To comply with PEP8, method names now use underscores.
# Deprecated method names are re-mapped for backwards compatibility.
self.setPayload = self.set_payload
self.getQuery = self.get_query
self.setQuery = self.set_query
self.delQuery = self.del_query
if self['id'] == '':
if self.stream is not None:
self['id'] = self.stream.getNewId()
else:
self['id'] = '0'
def unhandled(self):
"""
Send a feature-not-implemented error if the stanza is not handled.
Overrides StanzaBase.unhandled.
"""
if self['type'] in ('get', 'set'):
self.reply()
self['error']['condition'] = 'feature-not-implemented'
self['error']['text'] = 'No handlers registered for this request.'
self.send()
def set_payload(self, value):
"""
Set the XML contents of the <iq> stanza.
Arguments:
value -- An XML object to use as the <iq> stanza's contents
"""
self.clear()
StanzaBase.set_payload(self, value)
return self
def set_query(self, value):
"""
Add or modify a <query> element.
Query elements are differentiated by their namespace.
Arguments:
value -- The namespace of the <query> element.
"""
query = self.xml.find("{%s}query" % value)
if query is None and value:
self.clear()
query = ET.Element("{%s}query" % value)
self.xml.append(query)
return self
def get_query(self):
"""Return the namespace of the <query> element."""
for child in self.xml.getchildren():
if child.tag.endswith('query'):
ns = child.tag.split('}')[0]
if '{' in ns:
ns = ns[1:]
return ns
return ''
def del_query(self):
"""Remove the <query> element."""
for child in self.xml.getchildren():
if child.tag.endswith('query'):
self.xml.remove(child)
return self
def reply(self):
"""
Send a reply <iq> stanza.
Overrides StanzaBase.reply
Sets the 'type' to 'result' in addition to the default
StanzaBase.reply behavior.
"""
self['type'] = 'result'
StanzaBase.reply(self)
return self
def send(self, block=True, timeout=RESPONSE_TIMEOUT):
"""
Send an <iq> stanza over the XML stream.
The send call can optionally block until a response is received or
a timeout occurs. Be aware that using blocking in non-threaded event
handlers can drastically impact performance.
Overrides StanzaBase.send
Arguments:
block -- Specify if the send call will block until a response
is received, or a timeout occurs. Defaults to True.
timeout -- The length of time (in seconds) to wait for a response
before exiting the send call if blocking is used.
Defaults to sleekxmpp.xmlstream.RESPONSE_TIMEOUT
"""
if block and self['type'] in ('get', 'set'):
waitfor = Waiter('IqWait_%s' % self['id'], MatcherId(self['id']))
self.stream.registerHandler(waitfor)
StanzaBase.send(self)
return waitfor.wait(timeout)
else:
return StanzaBase.send(self)

View File

@@ -3,61 +3,163 @@
Copyright (C) 2010 Nathanael C. Fritz
This file is part of SleekXMPP.
See the file license.txt for copying permission.
See the file LICENSE for copying permission.
"""
from .. xmlstream.stanzabase import StanzaBase
from xml.etree import cElementTree as ET
from . error import Error
from . rootstanza import RootStanza
from sleekxmpp.stanza import Error
from sleekxmpp.stanza.rootstanza import RootStanza
from sleekxmpp.xmlstream import StanzaBase, ET
class Message(RootStanza):
interfaces = set(('type', 'to', 'from', 'id', 'body', 'subject', 'mucroom', 'mucnick'))
types = set((None, 'normal', 'chat', 'headline', 'error', 'groupchat'))
sub_interfaces = set(('body', 'subject'))
name = 'message'
plugin_attrib = name
namespace = 'jabber:client'
def getType(self):
return self.xml.attrib.get('type', 'normal')
def chat(self):
self['type'] = 'chat'
return self
def normal(self):
self['type'] = 'normal'
return self
def reply(self, body=None):
StanzaBase.reply(self)
if self['type'] == 'groupchat':
self['to'] = self['to'].bare
del self['id']
if body is not None:
self['body'] = body
return self
def getMucroom(self):
if self['type'] == 'groupchat':
return self['from'].bare
else:
return ''
def setMucroom(self, value):
pass
def delMucroom(self):
pass
def getMucnick(self):
if self['type'] == 'groupchat':
return self['from'].resource
else:
return ''
def setMucnick(self, value):
pass
def delMucnick(self):
pass
"""
XMPP's <message> stanzas are a "push" mechanism to send information
to other XMPP entities without requiring a response.
Chat clients will typically use <message> stanzas that have a type
of either "chat" or "groupchat".
When handling a message event, be sure to check if the message is
an error response.
Example <message> stanzas:
<message to="user1@example.com" from="user2@example.com">
<body>Hi!</body>
</message>
<message type="groupchat" to="room@conference.example.com">
<body>Hi everyone!</body>
</message>
Stanza Interface:
body -- The main contents of the message.
subject -- An optional description of the message's contents.
mucroom -- (Read-only) The name of the MUC room that sent the message.
mucnick -- (Read-only) The MUC nickname of message's sender.
Attributes:
types -- May be one of: normal, chat, headline, groupchat, or error.
Methods:
setup -- Overrides StanzaBase.setup.
chat -- Set the message type to 'chat'.
normal -- Set the message type to 'normal'.
reply -- Overrides StanzaBase.reply
get_type -- Overrides StanzaBase interface
get_mucroom -- Return the name of the MUC room of the message.
set_mucroom -- Dummy method to prevent assignment.
del_mucroom -- Dummy method to prevent deletion.
get_mucnick -- Return the MUC nickname of the message's sender.
set_mucnick -- Dummy method to prevent assignment.
del_mucnick -- Dummy method to prevent deletion.
"""
namespace = 'jabber:client'
name = 'message'
interfaces = set(('type', 'to', 'from', 'id', 'body', 'subject',
'mucroom', 'mucnick'))
sub_interfaces = set(('body', 'subject'))
plugin_attrib = name
types = set((None, 'normal', 'chat', 'headline', 'error', 'groupchat'))
def setup(self, xml=None):
"""
Populate the stanza object using an optional XML object.
Overrides StanzaBase.setup.
Arguments:
xml -- Use an existing XML object for the stanza's values.
"""
# To comply with PEP8, method names now use underscores.
# Deprecated method names are re-mapped for backwards compatibility.
self.getType = self.get_type
self.getMucroom = self.get_mucroom
self.setMucroom = self.set_mucroom
self.delMucroom = self.del_mucroom
self.getMucnick = self.get_mucnick
self.setMucnick = self.set_mucnick
self.delMucnick = self.del_mucnick
return StanzaBase.setup(self, xml)
def get_type(self):
"""
Return the message type.
Overrides default stanza interface behavior.
Returns 'normal' if no type attribute is present.
"""
return self._get_attr('type', 'normal')
def chat(self):
"""Set the message type to 'chat'."""
self['type'] = 'chat'
return self
def normal(self):
"""Set the message type to 'chat'."""
self['type'] = 'normal'
return self
def reply(self, body=None):
"""
Create a message reply.
Overrides StanzaBase.reply.
Sets proper 'to' attribute if the message is from a MUC, and
adds a message body if one is given.
Arguments:
body -- Optional text content for the message.
"""
StanzaBase.reply(self)
if self['type'] == 'groupchat':
self['to'] = self['to'].bare
del self['id']
if body is not None:
self['body'] = body
return self
def get_mucroom(self):
"""
Return the name of the MUC room where the message originated.
Read-only stanza interface.
"""
if self['type'] == 'groupchat':
return self['from'].bare
else:
return ''
def get_mucnick(self):
"""
Return the nickname of the MUC user that sent the message.
Read-only stanza interface.
"""
if self['type'] == 'groupchat':
return self['from'].resource
else:
return ''
def set_mucroom(self, value):
"""Dummy method to prevent modification."""
pass
def del_mucroom(self):
"""Dummy method to prevent deletion."""
pass
def set_mucnick(self, value):
"""Dummy method to prevent modification."""
pass
def del_mucnick(self):
"""Dummy method to prevent deletion."""
pass

View File

@@ -3,24 +3,87 @@
Copyright (C) 2010 Nathanael C. Fritz
This file is part of SleekXMPP.
See the file license.txt for copying permission.
See the file LICENSE for copying permission.
"""
from .. xmlstream.stanzabase import ElementBase, ET
from sleekxmpp.stanza import Message, Presence
from sleekxmpp.xmlstream import ElementBase, ET, register_stanza_plugin
class Nick(ElementBase):
namespace = 'http://jabber.org/nick/nick'
name = 'nick'
plugin_attrib = 'nick'
interfaces = set(('nick'))
plugin_attrib_map = set()
plugin_xml_map = set()
def setNick(self, nick):
self.xml.text = nick
def getNick(self):
return self.xml.text
def delNick(self):
if self.parent is not None:
self.parent().xml.remove(self.xml)
"""
XEP-0172: User Nickname allows the addition of a <nick> element
in several stanza types, including <message> and <presence> stanzas.
The nickname contained in a <nick> should be the global, friendly or
informal name chosen by the owner of a bare JID. The <nick> element
may be included when establishing communications with new entities,
such as normal XMPP users or MUC services.
The nickname contained in a <nick> element will not necessarily be
the same as the nickname used in a MUC.
Example stanzas:
<message to="user@example.com">
<nick xmlns="http://jabber.org/nick/nick">The User</nick>
<body>...</body>
</message>
<presence to="otheruser@example.com" type="subscribe">
<nick xmlns="http://jabber.org/nick/nick">The User</nick>
</presence>
Stanza Interface:
nick -- A global, friendly or informal name chosen by a user.
Methods:
setup -- Overrides ElementBase.setup.
get_nick -- Return the nickname in the <nick> element.
set_nick -- Add a <nick> element with the given nickname.
del_nick -- Remove the <nick> element.
"""
namespace = 'http://jabber.org/nick/nick'
name = 'nick'
plugin_attrib = name
interfaces = set(('nick',))
def setup(self, xml=None):
"""
Populate the stanza object using an optional XML object.
Overrides StanzaBase.setup.
Arguments:
xml -- Use an existing XML object for the stanza's values.
"""
# To comply with PEP8, method names now use underscores.
# Deprecated method names are re-mapped for backwards compatibility.
self.setNick = self.set_nick
self.getNick = self.get_nick
self.delNick = self.del_nick
return ElementBase.setup(self, xml)
def set_nick(self, nick):
"""
Add a <nick> element with the given nickname.
Arguments:
nick -- A human readable, informal name.
"""
self.xml.text = nick
def get_nick(self):
"""Return the nickname in the <nick> element."""
return self.xml.text
def del_nick(self):
"""Remove the <nick> element."""
if self.parent is not None:
self.parent().xml.remove(self.xml)
register_stanza_plugin(Message, Nick)
register_stanza_plugin(Presence, Nick)

View File

@@ -3,61 +3,184 @@
Copyright (C) 2010 Nathanael C. Fritz
This file is part of SleekXMPP.
See the file license.txt for copying permission.
See the file LICENSE for copying permission.
"""
from .. xmlstream.stanzabase import StanzaBase
from xml.etree import cElementTree as ET
from . error import Error
from . rootstanza import RootStanza
from sleekxmpp.stanza import Error
from sleekxmpp.stanza.rootstanza import RootStanza
from sleekxmpp.xmlstream import StanzaBase, ET
class Presence(RootStanza):
interfaces = set(('type', 'to', 'from', 'id', 'status', 'priority'))
types = set(('available', 'unavailable', 'error', 'probe', 'subscribe', 'subscribed', 'unsubscribe', 'unsubscribed'))
showtypes = set(('dnd', 'chat', 'xa', 'away'))
sub_interfaces = set(('status', 'priority'))
name = 'presence'
plugin_attrib = name
namespace = 'jabber:client'
def getShowElement(self):
return self.xml.find("{%s}show" % self.namespace)
"""
XMPP's <presence> stanza allows entities to know the status of other
clients and components. Since it is currently the only multi-cast
stanza in XMPP, many extensions add more information to <presence>
stanzas to broadcast to every entry in the roster, such as
capabilities, music choices, or locations (XEP-0115: Entity Capabilities
and XEP-0163: Personal Eventing Protocol).
def setType(self, value):
show = self.getShowElement()
if value in self.types:
if show is not None:
self.xml.remove(show)
if value == 'available':
value = ''
self._setAttr('type', value)
elif value in self.showtypes:
if show is None:
show = ET.Element("{%s}show" % self.namespace)
self.xml.append(show)
show.text = value
return self
Since <presence> stanzas are broadcast when an XMPP entity changes
its status, the bulk of the traffic in an XMPP network will be from
<presence> stanzas. Therefore, do not include more information than
necessary in a status message or within a <presence> stanza in order
to help keep the network running smoothly.
def setPriority(self, value):
self._setSubText('priority', text = str(value))
def getPriority(self):
p = self._getSubText('priority')
if not p: p = 0
return int(p)
def getType(self):
out = self._getAttr('type')
if not out:
show = self.getShowElement()
if show is not None:
out = show.text
if not out or out is None:
out = 'available'
return out
def reply(self):
if self['type'] == 'unsubscribe':
self['type'] = 'unsubscribed'
elif self['type'] == 'subscribe':
self['type'] = 'subscribed'
return StanzaBase.reply(self)
Example <presence> stanzas:
<presence />
<presence from="user@example.com">
<show>away</show>
<status>Getting lunch.</status>
<priority>5</priority>
</presence>
<presence type="unavailable" />
<presence to="user@otherhost.com" type="subscribe" />
Stanza Interface:
priority -- A value used by servers to determine message routing.
show -- The type of status, such as away or available for chat.
status -- Custom, human readable status message.
Attributes:
types -- One of: available, unavailable, error, probe,
subscribe, subscribed, unsubscribe,
and unsubscribed.
showtypes -- One of: away, chat, dnd, and xa.
Methods:
setup -- Overrides StanzaBase.setup
reply -- Overrides StanzaBase.reply
set_show -- Set the value of the <show> element.
get_type -- Get the value of the type attribute or <show> element.
set_type -- Set the value of the type attribute or <show> element.
get_priority -- Get the value of the <priority> element.
set_priority -- Set the value of the <priority> element.
"""
namespace = 'jabber:client'
name = 'presence'
interfaces = set(('type', 'to', 'from', 'id', 'show',
'status', 'priority'))
sub_interfaces = set(('show', 'status', 'priority'))
plugin_attrib = name
types = set(('available', 'unavailable', 'error', 'probe', 'subscribe',
'subscribed', 'unsubscribe', 'unsubscribed'))
showtypes = set(('dnd', 'chat', 'xa', 'away'))
def setup(self, xml=None):
"""
Populate the stanza object using an optional XML object.
Overrides ElementBase.setup.
Arguments:
xml -- Use an existing XML object for the stanza's values.
"""
# To comply with PEP8, method names now use underscores.
# Deprecated method names are re-mapped for backwards compatibility.
self.setShow = self.set_show
self.getType = self.get_type
self.setType = self.set_type
self.delType = self.get_type
self.getPriority = self.get_priority
self.setPriority = self.set_priority
return StanzaBase.setup(self, xml)
def exception(self, e):
"""
Override exception passback for presence.
"""
pass
def set_show(self, show):
"""
Set the value of the <show> element.
Arguments:
show -- Must be one of: away, chat, dnd, or xa.
"""
if show is None:
self._del_sub('show')
elif show in self.showtypes:
self._set_sub_text('show', text=show)
return self
def get_type(self):
"""
Return the value of the <presence> stanza's type attribute, or
the value of the <show> element.
"""
out = self._get_attr('type')
if not out:
out = self['show']
if not out or out is None:
out = 'available'
return out
def set_type(self, value):
"""
Set the type attribute's value, and the <show> element
if applicable.
Arguments:
value -- Must be in either self.types or self.showtypes.
"""
if value in self.types:
self['show'] = None
if value == 'available':
value = ''
self._set_attr('type', value)
elif value in self.showtypes:
self['show'] = value
return self
def del_type(self):
"""
Remove both the type attribute and the <show> element.
"""
self._del_attr('type')
self._del_sub('show')
def set_priority(self, value):
"""
Set the entity's priority value. Some server use priority to
determine message routing behavior.
Bot clients should typically use a priority of 0 if the same
JID is used elsewhere by a human-interacting client.
Arguments:
value -- An integer value greater than or equal to 0.
"""
self._set_sub_text('priority', text=str(value))
def get_priority(self):
"""
Return the value of the <presence> element as an integer.
"""
p = self._get_sub_text('priority')
if not p:
p = 0
try:
return int(p)
except ValueError:
# The priority is not a number: we consider it 0 as a default
return 0
def reply(self):
"""
Set the appropriate presence reply type.
Overrides StanzaBase.reply.
"""
if self['type'] == 'unsubscribe':
self['type'] = 'unsubscribed'
elif self['type'] == 'subscribe':
self['type'] = 'subscribed'
return StanzaBase.reply(self)

View File

@@ -3,34 +3,67 @@
Copyright (C) 2010 Nathanael C. Fritz
This file is part of SleekXMPP.
See the file license.txt for copying permission.
See the file LICENSE for copying permission.
"""
from .. xmlstream.stanzabase import StanzaBase
from xml.etree import cElementTree as ET
from . error import Error
from .. exceptions import XMPPError
import logging
import traceback
import sys
from sleekxmpp.exceptions import XMPPError
from sleekxmpp.stanza import Error
from sleekxmpp.xmlstream import ET, StanzaBase, register_stanza_plugin
log = logging.getLogger(__name__)
class RootStanza(StanzaBase):
def exception(self, e): #called when a handler raises an exception
self.reply()
if isinstance(e, XMPPError): # we raised this deliberately
self['error']['condition'] = e.condition
self['error']['text'] = e.text
if e.extension is not None: # extended error tag
extxml = ET.Element("{%s}%s" % (e.extension_ns, e.extension), e.extension_args)
self['error'].xml.append(extxml)
self['error']['type'] = e.etype
else: # we probably didn't raise this on purpose, so send back a traceback
self['error']['condition'] = 'undefined-condition'
if sys.version_info < (3,0):
self['error']['text'] = "SleekXMPP got into trouble."
else:
self['error']['text'] = traceback.format_tb(e.__traceback__)
self.send()
"""
A top-level XMPP stanza in an XMLStream.
# all jabber:client root stanzas should have the error plugin
RootStanza.plugin_attrib_map['error'] = Error
RootStanza.plugin_tag_map["{%s}%s" % (Error.namespace, Error.name)] = Error
The RootStanza class provides a more XMPP specific exception
handler than provided by the generic StanzaBase class.
Methods:
exception -- Overrides StanzaBase.exception
"""
def exception(self, e):
"""
Create and send an error reply.
Typically called when an event handler raises an exception.
The error's type and text content are based on the exception
object's type and content.
Overrides StanzaBase.exception.
Arguments:
e -- Exception object
"""
self.reply()
if isinstance(e, XMPPError):
# We raised this deliberately
self['error']['condition'] = e.condition
self['error']['text'] = e.text
if e.extension is not None:
# Extended error tag
extxml = ET.Element("{%s}%s" % (e.extension_ns, e.extension),
e.extension_args)
self['error'].append(extxml)
self['error']['type'] = e.etype
else:
# We probably didn't raise this on purpose, so send a traceback
self['error']['condition'] = 'undefined-condition'
if sys.version_info < (3, 0):
self['error']['text'] = "SleekXMPP got into trouble."
else:
self['error']['text'] = traceback.format_tb(e.__traceback__)
log.exception('Error handling {%s}%s stanza' %
(self.namespace, self.name))
self.send()
register_stanza_plugin(RootStanza, Error)

View File

@@ -3,51 +3,123 @@
Copyright (C) 2010 Nathanael C. Fritz
This file is part of SleekXMPP.
See the file license.txt for copying permission.
See the file LICENSE for copying permission.
"""
from .. xmlstream.stanzabase import ElementBase, ET, JID
import logging
from sleekxmpp.stanza import Iq
from sleekxmpp.xmlstream import JID
from sleekxmpp.xmlstream import ET, ElementBase, register_stanza_plugin
class Roster(ElementBase):
namespace = 'jabber:iq:roster'
name = 'query'
plugin_attrib = 'roster'
interfaces = set(('items',))
sub_interfaces = set()
def setItems(self, items):
self.delItems()
for jid in items:
ijid = str(jid)
item = ET.Element('{jabber:iq:roster}item', {'jid': ijid})
if 'subscription' in items[jid]:
item.attrib['subscription'] = items[jid]['subscription']
if 'name' in items[jid]:
item.attrib['name'] = items[jid]['name']
if 'groups' in items[jid]:
for group in items[jid]['groups']:
groupxml = ET.Element('{jabber:iq:roster}group')
groupxml.text = group
item.append(groupxml)
self.xml.append(item)
return self
def getItems(self):
items = {}
itemsxml = self.xml.findall('{jabber:iq:roster}item')
if itemsxml is not None:
for itemxml in itemsxml:
item = {}
item['name'] = itemxml.get('name', '')
item['subscription'] = itemxml.get('subscription', '')
item['groups'] = []
groupsxml = itemxml.findall('{jabber:iq:roster}group')
if groupsxml is not None:
for groupxml in groupsxml:
item['groups'].append(groupxml.text)
items[itemxml.get('jid')] = item
return items
def delItems(self):
for child in self.xml.getchildren():
self.xml.remove(child)
"""
Example roster stanzas:
<iq type="set">
<query xmlns="jabber:iq:roster">
<item jid="user@example.com" subscription="both" name="User">
<group>Friends</group>
</item>
</query>
</iq>
Stanza Inteface:
items -- A dictionary of roster entries contained
in the stanza.
Methods:
get_items -- Return a dictionary of roster entries.
set_items -- Add <item> elements.
del_items -- Remove all <item> elements.
"""
namespace = 'jabber:iq:roster'
name = 'query'
plugin_attrib = 'roster'
interfaces = set(('items',))
def setup(self, xml=None):
"""
Populate the stanza object using an optional XML object.
Overrides StanzaBase.setup.
Arguments:
xml -- Use an existing XML object for the stanza's values.
"""
# To comply with PEP8, method names now use underscores.
# Deprecated method names are re-mapped for backwards compatibility.
self.setItems = self.set_items
self.getItems = self.get_items
self.delItems = self.del_items
return ElementBase.setup(self, xml)
def set_items(self, items):
"""
Set the roster entries in the <roster> stanza.
Uses a dictionary using JIDs as keys, where each entry is itself
a dictionary that contains:
name -- An alias or nickname for the JID.
subscription -- The subscription type. Can be one of 'to',
'from', 'both', 'none', or 'remove'.
groups -- A list of group names to which the JID
has been assigned.
Arguments:
items -- A dictionary of roster entries.
"""
self.del_items()
for jid in items:
ijid = str(jid)
item = ET.Element('{jabber:iq:roster}item', {'jid': ijid})
if 'subscription' in items[jid]:
item.attrib['subscription'] = items[jid]['subscription']
if 'name' in items[jid]:
name = items[jid]['name']
if name is not None:
item.attrib['name'] = name
if 'groups' in items[jid]:
for group in items[jid]['groups']:
groupxml = ET.Element('{jabber:iq:roster}group')
groupxml.text = group
item.append(groupxml)
self.xml.append(item)
return self
def get_items(self):
"""
Return a dictionary of roster entries.
Each item is keyed using its JID, and contains:
name -- An assigned alias or nickname for the JID.
subscription -- The subscription type. Can be one of 'to',
'from', 'both', 'none', or 'remove'.
groups -- A list of group names to which the JID has
been assigned.
"""
items = {}
itemsxml = self.xml.findall('{jabber:iq:roster}item')
if itemsxml is not None:
for itemxml in itemsxml:
item = {}
item['name'] = itemxml.get('name', '')
item['subscription'] = itemxml.get('subscription', '')
item['groups'] = []
groupsxml = itemxml.findall('{jabber:iq:roster}group')
if groupsxml is not None:
for groupxml in groupsxml:
item['groups'].append(groupxml.text)
items[itemxml.get('jid')] = item
return items
def del_items(self):
"""
Remove all <item> elements from the roster stanza.
"""
for child in self.xml.getchildren():
self.xml.remove(child)
register_stanza_plugin(Iq, Roster)

View File

@@ -0,0 +1,11 @@
"""
SleekXMPP: The Sleek XMPP Library
Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout
This file is part of SleekXMPP.
See the file LICENSE for copying permission.
"""
from sleekxmpp.test.mocksocket import TestSocket
from sleekxmpp.test.livesocket import TestLiveSocket
from sleekxmpp.test.sleektest import *

View File

@@ -0,0 +1,145 @@
"""
SleekXMPP: The Sleek XMPP Library
Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout
This file is part of SleekXMPP.
See the file LICENSE for copying permission.
"""
import socket
try:
import queue
except ImportError:
import Queue as queue
class TestLiveSocket(object):
"""
A live test socket that reads and writes to queues in
addition to an actual networking socket.
Methods:
next_sent -- Return the next sent stanza.
next_recv -- Return the next received stanza.
recv_data -- Dummy method to have same interface as TestSocket.
recv -- Read the next stanza from the socket.
send -- Write a stanza to the socket.
makefile -- Dummy call, returns self.
read -- Read the next stanza from the socket.
"""
def __init__(self, *args, **kwargs):
"""
Create a new, live test socket.
Arguments:
Same as arguments for socket.socket
"""
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.recv_buffer = []
self.recv_queue = queue.Queue()
self.send_queue = queue.Queue()
self.is_live = True
def __getattr__(self, name):
"""
Return attribute values of internal, live socket.
Arguments:
name -- Name of the attribute requested.
"""
return getattr(self.socket, name)
# ------------------------------------------------------------------
# Testing Interface
def next_sent(self, timeout=None):
"""
Get the next stanza that has been sent.
Arguments:
timeout -- Optional timeout for waiting for a new value.
"""
args = {'block': False}
if timeout is not None:
args = {'block': True, 'timeout': timeout}
try:
return self.send_queue.get(**args)
except:
return None
def next_recv(self, timeout=None):
"""
Get the next stanza that has been received.
Arguments:
timeout -- Optional timeout for waiting for a new value.
"""
args = {'block': False}
if timeout is not None:
args = {'block': True, 'timeout': timeout}
try:
if self.recv_buffer:
return self.recv_buffer.pop(0)
else:
return self.recv_queue.get(**args)
except:
return None
def recv_data(self, data):
"""
Add data to a receive buffer for cases when more than a single stanza
was received.
"""
self.recv_buffer.append(data)
# ------------------------------------------------------------------
# Socket Interface
def recv(self, *args, **kwargs):
"""
Read data from the socket.
Store a copy in the receive queue.
Arguments:
Placeholders. Same as for socket.recv.
"""
data = self.socket.recv(*args, **kwargs)
self.recv_queue.put(data)
return data
def send(self, data):
"""
Send data on the socket.
Store a copy in the send queue.
Arguments:
data -- String value to write.
"""
self.send_queue.put(data)
self.socket.send(data)
# ------------------------------------------------------------------
# File Socket
def makefile(self, *args, **kwargs):
"""
File socket version to use with ElementTree.
Arguments:
Placeholders, same as socket.makefile()
"""
return self
def read(self, *args, **kwargs):
"""
Implement the file socket read interface.
Arguments:
Placeholders, same as socket.recv()
"""
return self.recv(*args, **kwargs)

View File

@@ -0,0 +1,140 @@
"""
SleekXMPP: The Sleek XMPP Library
Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout
This file is part of SleekXMPP.
See the file LICENSE for copying permission.
"""
import socket
try:
import queue
except ImportError:
import Queue as queue
class TestSocket(object):
"""
A dummy socket that reads and writes to queues instead
of an actual networking socket.
Methods:
next_sent -- Return the next sent stanza.
recv_data -- Make a stanza available to read next.
recv -- Read the next stanza from the socket.
send -- Write a stanza to the socket.
makefile -- Dummy call, returns self.
read -- Read the next stanza from the socket.
"""
def __init__(self, *args, **kwargs):
"""
Create a new test socket.
Arguments:
Same as arguments for socket.socket
"""
self.socket = socket.socket(*args, **kwargs)
self.recv_queue = queue.Queue()
self.send_queue = queue.Queue()
self.is_live = False
def __getattr__(self, name):
"""
Return attribute values of internal, dummy socket.
Some attributes and methods are disabled to prevent the
socket from connecting to the network.
Arguments:
name -- Name of the attribute requested.
"""
def dummy(*args):
"""Method to do nothing and prevent actual socket connections."""
return None
overrides = {'connect': dummy,
'close': dummy,
'shutdown': dummy}
return overrides.get(name, getattr(self.socket, name))
# ------------------------------------------------------------------
# Testing Interface
def next_sent(self, timeout=None):
"""
Get the next stanza that has been 'sent'.
Arguments:
timeout -- Optional timeout for waiting for a new value.
"""
args = {'block': False}
if timeout is not None:
args = {'block': True, 'timeout': timeout}
try:
return self.send_queue.get(**args)
except:
return None
def recv_data(self, data):
"""
Add data to the receiving queue.
Arguments:
data -- String data to 'write' to the socket to be received
by the XMPP client.
"""
self.recv_queue.put(data)
# ------------------------------------------------------------------
# Socket Interface
def recv(self, *args, **kwargs):
"""
Read a value from the received queue.
Arguments:
Placeholders. Same as for socket.Socket.recv.
"""
return self.read(block=True)
def send(self, data):
"""
Send data by placing it in the send queue.
Arguments:
data -- String value to write.
"""
self.send_queue.put(data)
# ------------------------------------------------------------------
# File Socket
def makefile(self, *args, **kwargs):
"""
File socket version to use with ElementTree.
Arguments:
Placeholders, same as socket.Socket.makefile()
"""
return self
def read(self, block=True, timeout=None, **kwargs):
"""
Implement the file socket interface.
Arguments:
block -- Indicate if the read should block until a
value is ready.
timeout -- Time in seconds a block should last before
returning None.
"""
if timeout is not None:
block = True
try:
return self.recv_queue.get(block, timeout)
except:
return None

628
sleekxmpp/test/sleektest.py Normal file
View File

@@ -0,0 +1,628 @@
"""
SleekXMPP: The Sleek XMPP Library
Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout
This file is part of SleekXMPP.
See the file LICENSE for copying permission.
"""
import unittest
import sleekxmpp
from sleekxmpp import ClientXMPP, ComponentXMPP
from sleekxmpp.stanza import Message, Iq, Presence
from sleekxmpp.test import TestSocket, TestLiveSocket
from sleekxmpp.xmlstream import StanzaBase, ET, register_stanza_plugin
from sleekxmpp.xmlstream.tostring import tostring
class SleekTest(unittest.TestCase):
"""
A SleekXMPP specific TestCase class that provides
methods for comparing message, iq, and presence stanzas.
Methods:
Message -- Create a Message stanza object.
Iq -- Create an Iq stanza object.
Presence -- Create a Presence stanza object.
check_jid -- Check a JID and its component parts.
check -- Compare a stanza against an XML string.
stream_start -- Initialize a dummy XMPP client.
stream_close -- Disconnect the XMPP client.
make_header -- Create a stream header.
send_header -- Check that the given header has been sent.
send_feature -- Send a raw XML element.
send -- Check that the XMPP client sent the given
generic stanza.
recv -- Queue data for XMPP client to receive, or
verify the data that was received from a
live connection.
recv_header -- Check that a given stream header
was received.
recv_feature -- Check that a given, raw XML element
was recveived.
fix_namespaces -- Add top-level namespace to an XML object.
compare -- Compare XML objects against each other.
"""
def runTest(self):
pass
def parse_xml(self, xml_string):
try:
xml = ET.fromstring(xml_string)
return xml
except SyntaxError as e:
if 'unbound' in e.msg:
known_prefixes = {
'stream': 'http://etherx.jabber.org/streams'}
prefix = xml_string.split('<')[1].split(':')[0]
if prefix in known_prefixes:
xml_string = '<fixns xmlns:%s="%s">%s</fixns>' % (
prefix,
known_prefixes[prefix],
xml_string)
xml = self.parse_xml(xml_string)
xml = xml.getchildren()[0]
return xml
# ------------------------------------------------------------------
# Shortcut methods for creating stanza objects
def Message(self, *args, **kwargs):
"""
Create a Message stanza.
Uses same arguments as StanzaBase.__init__
Arguments:
xml -- An XML object to use for the Message's values.
"""
return Message(None, *args, **kwargs)
def Iq(self, *args, **kwargs):
"""
Create an Iq stanza.
Uses same arguments as StanzaBase.__init__
Arguments:
xml -- An XML object to use for the Iq's values.
"""
return Iq(None, *args, **kwargs)
def Presence(self, *args, **kwargs):
"""
Create a Presence stanza.
Uses same arguments as StanzaBase.__init__
Arguments:
xml -- An XML object to use for the Iq's values.
"""
return Presence(None, *args, **kwargs)
def check_jid(self, jid, user=None, domain=None, resource=None,
bare=None, full=None, string=None):
"""
Verify the components of a JID.
Arguments:
jid -- The JID object to test.
user -- Optional. The user name portion of the JID.
domain -- Optional. The domain name portion of the JID.
resource -- Optional. The resource portion of the JID.
bare -- Optional. The bare JID.
full -- Optional. The full JID.
string -- Optional. The string version of the JID.
"""
if user is not None:
self.assertEqual(jid.user, user,
"User does not match: %s" % jid.user)
if domain is not None:
self.assertEqual(jid.domain, domain,
"Domain does not match: %s" % jid.domain)
if resource is not None:
self.assertEqual(jid.resource, resource,
"Resource does not match: %s" % jid.resource)
if bare is not None:
self.assertEqual(jid.bare, bare,
"Bare JID does not match: %s" % jid.bare)
if full is not None:
self.assertEqual(jid.full, full,
"Full JID does not match: %s" % jid.full)
if string is not None:
self.assertEqual(str(jid), string,
"String does not match: %s" % str(jid))
# ------------------------------------------------------------------
# Methods for comparing stanza objects to XML strings
def check(self, stanza, xml_string,
defaults=None, use_values=True):
"""
Create and compare several stanza objects to a correct XML string.
If use_values is False, test using getStanzaValues() and
setStanzaValues() will not be used.
Some stanzas provide default values for some interfaces, but
these defaults can be problematic for testing since they can easily
be forgotten when supplying the XML string. A list of interfaces that
use defaults may be provided and the generated stanzas will use the
default values for those interfaces if needed.
However, correcting the supplied XML is not possible for interfaces
that add or remove XML elements. Only interfaces that map to XML
attributes may be set using the defaults parameter. The supplied XML
must take into account any extra elements that are included by default.
Arguments:
stanza -- The stanza object to test.
xml_string -- A string version of the correct XML expected.
defaults -- A list of stanza interfaces that have default
values. These interfaces will be set to their
defaults for the given and generated stanzas to
prevent unexpected test failures.
use_values -- Indicates if testing using getStanzaValues() and
setStanzaValues() should be used. Defaults to
True.
"""
stanza_class = stanza.__class__
xml = self.parse_xml(xml_string)
# Ensure that top level namespaces are used, even if they
# were not provided.
self.fix_namespaces(stanza.xml, 'jabber:client')
self.fix_namespaces(xml, 'jabber:client')
stanza2 = stanza_class(xml=xml)
if use_values:
# Using getStanzaValues() and setStanzaValues() will add
# XML for any interface that has a default value. We need
# to set those defaults on the existing stanzas and XML
# so that they will compare correctly.
default_stanza = stanza_class()
if defaults is None:
known_defaults = {
Message: ['type'],
Presence: ['priority']
}
defaults = known_defaults.get(stanza_class, [])
for interface in defaults:
stanza[interface] = stanza[interface]
stanza2[interface] = stanza2[interface]
# Can really only automatically add defaults for top
# level attribute values. Anything else must be accounted
# for in the provided XML string.
if interface not in xml.attrib:
if interface in default_stanza.xml.attrib:
value = default_stanza.xml.attrib[interface]
xml.attrib[interface] = value
values = stanza2.getStanzaValues()
stanza3 = stanza_class()
stanza3.setStanzaValues(values)
debug = "Three methods for creating stanzas do not match.\n"
debug += "Given XML:\n%s\n" % tostring(xml)
debug += "Given stanza:\n%s\n" % tostring(stanza.xml)
debug += "Generated stanza:\n%s\n" % tostring(stanza2.xml)
debug += "Second generated stanza:\n%s\n" % tostring(stanza3.xml)
result = self.compare(xml, stanza.xml, stanza2.xml, stanza3.xml)
else:
debug = "Two methods for creating stanzas do not match.\n"
debug += "Given XML:\n%s\n" % tostring(xml)
debug += "Given stanza:\n%s\n" % tostring(stanza.xml)
debug += "Generated stanza:\n%s\n" % tostring(stanza2.xml)
result = self.compare(xml, stanza.xml, stanza2.xml)
self.failUnless(result, debug)
# ------------------------------------------------------------------
# Methods for simulating stanza streams.
def stream_start(self, mode='client', skip=True, header=None,
socket='mock', jid='tester@localhost',
password='test', server='localhost',
port=5222):
"""
Initialize an XMPP client or component using a dummy XML stream.
Arguments:
mode -- Either 'client' or 'component'. Defaults to 'client'.
skip -- Indicates if the first item in the sent queue (the
stream header) should be removed. Tests that wish
to test initializing the stream should set this to
False. Otherwise, the default of True should be used.
socket -- Either 'mock' or 'live' to indicate if the socket
should be a dummy, mock socket or a live, functioning
socket. Defaults to 'mock'.
jid -- The JID to use for the connection.
Defaults to 'tester@localhost'.
password -- The password to use for the connection.
Defaults to 'test'.
server -- The name of the XMPP server. Defaults to 'localhost'.
port -- The port to use when connecting to the server.
Defaults to 5222.
"""
if mode == 'client':
self.xmpp = ClientXMPP(jid, password)
elif mode == 'component':
self.xmpp = ComponentXMPP(jid, password,
server, port)
else:
raise ValueError("Unknown XMPP connection mode.")
if socket == 'mock':
self.xmpp.set_socket(TestSocket())
# Simulate connecting for mock sockets.
self.xmpp.auto_reconnect = False
self.xmpp.is_client = True
self.xmpp.state._set_state('connected')
# Must have the stream header ready for xmpp.process() to work.
if not header:
header = self.xmpp.stream_header
self.xmpp.socket.recv_data(header)
elif socket == 'live':
self.xmpp.socket_class = TestLiveSocket
self.xmpp.connect()
else:
raise ValueError("Unknown socket type.")
self.xmpp.register_plugins()
self.xmpp.process(threaded=True)
if skip:
# Clear startup stanzas
self.xmpp.socket.next_sent(timeout=1)
if mode == 'component':
self.xmpp.socket.next_sent(timeout=1)
def make_header(self, sto='',
sfrom='',
sid='',
stream_ns="http://etherx.jabber.org/streams",
default_ns="jabber:client",
version="1.0",
xml_header=True):
"""
Create a stream header to be received by the test XMPP agent.
The header must be saved and passed to stream_start.
Arguments:
sto -- The recipient of the stream header.
sfrom -- The agent sending the stream header.
sid -- The stream's id.
stream_ns -- The namespace of the stream's root element.
default_ns -- The default stanza namespace.
version -- The stream version.
xml_header -- Indicates if the XML version header should be
appended before the stream header.
"""
header = '<stream:stream %s>'
parts = []
if xml_header:
header = '<?xml version="1.0"?>' + header
if sto:
parts.append('to="%s"' % sto)
if sfrom:
parts.append('from="%s"' % sfrom)
if sid:
parts.append('id="%s"' % sid)
parts.append('version="%s"' % version)
parts.append('xmlns:stream="%s"' % stream_ns)
parts.append('xmlns="%s"' % default_ns)
return header % ' '.join(parts)
def recv(self, data, stanza_class=StanzaBase, defaults=[],
use_values=True, timeout=1):
"""
Pass data to the dummy XMPP client as if it came from an XMPP server.
If using a live connection, verify what the server has sent.
Arguments:
data -- String stanza XML to be received and processed by
the XMPP client or component.
stanza_class -- The stanza object class for verifying data received
by a live connection. Defaults to StanzaBase.
defaults -- A list of stanza interfaces with default values that
may interfere with comparisons.
use_values -- Indicates if stanza comparisons should test using
getStanzaValues() and setStanzaValues().
Defaults to True.
timeout -- Time to wait in seconds for data to be received by
a live connection.
"""
if self.xmpp.socket.is_live:
# we are working with a live connection, so we should
# verify what has been received instead of simulating
# receiving data.
recv_data = self.xmpp.socket.next_recv(timeout)
if recv_data is None:
return False
stanza = stanza_class(xml=self.parse_xml(recv_data))
return self.check(stanza_class, stanza, data,
defaults=defaults,
use_values=use_values)
else:
# place the data in the dummy socket receiving queue.
data = str(data)
self.xmpp.socket.recv_data(data)
def recv_header(self, sto='',
sfrom='',
sid='',
stream_ns="http://etherx.jabber.org/streams",
default_ns="jabber:client",
version="1.0",
xml_header=False,
timeout=1):
"""
Check that a given stream header was received.
Arguments:
sto -- The recipient of the stream header.
sfrom -- The agent sending the stream header.
sid -- The stream's id. Set to None to ignore.
stream_ns -- The namespace of the stream's root element.
default_ns -- The default stanza namespace.
version -- The stream version.
xml_header -- Indicates if the XML version header should be
appended before the stream header.
timeout -- Length of time to wait in seconds for a
response.
"""
header = self.make_header(sto, sfrom, sid,
stream_ns=stream_ns,
default_ns=default_ns,
version=version,
xml_header=xml_header)
recv_header = self.xmpp.socket.next_recv(timeout)
if recv_header is None:
raise ValueError("Socket did not return data.")
# Apply closing elements so that we can construct
# XML objects for comparison.
header2 = header + '</stream:stream>'
recv_header2 = recv_header + '</stream:stream>'
xml = self.parse_xml(header2)
recv_xml = self.parse_xml(recv_header2)
if sid is None:
# Ignore the id sent by the server since
# we can't know in advance what it will be.
if 'id' in recv_xml.attrib:
del recv_xml.attrib['id']
# Ignore the xml:lang attribute for now.
if 'xml:lang' in recv_xml.attrib:
del recv_xml.attrib['xml:lang']
xml_ns = 'http://www.w3.org/XML/1998/namespace'
if '{%s}lang' % xml_ns in recv_xml.attrib:
del recv_xml.attrib['{%s}lang' % xml_ns]
if recv_xml.getchildren:
# We received more than just the header
for xml in recv_xml.getchildren():
self.xmpp.socket.recv_data(tostring(xml))
attrib = recv_xml.attrib
recv_xml.clear()
recv_xml.attrib = attrib
self.failUnless(
self.compare(xml, recv_xml),
"Stream headers do not match:\nDesired:\n%s\nReceived:\n%s" % (
'%s %s' % (xml.tag, xml.attrib),
'%s %s' % (recv_xml.tag, recv_xml.attrib)))
def recv_feature(self, data, use_values=True, timeout=1):
"""
"""
if self.xmpp.socket.is_live:
# we are working with a live connection, so we should
# verify what has been received instead of simulating
# receiving data.
recv_data = self.xmpp.socket.next_recv(timeout)
if recv_data is None:
return False
xml = self.parse_xml(data)
recv_xml = self.parse_xml(recv_data)
self.failUnless(self.compare(xml, recv_xml),
"Features do not match.\nDesired:\n%s\nReceived:\n%s" % (
tostring(xml), tostring(recv_xml)))
else:
# place the data in the dummy socket receiving queue.
data = str(data)
self.xmpp.socket.recv_data(data)
def send_header(self, sto='',
sfrom='',
sid='',
stream_ns="http://etherx.jabber.org/streams",
default_ns="jabber:client",
version="1.0",
xml_header=False,
timeout=1):
"""
Check that a given stream header was sent.
Arguments:
sto -- The recipient of the stream header.
sfrom -- The agent sending the stream header.
sid -- The stream's id.
stream_ns -- The namespace of the stream's root element.
default_ns -- The default stanza namespace.
version -- The stream version.
xml_header -- Indicates if the XML version header should be
appended before the stream header.
timeout -- Length of time to wait in seconds for a
response.
"""
header = self.make_header(sto, sfrom, sid,
stream_ns=stream_ns,
default_ns=default_ns,
version=version,
xml_header=xml_header)
sent_header = self.xmpp.socket.next_sent(timeout)
if sent_header is None:
raise ValueError("Socket did not return data.")
# Apply closing elements so that we can construct
# XML objects for comparison.
header2 = header + '</stream:stream>'
sent_header2 = sent_header + b'</stream:stream>'
xml = self.parse_xml(header2)
sent_xml = self.parse_xml(sent_header2)
self.failUnless(
self.compare(xml, sent_xml),
"Stream headers do not match:\nDesired:\n%s\nSent:\n%s" % (
header, sent_header))
def send_feature(self, data, use_values=True, timeout=1):
"""
"""
sent_data = self.xmpp.socket.next_sent(timeout)
if sent_data is None:
return False
xml = self.parse_xml(data)
sent_xml = self.parse_xml(sent_data)
self.failUnless(self.compare(xml, sent_xml),
"Features do not match.\nDesired:\n%s\nSent:\n%s" % (
tostring(xml), tostring(sent_xml)))
def send(self, data, defaults=None,
use_values=True, timeout=.1):
"""
Check that the XMPP client sent the given stanza XML.
Extracts the next sent stanza and compares it with the given
XML using check.
Arguments:
stanza_class -- The class of the sent stanza object.
data -- The XML string of the expected Message stanza,
or an equivalent stanza object.
use_values -- Modifies the type of tests used by check_message.
defaults -- A list of stanza interfaces that have defaults
values which may interfere with comparisons.
timeout -- Time in seconds to wait for a stanza before
failing the check.
"""
if isinstance(data, str):
xml = self.parse_xml(data)
self.fix_namespaces(xml, 'jabber:client')
data = self.xmpp._build_stanza(xml, 'jabber:client')
sent = self.xmpp.socket.next_sent(timeout)
self.check(data, sent,
defaults=defaults,
use_values=use_values)
def stream_close(self):
"""
Disconnect the dummy XMPP client.
Can be safely called even if stream_start has not been called.
Must be placed in the tearDown method of a test class to ensure
that the XMPP client is disconnected after an error.
"""
if hasattr(self, 'xmpp') and self.xmpp is not None:
self.xmpp.socket.recv_data(self.xmpp.stream_footer)
self.xmpp.disconnect()
# ------------------------------------------------------------------
# XML Comparison and Cleanup
def fix_namespaces(self, xml, ns):
"""
Assign a namespace to an element and any children that
don't have a namespace.
Arguments:
xml -- The XML object to fix.
ns -- The namespace to add to the XML object.
"""
if xml.tag.startswith('{'):
return
xml.tag = '{%s}%s' % (ns, xml.tag)
for child in xml.getchildren():
self.fix_namespaces(child, ns)
def compare(self, xml, *other):
"""
Compare XML objects.
Arguments:
xml -- The XML object to compare against.
*other -- The list of XML objects to compare.
"""
if not other:
return False
# Compare multiple objects
if len(other) > 1:
for xml2 in other:
if not self.compare(xml, xml2):
return False
return True
other = other[0]
# Step 1: Check tags
if xml.tag != other.tag:
return False
# Step 2: Check attributes
if xml.attrib != other.attrib:
return False
# Step 3: Check text
if xml.text is None:
xml.text = ""
if other.text is None:
other.text = ""
xml.text = xml.text.strip()
other.text = other.text.strip()
if xml.text != other.text:
return False
# Step 4: Check children count
if len(xml.getchildren()) != len(other.getchildren()):
return False
# Step 5: Recursively check children
for child in xml:
child2s = other.findall("%s" % child.tag)
if child2s is None:
return False
for child2 in child2s:
if self.compare(child, child2):
break
else:
return False
# Step 6: Recursively check children the other way.
for child in other:
child2s = xml.findall("%s" % child.tag)
if child2s is None:
return False
for child2 in child2s:
if self.compare(child, child2):
break
else:
return False
# Everything matches
return True

0
sleekxmpp/thirdparty/__init__.py vendored Normal file
View File

287
sleekxmpp/thirdparty/statemachine.py vendored Normal file
View File

@@ -0,0 +1,287 @@
"""
SleekXMPP: The Sleek XMPP Library
Copyright (C) 2010 Nathanael C. Fritz
This file is part of SleekXMPP.
See the file LICENSE for copying permission.
"""
import threading
import time
import logging
log = logging.getLogger(__name__)
class StateMachine(object):
def __init__(self, states=[]):
self.lock = threading.Lock()
self.notifier = threading.Event()
self.__states = []
self.addStates(states)
self.__default_state = self.__states[0]
self.__current_state = self.__default_state
def addStates(self, states):
self.lock.acquire()
try:
for state in states:
if state in self.__states:
raise IndexError("The state '%s' is already in the StateMachine." % state)
self.__states.append(state)
finally: self.lock.release()
def transition(self, from_state, to_state, wait=0.0, func=None, args=[], kwargs={}):
'''
Transition from the given `from_state` to the given `to_state`.
This method will return `True` if the state machine is now in `to_state`. It
will return `False` if a timeout occurred the transition did not occur.
If `wait` is 0 (the default,) this method returns immediately if the state machine
is not in `from_state`.
If you want the thread to block and transition once the state machine to enters
`from_state`, set `wait` to a non-negative value. Note there is no 'block
indefinitely' flag since this leads to deadlock. If you want to wait indefinitely,
choose a reasonable value for `wait` (e.g. 20 seconds) and do so in a while loop like so:
::
while not thread_should_exit and not state_machine.transition('disconnected', 'connecting', wait=20 ):
pass # timeout will occur every 20s unless transition occurs
if thread_should_exit: return
# perform actions here after successful transition
This allows the thread to be responsive by setting `thread_should_exit=True`.
The optional `func` argument allows the user to pass a callable operation which occurs
within the context of the state transition (e.g. while the state machine is locked.)
If `func` returns a True value, the transition will occur. If `func` returns a non-
True value or if an exception is thrown, the transition will not occur. Any thrown
exception is not caught by the state machine and is the caller's responsibility to handle.
If `func` completes normally, this method will return the value returned by `func.` If
values for `args` and `kwargs` are provided, they are expanded and passed like so:
`func( *args, **kwargs )`.
'''
return self.transition_any((from_state,), to_state, wait=wait,
func=func, args=args, kwargs=kwargs)
def transition_any(self, from_states, to_state, wait=0.0, func=None, args=[], kwargs={}):
'''
Transition from any of the given `from_states` to the given `to_state`.
'''
if not (isinstance(from_states,tuple) or isinstance(from_states,list)):
raise ValueError("from_states should be a list or tuple")
for state in from_states:
if not state in self.__states:
raise ValueError("StateMachine does not contain from_state %s." % state)
if not to_state in self.__states:
raise ValueError("StateMachine does not contain to_state %s." % to_state)
start = time.time()
while not self.lock.acquire(False):
time.sleep(.001)
if (start + wait - time.time()) <= 0.0:
log.debug("Could not acquire lock")
return False
while not self.__current_state in from_states:
# detect timeout:
remainder = start + wait - time.time()
if remainder > 0:
self.notifier.wait(remainder)
else:
log.debug("State was not ready")
self.lock.release()
return False
try: # lock is acquired; all other threads will return false or wait until notify/timeout
if self.__current_state in from_states: # should always be True due to lock
# Note that func might throw an exception, but that's OK, it aborts the transition
return_val = func(*args,**kwargs) if func is not None else True
# some 'false' value returned from func,
# indicating that transition should not occur:
if not return_val: return return_val
log.debug(' ==== TRANSITION %s -> %s', self.__current_state, to_state)
self._set_state(to_state)
return return_val # some 'true' value returned by func or True if func was None
else:
log.error("StateMachine bug!! The lock should ensure this doesn't happen!")
return False
finally:
self.notifier.set() # notify any waiting threads that the state has changed.
self.notifier.clear()
self.lock.release()
def transition_ctx(self, from_state, to_state, wait=0.0):
'''
Use the state machine as a context manager. The transition occurs on /exit/ from
the `with` context, so long as no exception is thrown. For example:
::
with state_machine.transition_ctx('one','two', wait=5) as locked:
if locked:
# the state machine is currently locked in state 'one', and will
# transition to 'two' when the 'with' statement ends, so long as
# no exception is thrown.
print 'Currently locked in state one: %s' % state_machine['one']
else:
# The 'wait' timed out, and no lock has been acquired
print 'Timed out before entering state "one"'
print 'Since no exception was thrown, we are now in state "two": %s' % state_machine['two']
The other main difference between this method and `transition()` is that the
state machine is locked for the duration of the `with` statement. Normally,
after a `transition()` occurs, the state machine is immediately unlocked and
available to another thread to call `transition()` again.
'''
if not from_state in self.__states:
raise ValueError("StateMachine does not contain from_state %s." % from_state)
if not to_state in self.__states:
raise ValueError("StateMachine does not contain to_state %s." % to_state)
return _StateCtx(self, from_state, to_state, wait)
def ensure(self, state, wait=0.0, block_on_transition=False):
'''
Ensure the state machine is currently in `state`, or wait until it enters `state`.
'''
return self.ensure_any((state,), wait=wait, block_on_transition=block_on_transition)
def ensure_any(self, states, wait=0.0, block_on_transition=False):
'''
Ensure we are currently in one of the given `states` or wait until
we enter one of those states.
Note that due to the nature of the function, you cannot guarantee that
the entirety of some operation completes while you remain in a given
state. That would require acquiring and holding a lock, which
would mean no other threads could do the same. (You'd essentially
be serializing all of the threads that are 'ensuring' their tasks
occurred in some state.
'''
if not (isinstance(states,tuple) or isinstance(states,list)):
raise ValueError('states arg should be a tuple or list')
for state in states:
if not state in self.__states:
raise ValueError("StateMachine does not contain state '%s'" % state)
# if we're in the middle of a transition, determine whether we should
# 'fall back' to the 'current' state, or wait for the new state, in order to
# avoid an operation occurring in the wrong state.
# TODO another option would be an ensure_ctx that uses a semaphore to allow
# threads to indicate they want to remain in a particular state.
# will return immediately if no transition is in process.
if block_on_transition:
# we're not in the middle of a transition; don't hold the lock
if self.lock.acquire(False): self.lock.release()
# wait for the transition to complete
else: self.notifier.wait()
start = time.time()
while not self.__current_state in states:
# detect timeout:
remainder = start + wait - time.time()
if remainder > 0: self.notifier.wait(remainder)
else: return False
return True
def reset(self):
# TODO need to lock before calling this?
self.transition(self.__current_state, self.__default_state)
def _set_state(self, state): #unsynchronized, only call internally after lock is acquired
self.__current_state = state
return state
def current_state(self):
'''
Return the current state name.
'''
return self.__current_state
def __getitem__(self, state):
'''
Non-blocking, non-synchronized test to determine if we are in the given state.
Use `StateMachine.ensure(state)` to wait until the machine enters a certain state.
'''
return self.__current_state == state
def __str__(self):
return "".join(("StateMachine(", ','.join(self.__states), "): ", self.__current_state))
class _StateCtx:
def __init__(self, state_machine, from_state, to_state, wait):
self.state_machine = state_machine
self.from_state = from_state
self.to_state = to_state
self.wait = wait
self._locked = False
def __enter__(self):
start = time.time()
while not self.state_machine[self.from_state] or not self.state_machine.lock.acquire(False):
# detect timeout:
remainder = start + self.wait - time.time()
if remainder > 0: self.state_machine.notifier.wait(remainder)
else:
log.debug('StateMachine timeout while waiting for state: %s', self.from_state)
return False
self._locked = True # lock has been acquired at this point
self.state_machine.notifier.clear()
log.debug('StateMachine entered context in state: %s',
self.state_machine.current_state())
return True
def __exit__(self, exc_type, exc_val, exc_tb):
if exc_val is not None:
log.exception("StateMachine exception in context, remaining in state: %s\n%s:%s",
self.state_machine.current_state(), exc_type.__name__, exc_val)
if self._locked:
if exc_val is None:
log.debug(' ==== TRANSITION %s -> %s',
self.state_machine.current_state(), self.to_state)
self.state_machine._set_state(self.to_state)
self.state_machine.notifier.set()
self.state_machine.lock.release()
return False # re-raise any exception
if __name__ == '__main__':
def callback(s, s2):
print((1, s.transition('on', 'off', wait=0.0, func=callback, args=[s,s2])))
print((2, s2.transition('off', 'on', func=callback, args=[s,s2])))
return True
s = StateMachine(('off', 'on'))
s2 = StateMachine(('off', 'on'))
print((3, s.transition('off', 'on', wait=0.0, func=callback, args=[s,s2]),))
print((s.current_state(), s2.current_state()))

View File

@@ -0,0 +1,19 @@
"""
SleekXMPP: The Sleek XMPP Library
Copyright (C) 2010 Nathanael C. Fritz
This file is part of SleekXMPP.
See the file LICENSE for copying permission.
"""
from sleekxmpp.xmlstream.jid import JID
from sleekxmpp.xmlstream.scheduler import Scheduler
from sleekxmpp.xmlstream.stanzabase import StanzaBase, ElementBase, ET
from sleekxmpp.xmlstream.stanzabase import register_stanza_plugin
from sleekxmpp.xmlstream.tostring import tostring
from sleekxmpp.xmlstream.xmlstream import XMLStream, RESPONSE_TIMEOUT
from sleekxmpp.xmlstream.xmlstream import RestartStream
__all__ = ['JID', 'Scheduler', 'StanzaBase', 'ElementBase',
'ET', 'StateMachine', 'tostring', 'XMLStream',
'RESPONSE_TIMEOUT', 'RestartStream']

View File

@@ -3,23 +3,39 @@
Copyright (C) 2010 Nathanael C. Fritz
This file is part of SleekXMPP.
See the file license.txt for copying permission.
See the file LICENSE for copying permission.
"""
from socket import _fileobject
import socket
class filesocket(_fileobject):
def read(self, size=4096):
data = self._sock.recv(size)
if data is not None:
return data
class FileSocket(_fileobject):
"""
Create a file object wrapper for a socket to work around
issues present in Python 2.6 when using sockets as file objects.
The parser for xml.etree.cElementTree requires a file, but we will
be reading from the XMPP connection socket instead.
"""
def read(self, size=4096):
"""Read data from the socket as if it were a file."""
data = self._sock.recv(size)
if data is not None:
return data
class Socket26(socket._socketobject):
def makefile(self, mode='r', bufsize=-1):
"""makefile([mode[, bufsize]]) -> file object
Return a regular file object corresponding to the socket. The mode
and bufsize arguments are as for the built-in open() function."""
return filesocket(self._sock, mode, bufsize)
"""
A custom socket implementation that uses our own FileSocket class
to work around issues in Python 2.6 when using sockets as files.
"""
def makefile(self, mode='r', bufsize=-1):
"""makefile([mode[, bufsize]]) -> file object
Return a regular file object corresponding to the socket. The mode
and bufsize arguments are as for the built-in open() function."""
return FileSocket(self._sock, mode, bufsize)

View File

@@ -0,0 +1,14 @@
"""
SleekXMPP: The Sleek XMPP Library
Copyright (C) 2010 Nathanael C. Fritz
This file is part of SleekXMPP.
See the file LICENSE for copying permission.
"""
from sleekxmpp.xmlstream.handler.callback import Callback
from sleekxmpp.xmlstream.handler.waiter import Waiter
from sleekxmpp.xmlstream.handler.xmlcallback import XMLCallback
from sleekxmpp.xmlstream.handler.xmlwaiter import XMLWaiter
__all__ = ['Callback', 'Waiter', 'XMLCallback', 'XMLWaiter']

View File

@@ -3,26 +3,87 @@
Copyright (C) 2010 Nathanael C. Fritz
This file is part of SleekXMPP.
See the file license.txt for copying permission.
See the file LICENSE for copying permission.
"""
class BaseHandler(object):
"""
Base class for stream handlers. Stream handlers are matched with
incoming stanzas so that the stanza may be processed in some way.
Stanzas may be matched with multiple handlers.
def __init__(self, name, matcher):
self.name = name
self._destroy = False
self._payload = None
self._matcher = matcher
def match(self, xml):
return self._matcher.match(xml)
def prerun(self, payload): # what's the point of this if the payload is called again in run??
self._payload = payload
Handler execution may take place in two phases. The first is during
the stream processing itself. The second is after stream processing
and during SleekXMPP's main event loop. The prerun method is used
for execution during stream processing, and the run method is used
during the main event loop.
def run(self, payload):
self._payload = payload
def checkDelete(self):
return self._destroy
Attributes:
name -- The name of the handler.
stream -- The stream this handler is assigned to.
Methods:
match -- Compare a stanza with the handler's matcher.
prerun -- Handler execution during stream processing.
run -- Handler execution during the main event loop.
check_delete -- Indicate if the handler may be removed from use.
"""
def __init__(self, name, matcher, stream=None):
"""
Create a new stream handler.
Arguments:
name -- The name of the handler.
matcher -- A matcher object from xmlstream.matcher that will be
used to determine if a stanza should be accepted by
this handler.
stream -- The XMLStream instance the handler should monitor.
"""
self.checkDelete = self.check_delete
self.name = name
self.stream = stream
self._destroy = False
self._payload = None
self._matcher = matcher
if stream is not None:
stream.registerHandler(self)
def match(self, xml):
"""
Compare a stanza or XML object with the handler's matcher.
Arguments
xml -- An XML or stanza object.
"""
return self._matcher.match(xml)
def prerun(self, payload):
"""
Prepare the handler for execution while the XML stream is being
processed.
Arguments:
payload -- A stanza object.
"""
self._payload = payload
def run(self, payload):
"""
Execute the handler after XML stream processing and during the
main event loop.
Arguments:
payload -- A stanza object.
"""
self._payload = payload
def check_delete(self):
"""
Check if the handler should be removed from the list of stream
handlers.
"""
return self._destroy

View File

@@ -3,34 +3,82 @@
Copyright (C) 2010 Nathanael C. Fritz
This file is part of SleekXMPP.
See the file license.txt for copying permission.
See the file LICENSE for copying permission.
"""
from . import base
import logging
class Callback(base.BaseHandler):
def __init__(self, name, matcher, pointer, thread=False, once=False, instream=False):
base.BaseHandler.__init__(self, name, matcher)
self._pointer = pointer
self._thread = thread
self._once = once
self._instream = instream
from sleekxmpp.xmlstream.handler.base import BaseHandler
def prerun(self, payload): # prerun actually calls run?!? WTF! Then it gets run AGAIN!
base.BaseHandler.prerun(self, payload)
if self._instream:
logging.debug('callback "%s" prerun', self.name)
self.run(payload, True)
def run(self, payload, instream=False):
if not self._instream or instream:
logging.debug('callback "%s" run', self.name)
base.BaseHandler.run(self, payload)
#if self._thread:
# x = threading.Thread(name="Callback_%s" % self.name, target=self._pointer, args=(payload,))
# x.start()
#else:
self._pointer(payload)
if self._once:
self._destroy = True
class Callback(BaseHandler):
"""
The Callback handler will execute a callback function with
matched stanzas.
The handler may execute the callback either during stream
processing or during the main event loop.
Callback functions are all executed in the same thread, so be
aware if you are executing functions that will block for extended
periods of time. Typically, you should signal your own events using the
SleekXMPP object's event() method to pass the stanza off to a threaded
event handler for further processing.
Methods:
prerun -- Overrides BaseHandler.prerun
run -- Overrides BaseHandler.run
"""
def __init__(self, name, matcher, pointer, thread=False,
once=False, instream=False, stream=None):
"""
Create a new callback handler.
Arguments:
name -- The name of the handler.
matcher -- A matcher object for matching stanza objects.
pointer -- The function to execute during callback.
thread -- DEPRECATED. Remains only for backwards compatibility.
once -- Indicates if the handler should be used only
once. Defaults to False.
instream -- Indicates if the callback should be executed
during stream processing instead of in the
main event loop.
stream -- The XMLStream instance this handler should monitor.
"""
BaseHandler.__init__(self, name, matcher, stream)
self._pointer = pointer
self._once = once
self._instream = instream
def prerun(self, payload):
"""
Execute the callback during stream processing, if
the callback was created with instream=True.
Overrides BaseHandler.prerun
Arguments:
payload -- The matched stanza object.
"""
BaseHandler.prerun(self, payload)
if self._instream:
self.run(payload, True)
def run(self, payload, instream=False):
"""
Execute the callback function with the matched stanza payload.
Overrides BaseHandler.run
Arguments:
payload -- The matched stanza object.
instream -- Force the handler to execute during
stream processing. Used only by prerun.
Defaults to False.
"""
if not self._instream or instream:
BaseHandler.run(self, payload)
self._pointer(payload)
if self._once:
self._destroy = True

View File

@@ -3,34 +3,99 @@
Copyright (C) 2010 Nathanael C. Fritz
This file is part of SleekXMPP.
See the file license.txt for copying permission.
See the file LICENSE for copying permission.
"""
from . import base
try:
import queue
except ImportError:
import Queue as queue
import logging
from .. stanzabase import StanzaBase
try:
import queue
except ImportError:
import Queue as queue
class Waiter(base.BaseHandler):
def __init__(self, name, matcher):
base.BaseHandler.__init__(self, name, matcher)
self._payload = queue.Queue()
def prerun(self, payload):
self._payload.put(payload)
def run(self, payload):
pass
from sleekxmpp.xmlstream import StanzaBase, RESPONSE_TIMEOUT
from sleekxmpp.xmlstream.handler.base import BaseHandler
def wait(self, timeout=60):
try:
return self._payload.get(True, timeout)
except queue.Empty:
logging.warning("Timed out waiting for %s" % self.name)
return False
def checkDelete(self):
return True
log = logging.getLogger(__name__)
class Waiter(BaseHandler):
"""
The Waiter handler allows an event handler to block
until a particular stanza has been received. The handler
will either be given the matched stanza, or False if the
waiter has timed out.
Methods:
check_delete -- Overrides BaseHandler.check_delete
prerun -- Overrides BaseHandler.prerun
run -- Overrides BaseHandler.run
wait -- Wait for a stanza to arrive and return it to
an event handler.
"""
def __init__(self, name, matcher, stream=None):
"""
Create a new Waiter.
Arguments:
name -- The name of the waiter.
matcher -- A matcher object to detect the desired stanza.
stream -- Optional XMLStream instance to monitor.
"""
BaseHandler.__init__(self, name, matcher, stream=stream)
self._payload = queue.Queue()
def prerun(self, payload):
"""
Store the matched stanza.
Overrides BaseHandler.prerun
Arguments:
payload -- The matched stanza object.
"""
self._payload.put(payload)
def run(self, payload):
"""
Do not process this handler during the main event loop.
Overrides BaseHandler.run
Arguments:
payload -- The matched stanza object.
"""
pass
def wait(self, timeout=RESPONSE_TIMEOUT):
"""
Block an event handler while waiting for a stanza to arrive.
Be aware that this will impact performance if called from a
non-threaded event handler.
Will return either the received stanza, or False if the waiter
timed out.
Arguments:
timeout -- The number of seconds to wait for the stanza to
arrive. Defaults to the global default timeout
value sleekxmpp.xmlstream.RESPONSE_TIMEOUT.
"""
try:
stanza = self._payload.get(True, timeout)
except queue.Empty:
stanza = False
log.warning("Timed out waiting for %s" % self.name)
self.stream.removeHandler(self.name)
return stanza
def check_delete(self):
"""
Always remove waiters after use.
Overrides BaseHandler.check_delete
"""
return True

View File

@@ -3,12 +3,34 @@
Copyright (C) 2010 Nathanael C. Fritz
This file is part of SleekXMPP.
See the file license.txt for copying permission.
See the file LICENSE for copying permission.
"""
import threading
from . callback import Callback
from sleekxmpp.xmlstream.handler import Callback
class XMLCallback(Callback):
def run(self, payload, instream=False):
Callback.run(self, payload.xml, instream)
"""
The XMLCallback class is identical to the normal Callback class,
except that XML contents of matched stanzas will be processed instead
of the stanza objects themselves.
Methods:
run -- Overrides Callback.run
"""
def run(self, payload, instream=False):
"""
Execute the callback function with the matched stanza's
XML contents, instead of the stanza itself.
Overrides BaseHandler.run
Arguments:
payload -- The matched stanza object.
instream -- Force the handler to execute during
stream processing. Used only by prerun.
Defaults to False.
"""
Callback.run(self, payload.xml, instream)

View File

@@ -3,11 +3,31 @@
Copyright (C) 2010 Nathanael C. Fritz
This file is part of SleekXMPP.
See the file license.txt for copying permission.
See the file LICENSE for copying permission.
"""
from . waiter import Waiter
from sleekxmpp.xmlstream.handler import Waiter
class XMLWaiter(Waiter):
def prerun(self, payload):
Waiter.prerun(self, payload.xml)
"""
The XMLWaiter class is identical to the normal Waiter class
except that it returns the XML contents of the stanza instead
of the full stanza object itself.
Methods:
prerun -- Overrides Waiter.prerun
"""
def prerun(self, payload):
"""
Store the XML contents of the stanza to return to the
waiting event handler.
Overrides Waiter.prerun
Arguments:
payload -- The matched stanza object.
"""
Waiter.prerun(self, payload.xml)

123
sleekxmpp/xmlstream/jid.py Normal file
View File

@@ -0,0 +1,123 @@
"""
SleekXMPP: The Sleek XMPP Library
Copyright (C) 2010 Nathanael C. Fritz
This file is part of SleekXMPP.
See the file LICENSE for copying permission.
"""
class JID(object):
"""
A representation of a Jabber ID, or JID.
Each JID may have three components: a user, a domain, and an optional
resource. For example: user@domain/resource
When a resource is not used, the JID is called a bare JID.
The JID is a full JID otherwise.
Attributes:
jid -- Alias for 'full'.
full -- The value of the full JID.
bare -- The value of the bare JID.
user -- The username portion of the JID.
domain -- The domain name portion of the JID.
server -- Alias for 'domain'.
resource -- The resource portion of the JID.
Methods:
reset -- Use a new JID value.
regenerate -- Recreate the JID from its components.
"""
def __init__(self, jid):
"""Initialize a new JID"""
self.reset(jid)
def reset(self, jid):
"""
Start fresh from a new JID string.
Arguments:
jid - The new JID value.
"""
self._full = self._jid = str(jid)
self._domain = None
self._resource = None
self._user = None
self._bare = None
def __getattr__(self, name):
"""
Handle getting the JID values, using cache if available.
Arguments:
name -- One of: user, server, domain, resource,
full, or bare.
"""
if name == 'resource':
if self._resource is None and '/' in self._jid:
self._resource = self._jid.split('/', 1)[-1]
return self._resource or ""
elif name == 'user':
if self._user is None:
if '@' in self._jid:
self._user = self._jid.split('@', 1)[0]
else:
self._user = self._user
return self._user or ""
elif name in ('server', 'domain', 'host'):
if self._domain is None:
self._domain = self._jid.split('@', 1)[-1].split('/', 1)[0]
return self._domain or ""
elif name == 'full':
return self._jid or ""
elif name == 'bare':
if self._bare is None:
self._bare = self._jid.split('/', 1)[0]
return self._bare or ""
def __setattr__(self, name, value):
"""
Edit a JID by updating it's individual values, resetting the
generated JID in the end.
Arguments:
name -- The name of the JID part. One of: user, domain,
server, resource, full, jid, or bare.
value -- The new value for the JID part.
"""
if name in ('resource', 'user', 'domain'):
object.__setattr__(self, "_%s" % name, value)
self.regenerate()
elif name in ('server', 'domain', 'host'):
self.domain = value
elif name in ('full', 'jid'):
self.reset(value)
self.regenerate()
elif name == 'bare':
if '@' in value:
u, d = value.split('@', 1)
object.__setattr__(self, "_user", u)
object.__setattr__(self, "_domain", d)
else:
object.__setattr__(self, "_user", '')
object.__setattr__(self, "_domain", value)
self.regenerate()
else:
object.__setattr__(self, name, value)
def regenerate(self):
"""Generate a new JID based on current values, useful after editing."""
jid = ""
if self.user:
jid = "%s@" % self.user
jid += self.domain
if self.resource:
jid += "/%s" % self.resource
self.reset(jid)
def __str__(self):
"""Use the full JID as the string value."""
return self.full

View File

@@ -0,0 +1,16 @@
"""
SleekXMPP: The Sleek XMPP Library
Copyright (C) 2010 Nathanael C. Fritz
This file is part of SleekXMPP.
See the file LICENSE for copying permission.
"""
from sleekxmpp.xmlstream.matcher.id import MatcherId
from sleekxmpp.xmlstream.matcher.many import MatchMany
from sleekxmpp.xmlstream.matcher.stanzapath import StanzaPath
from sleekxmpp.xmlstream.matcher.xmlmask import MatchXMLMask
from sleekxmpp.xmlstream.matcher.xpath import MatchXPath
__all__ = ['MatcherId', 'MatchMany', 'StanzaPath',
'MatchXMLMask', 'MatchXPath']

View File

@@ -3,12 +3,32 @@
Copyright (C) 2010 Nathanael C. Fritz
This file is part of SleekXMPP.
See the file license.txt for copying permission.
See the file LICENSE for copying permission.
"""
class MatcherBase(object):
def __init__(self, criteria):
self._criteria = criteria
def match(self, xml):
return False
"""
Base class for stanza matchers. Stanza matchers are used to pick
stanzas out of the XML stream and pass them to the appropriate
stream handlers.
"""
def __init__(self, criteria):
"""
Create a new stanza matcher.
Arguments:
criteria -- Object to compare some aspect of a stanza
against.
"""
self._criteria = criteria
def match(self, xml):
"""
Check if a stanza matches the stored criteria.
Meant to be overridden.
"""
return False

View File

@@ -3,11 +3,30 @@
Copyright (C) 2010 Nathanael C. Fritz
This file is part of SleekXMPP.
See the file license.txt for copying permission.
See the file LICENSE for copying permission.
"""
from . import base
class MatcherId(base.MatcherBase):
def match(self, xml):
return xml['id'] == self._criteria
from sleekxmpp.xmlstream.matcher.base import MatcherBase
class MatcherId(MatcherBase):
"""
The ID matcher selects stanzas that have the same stanza 'id'
interface value as the desired ID.
Methods:
match -- Overrides MatcherBase.match.
"""
def match(self, xml):
"""
Compare the given stanza's 'id' attribute to the stored
id value.
Overrides MatcherBase.match.
Arguments:
xml -- The stanza to compare against.
"""
return xml['id'] == self._criteria

View File

@@ -3,15 +3,38 @@
Copyright (C) 2010 Nathanael C. Fritz
This file is part of SleekXMPP.
See the file license.txt for copying permission.
See the file LICENSE for copying permission.
"""
from . import base
from xml.etree import cElementTree
class MatchMany(base.MatcherBase):
from sleekxmpp.xmlstream.matcher.base import MatcherBase
def match(self, xml):
for m in self._criteria:
if m.match(xml):
return True
return False
class MatchMany(MatcherBase):
"""
The MatchMany matcher may compare a stanza against multiple
criteria. It is essentially an OR relation combining multiple
matchers.
Each of the criteria must implement a match() method.
Methods:
match -- Overrides MatcherBase.match.
"""
def match(self, xml):
"""
Match a stanza against multiple criteria. The match is successful
if one of the criteria matches.
Each of the criteria must implement a match() method.
Overrides MatcherBase.match.
Arguments:
xml -- The stanza object to compare against.
"""
for m in self._criteria:
if m.match(xml):
return True
return False

View File

@@ -3,12 +3,36 @@
Copyright (C) 2010 Nathanael C. Fritz
This file is part of SleekXMPP.
See the file license.txt for copying permission.
See the file LICENSE for copying permission.
"""
from . import base
from xml.etree import cElementTree
class StanzaPath(base.MatcherBase):
from sleekxmpp.xmlstream.matcher.base import MatcherBase
def match(self, stanza):
return stanza.match(self._criteria)
class StanzaPath(MatcherBase):
"""
The StanzaPath matcher selects stanzas that match a given "stanza path",
which is similar to a normal XPath except that it uses the interfaces and
plugins of the stanza instead of the actual, underlying XML.
In most cases, the stanza path and XPath should be identical, but be
aware that differences may occur.
Methods:
match -- Overrides MatcherBase.match.
"""
def match(self, stanza):
"""
Compare a stanza against a "stanza path". A stanza path is similar to
an XPath expression, but uses the stanza's interfaces and plugins
instead of the underlying XML. For most cases, the stanza path and
XPath should be identical, but be aware that differences may occur.
Overrides MatcherBase.match.
Arguments:
stanza -- The stanza object to compare against.
"""
return stanza.match(self._criteria)

View File

@@ -3,65 +3,157 @@
Copyright (C) 2010 Nathanael C. Fritz
This file is part of SleekXMPP.
See the file license.txt for copying permission.
See the file LICENSE for copying permission.
"""
from . import base
from xml.etree import cElementTree
import logging
from xml.parsers.expat import ExpatError
ignore_ns = False
from sleekxmpp.xmlstream.stanzabase import ET
from sleekxmpp.xmlstream.matcher.base import MatcherBase
class MatchXMLMask(base.MatcherBase):
def __init__(self, criteria):
base.MatcherBase.__init__(self, criteria)
if type(criteria) == type(''):
self._criteria = cElementTree.fromstring(self._criteria)
self.default_ns = 'jabber:client'
def setDefaultNS(self, ns):
self.default_ns = ns
# Flag indicating if the builtin XPath matcher should be used, which
# uses namespaces, or a custom matcher that ignores namespaces.
# Changing this will affect ALL XMLMask matchers.
IGNORE_NS = False
def match(self, xml):
if hasattr(xml, 'xml'):
xml = xml.xml
return self.maskcmp(xml, self._criteria, True)
def maskcmp(self, source, maskobj, use_ns=False, default_ns='__no_ns__'):
"""maskcmp(xmlobj, maskobj):
Compare etree xml object to etree xml object mask"""
use_ns = not ignore_ns
#TODO require namespaces
if source == None: #if element not found (happens during recursive check below)
return False
if not hasattr(maskobj, 'attrib'): #if the mask is a string, make it an xml obj
try:
maskobj = cElementTree.fromstring(maskobj)
except ExpatError:
logging.log(logging.WARNING, "Expat error: %s\nIn parsing: %s" % ('', maskobj))
if not use_ns and source.tag.split('}', 1)[-1] != maskobj.tag.split('}', 1)[-1]: # strip off ns and compare
return False
if use_ns and (source.tag != maskobj.tag and "{%s}%s" % (self.default_ns, maskobj.tag) != source.tag ):
return False
if maskobj.text and source.text != maskobj.text:
return False
for attr_name in maskobj.attrib: #compare attributes
if source.attrib.get(attr_name, "__None__") != maskobj.attrib[attr_name]:
return False
#for subelement in maskobj.getiterator()[1:]: #recursively compare subelements
for subelement in maskobj: #recursively compare subelements
if use_ns:
if not self.maskcmp(source.find(subelement.tag), subelement, use_ns):
return False
else:
if not self.maskcmp(self.getChildIgnoreNS(source, subelement.tag), subelement, use_ns):
return False
return True
def getChildIgnoreNS(self, xml, tag):
tag = tag.split('}')[-1]
try:
idx = [c.tag.split('}')[-1] for c in xml.getchildren()].index(tag)
except ValueError:
return None
return xml.getchildren()[idx]
log = logging.getLogger(__name__)
class MatchXMLMask(MatcherBase):
"""
The XMLMask matcher selects stanzas whose XML matches a given
XML pattern, or mask. For example, message stanzas with body elements
could be matched using the mask:
<message xmlns="jabber:client"><body /></message>
Use of XMLMask is discouraged, and XPath or StanzaPath should be used
instead.
The use of namespaces in the mask comparison is controlled by
IGNORE_NS. Setting IGNORE_NS to True will disable namespace based matching
for ALL XMLMask matchers.
Methods:
match -- Overrides MatcherBase.match.
setDefaultNS -- Set the default namespace for the mask.
"""
def __init__(self, criteria):
"""
Create a new XMLMask matcher.
Arguments:
criteria -- Either an XML object or XML string to use as a mask.
"""
MatcherBase.__init__(self, criteria)
if isinstance(criteria, str):
self._criteria = ET.fromstring(self._criteria)
self.default_ns = 'jabber:client'
def setDefaultNS(self, ns):
"""
Set the default namespace to use during comparisons.
Arguments:
ns -- The new namespace to use as the default.
"""
self.default_ns = ns
def match(self, xml):
"""
Compare a stanza object or XML object against the stored XML mask.
Overrides MatcherBase.match.
Arguments:
xml -- The stanza object or XML object to compare against.
"""
if hasattr(xml, 'xml'):
xml = xml.xml
return self._mask_cmp(xml, self._criteria, True)
def _mask_cmp(self, source, mask, use_ns=False, default_ns='__no_ns__'):
"""
Compare an XML object against an XML mask.
Arguments:
source -- The XML object to compare against the mask.
mask -- The XML object serving as the mask.
use_ns -- Indicates if namespaces should be respected during
the comparison.
default_ns -- The default namespace to apply to elements that
do not have a specified namespace.
Defaults to "__no_ns__".
"""
use_ns = not IGNORE_NS
if source is None:
# If the element was not found. May happend during recursive calls.
return False
# Convert the mask to an XML object if it is a string.
if not hasattr(mask, 'attrib'):
try:
mask = ET.fromstring(mask)
except ExpatError:
log.warning("Expat error: %s\nIn parsing: %s" % ('', mask))
if not use_ns:
# Compare the element without using namespaces.
source_tag = source.tag.split('}', 1)[-1]
mask_tag = mask.tag.split('}', 1)[-1]
if source_tag != mask_tag:
return False
else:
# Compare the element using namespaces
mask_ns_tag = "{%s}%s" % (self.default_ns, mask.tag)
if source.tag not in [mask.tag, mask_ns_tag]:
return False
# If the mask includes text, compare it.
if mask.text and source.text != mask.text:
return False
# Compare attributes. The stanza must include the attributes
# defined by the mask, but may include others.
for name, value in mask.attrib.items():
if source.attrib.get(name, "__None__") != value:
return False
# Recursively check subelements.
for subelement in mask:
if use_ns:
if not self._mask_cmp(source.find(subelement.tag),
subelement, use_ns):
return False
else:
if not self._mask_cmp(self._get_child(source, subelement.tag),
subelement, use_ns):
return False
# Everything matches.
return True
def _get_child(self, xml, tag):
"""
Return a child element given its tag, ignoring namespace values.
Returns None if the child was not found.
Arguments:
xml -- The XML object to search for the given child tag.
tag -- The name of the subelement to find.
"""
tag = tag.split('}')[-1]
try:
children = [c.tag.split('}')[-1] for c in xml.getchildren()]
index = children.index(tag)
except ValueError:
return None
return xml.getchildren()[index]

View File

@@ -3,32 +3,77 @@
Copyright (C) 2010 Nathanael C. Fritz
This file is part of SleekXMPP.
See the file license.txt for copying permission.
See the file LICENSE for copying permission.
"""
from . import base
from xml.etree import cElementTree
ignore_ns = False
from sleekxmpp.xmlstream.stanzabase import ET
from sleekxmpp.xmlstream.matcher.base import MatcherBase
class MatchXPath(base.MatcherBase):
def match(self, xml):
if hasattr(xml, 'xml'):
xml = xml.xml
x = cElementTree.Element('x')
x.append(xml)
if not ignore_ns:
if x.find(self._criteria) is not None:
return True
return False
else:
criteria = [c.split('}')[-1] for c in self._criteria.split('/')]
xml = x
for tag in criteria:
children = [c.tag.split('}')[-1] for c in xml.getchildren()]
try:
idx = children.index(tag)
except ValueError:
return False
xml = xml.getchildren()[idx]
return True
# Flag indicating if the builtin XPath matcher should be used, which
# uses namespaces, or a custom matcher that ignores namespaces.
# Changing this will affect ALL XPath matchers.
IGNORE_NS = False
class MatchXPath(MatcherBase):
"""
The XPath matcher selects stanzas whose XML contents matches a given
XPath expression.
Note that using this matcher may not produce expected behavior when using
attribute selectors. For Python 2.6 and 3.1, the ElementTree find method
does not support the use of attribute selectors. If you need to support
Python 2.6 or 3.1, it might be more useful to use a StanzaPath matcher.
If the value of IGNORE_NS is set to true, then XPath expressions will
be matched without using namespaces.
Methods:
match -- Overrides MatcherBase.match.
"""
def match(self, xml):
"""
Compare a stanza's XML contents to an XPath expression.
If the value of IGNORE_NS is set to true, then XPath expressions
will be matched without using namespaces.
Note that in Python 2.6 and 3.1 the ElementTree find method does
not support attribute selectors in the XPath expression.
Arguments:
xml -- The stanza object to compare against.
"""
if hasattr(xml, 'xml'):
xml = xml.xml
x = ET.Element('x')
x.append(xml)
if not IGNORE_NS:
# Use builtin, namespace respecting, XPath matcher.
if x.find(self._criteria) is not None:
return True
return False
else:
# Remove namespaces from the XPath expression.
criteria = []
for ns_block in self._criteria.split('{'):
criteria.extend(ns_block.split('}')[-1].split('/'))
# Walk the XPath expression.
xml = x
for tag in criteria:
if not tag:
# Skip empty tag name artifacts from the cleanup phase.
continue
children = [c.tag.split('}')[-1] for c in xml.getchildren()]
try:
index = children.index(tag)
except ValueError:
return False
xml = xml.getchildren()[index]
return True

View File

@@ -1,87 +1,205 @@
try:
import queue
except ImportError:
import Queue as queue
"""
SleekXMPP: The Sleek XMPP Library
Copyright (C) 2010 Nathanael C. Fritz
This file is part of SleekXMPP.
See the file LICENSE for copying permission.
"""
import time
import threading
import logging
try:
import queue
except ImportError:
import Queue as queue
log = logging.getLogger(__name__)
class Task(object):
"""Task object for the Scheduler class"""
def __init__(self, name, seconds, callback, args=None, kwargs=None, repeat=False, qpointer=None):
self.name = name
self.seconds = seconds
self.callback = callback
self.args = args or tuple()
self.kwargs = kwargs or {}
self.repeat = repeat
self.next = time.time() + self.seconds
self.qpointer = qpointer
def run(self):
if self.qpointer is not None:
self.qpointer.put(('schedule', self.callback, self.args))
else:
self.callback(*self.args, **self.kwargs)
self.reset()
return self.repeat
def reset(self):
self.next = time.time() + self.seconds
"""
A scheduled task that will be executed by the scheduler
after a given time interval has passed.
Attributes:
name -- The name of the task.
seconds -- The number of seconds to wait before executing.
callback -- The function to execute.
args -- The arguments to pass to the callback.
kwargs -- The keyword arguments to pass to the callback.
repeat -- Indicates if the task should repeat.
Defaults to False.
qpointer -- A pointer to an event queue for queuing callback
execution instead of executing immediately.
Methods:
run -- Either queue or execute the callback.
reset -- Reset the task's timer.
"""
def __init__(self, name, seconds, callback, args=None,
kwargs=None, repeat=False, qpointer=None):
"""
Create a new task.
Arguments:
name -- The name of the task.
seconds -- The number of seconds to wait before executing.
callback -- The function to execute.
args -- The arguments to pass to the callback.
kwargs -- The keyword arguments to pass to the callback.
repeat -- Indicates if the task should repeat.
Defaults to False.
qpointer -- A pointer to an event queue for queuing callback
execution instead of executing immediately.
"""
self.name = name
self.seconds = seconds
self.callback = callback
self.args = args or tuple()
self.kwargs = kwargs or {}
self.repeat = repeat
self.next = time.time() + self.seconds
self.qpointer = qpointer
def run(self):
"""
Execute the task's callback.
If an event queue was supplied, place the callback in the queue;
otherwise, execute the callback immediately.
"""
if self.qpointer is not None:
self.qpointer.put(('schedule', self.callback, self.args))
else:
self.callback(*self.args, **self.kwargs)
self.reset()
return self.repeat
def reset(self):
"""
Reset the task's timer so that it will repeat.
"""
self.next = time.time() + self.seconds
class Scheduler(object):
"""Threaded scheduler that allows for updates mid-execution unlike http://docs.python.org/library/sched.html#module-sched"""
def __init__(self, parentqueue=None):
self.addq = queue.Queue()
self.schedule = []
self.thread = None
self.run = False
self.parentqueue = parentqueue
def process(self, threaded=True):
if threaded:
self.thread = threading.Thread(name='shedulerprocess', target=self._process)
self.thread.start()
else:
self._process()
def _process(self):
self.run = True
while self.run:
try:
wait = 1
updated = False
if self.schedule:
wait = self.schedule[0].next - time.time()
try:
if wait <= 0.0:
newtask = self.addq.get(False)
else:
newtask = self.addq.get(True, wait)
except queue.Empty:
cleanup = []
for task in self.schedule:
if time.time() >= task.next:
updated = True
if not task.run():
cleanup.append(task)
else:
break
for task in cleanup:
x = self.schedule.pop(self.schedule.index(task))
else:
updated = True
self.schedule.append(newtask)
finally:
if updated: self.schedule = sorted(self.schedule, key=lambda task: task.next)
except KeyboardInterrupt:
self.run = False
logging.debug("Quitting Scheduler thread")
if self.parentqueue is not None:
self.parentqueue.put(('quit', None, None))
"""
A threaded scheduler that allows for updates mid-execution unlike the
scheduler in the standard library.
def add(self, name, seconds, callback, args=None, kwargs=None, repeat=False, qpointer=None):
self.addq.put(Task(name, seconds, callback, args, kwargs, repeat, qpointer))
def quit(self):
self.run = False
http://docs.python.org/library/sched.html#module-sched
Attributes:
addq -- A queue storing added tasks.
schedule -- A list of tasks in order of execution times.
thread -- If threaded, the thread processing the schedule.
run -- Indicates if the scheduler is running.
parentqueue -- A parent event queue in control of this scheduler.
Methods:
add -- Add a new task to the schedule.
process -- Process and schedule tasks.
quit -- Stop the scheduler.
"""
def __init__(self, parentqueue=None, parentstop=None):
"""
Create a new scheduler.
Arguments:
parentqueue -- A separate event queue controlling this scheduler.
"""
self.addq = queue.Queue()
self.schedule = []
self.thread = None
self.run = False
self.parentqueue = parentqueue
self.parentstop = parentstop
def process(self, threaded=True):
"""
Begin accepting and processing scheduled tasks.
Arguments:
threaded -- Indicates if the scheduler should execute in its own
thread. Defaults to True.
"""
if threaded:
self.thread = threading.Thread(name='sheduler_process',
target=self._process)
self.thread.start()
else:
self._process()
def _process(self):
"""Process scheduled tasks."""
self.run = True
try:
while self.run and (self.parentstop is None or not self.parentstop.isSet()):
wait = 1
updated = False
if self.schedule:
wait = self.schedule[0].next - time.time()
try:
if wait <= 0.0:
newtask = self.addq.get(False)
else:
newtask = self.addq.get(True, wait)
except queue.Empty:
cleanup = []
for task in self.schedule:
if time.time() >= task.next:
updated = True
if not task.run():
cleanup.append(task)
else:
break
for task in cleanup:
x = self.schedule.pop(self.schedule.index(task))
else:
updated = True
self.schedule.append(newtask)
finally:
if updated:
self.schedule = sorted(self.schedule,
key=lambda task: task.next)
except KeyboardInterrupt:
self.run = False
if self.parentstop is not None:
log.debug("stopping parent")
self.parentstop.set()
except SystemExit:
self.run = False
if self.parentstop is not None:
self.parentstop.set()
log.debug("Quitting Scheduler thread")
if self.parentqueue is not None:
self.parentqueue.put(('quit', None, None))
def add(self, name, seconds, callback, args=None,
kwargs=None, repeat=False, qpointer=None):
"""
Schedule a new task.
Arguments:
name -- The name of the task.
seconds -- The number of seconds to wait before executing.
callback -- The function to execute.
args -- The arguments to pass to the callback.
kwargs -- The keyword arguments to pass to the callback.
repeat -- Indicates if the task should repeat.
Defaults to False.
qpointer -- A pointer to an event queue for queuing callback
execution instead of executing immediately.
"""
self.addq.put(Task(name, seconds, callback, args,
kwargs, repeat, qpointer))
def quit(self):
"""Shutdown the scheduler."""
self.run = False

File diff suppressed because it is too large Load Diff

View File

@@ -1,234 +0,0 @@
"""
SleekXMPP: The Sleek XMPP Library
Copyright (C) 2010 Nathanael C. Fritz
This file is part of SleekXMPP.
See the file license.txt for copying permission.
"""
from __future__ import with_statement
import threading
import time
import logging
class StateMachine(object):
def __init__(self, states=[]):
self.lock = threading.Condition(threading.RLock())
self.__states= []
self.addStates(states)
self.__default_state = self.__states[0]
self.__current_state = self.__default_state
def addStates(self, states):
with self.lock:
for state in states:
if state in self.__states:
raise IndexError("The state '%s' is already in the StateMachine." % state)
self.__states.append( state )
def transition(self, from_state, to_state, wait=0.0, func=None, args=[], kwargs={} ):
'''
Transition from the given `from_state` to the given `to_state`.
This method will return `True` if the state machine is now in `to_state`. It
will return `False` if a timeout occurred the transition did not occur.
If `wait` is 0 (the default,) this method returns immediately if the state machine
is not in `from_state`.
If you want the thread to block and transition once the state machine to enters
`from_state`, set `wait` to a non-negative value. Note there is no 'block
indefinitely' flag since this leads to deadlock. If you want to wait indefinitely,
choose a reasonable value for `wait` (e.g. 20 seconds) and do so in a while loop like so:
::
while not thread_should_exit and not state_machine.transition('disconnected', 'connecting', wait=20 ):
pass # timeout will occur every 20s unless transition occurs
if thread_should_exit: return
# perform actions here after successful transition
This allows the thread to be responsive by setting `thread_should_exit=True`.
The optional `func` argument allows the user to pass a callable operation which occurs
within the context of the state transition (e.g. while the state machine is locked.)
If `func` returns a True value, the transition will occur. If `func` returns a non-
True value or if an exception is thrown, the transition will not occur. Any thrown
exception is not caught by the state machine and is the caller's responsibility to handle.
If `func` completes normally, this method will return the value returned by `func.` If
values for `args` and `kwargs` are provided, they are expanded and passed like so:
`func( *args, **kwargs )`.
'''
return self.transition_any( (from_state,), to_state, wait=wait,
func=func, args=args, kwargs=kwargs )
def transition_any(self, from_states, to_state, wait=0.0, func=None, args=[], kwargs={} ):
'''
Transition from any of the given `from_states` to the given `to_state`.
'''
if not (isinstance(from_states,tuple) or isinstance(from_states,list)):
raise ValueError( "from_states should be a list or tuple" )
for state in from_states:
if not state in self.__states:
raise ValueError( "StateMachine does not contain from_state %s." % state )
if not to_state in self.__states:
raise ValueError( "StateMachine does not contain to_state %s." % to_state )
with self.lock:
start = time.time()
while not self.__current_state in from_states:
# detect timeout:
if time.time() >= start + wait: return False
self.lock.wait(wait)
if self.__current_state in from_states: # should always be True due to lock
return_val = True
# Note that func might throw an exception, but that's OK, it aborts the transition
if func is not None: return_val = func(*args,**kwargs)
# some 'false' value returned from func,
# indicating that transition should not occur:
if not return_val: return return_val
logging.debug(' ==== TRANSITION %s -> %s', self.__current_state, to_state)
self.__current_state = to_state
self.lock.notify_all()
return return_val # some 'true' value returned by func or True if func was None
else:
logging.error( "StateMachine bug!! The lock should ensure this doesn't happen!" )
return False
def transition_ctx(self, from_state, to_state, wait=0.0):
'''
Use the state machine as a context manager. The transition occurs on /exit/ from
the `with` context, so long as no exception is thrown. For example:
::
with state_machine.transition_ctx('one','two', wait=5) as locked:
if locked:
# the state machine is currently locked in state 'one', and will
# transition to 'two' when the 'with' statement ends, so long as
# no exception is thrown.
print 'Currently locked in state one: %s' % state_machine['one']
else:
# The 'wait' timed out, and no lock has been acquired
print 'Timed out before entering state "one"'
print 'Since no exception was thrown, we are now in state "two": %s' % state_machine['two']
The other main difference between this method and `transition()` is that the
state machine is locked for the duration of the `with` statement. Normally,
after a `transition()` occurs, the state machine is immediately unlocked and
available to another thread to call `transition()` again.
'''
if not from_state in self.__states:
raise ValueError( "StateMachine does not contain from_state %s." % from_state )
if not to_state in self.__states:
raise ValueError( "StateMachine does not contain to_state %s." % to_state )
return _StateCtx(self, from_state, to_state, wait)
def ensure(self, state, wait=0.0):
'''
Ensure the state machine is currently in `state`, or wait until it enters `state`.
'''
return self.ensure_any( (state,), wait=wait )
def ensure_any(self, states, wait=0.0):
'''
Ensure we are currently in one of the given `states`
'''
if not (isinstance(states,tuple) or isinstance(states,list)):
raise ValueError('states arg should be a tuple or list')
for state in states:
if not state in self.__states:
raise ValueError( "StateMachine does not contain state '%s'" % state )
with self.lock:
start = time.time()
while not self.__current_state in states:
# detect timeout:
if time.time() >= start + wait: return False
self.lock.wait(wait)
return self.__current_state in states # should always be True due to lock
def reset(self):
# TODO need to lock before calling this?
self.transition(self.__current_state, self._default_state)
def _set_state(self, state): #unsynchronized, only call internally after lock is acquired
self.__current_state = state
return state
def current_state(self):
'''
Return the current state name.
'''
return self.__current_state
def __getitem__(self, state):
'''
Non-blocking, non-synchronized test to determine if we are in the given state.
Use `StateMachine.ensure(state)` to wait until the machine enters a certain state.
'''
return self.__current_state == state
def __str__(self):
return "".join(( "StateMachine(", ','.join(self.__states), "): ", self.__current_state ))
class _StateCtx:
def __init__( self, state_machine, from_state, to_state, wait ):
self.state_machine = state_machine
self.from_state = from_state
self.to_state = to_state
self.wait = wait
self._timeout = False
def __enter__(self):
self.state_machine.lock.acquire()
start = time.time()
while not self.state_machine[ self.from_state ]:
# detect timeout:
if time.time() >= start + self.wait:
logging.debug('StateMachine timeout while waiting for state: %s', self.from_state )
self._timeout = True # to indicate we should not transition
return False
self.state_machine.lock.wait(self.wait)
logging.debug('StateMachine entered context in state: %s',
self.state_machine.current_state() )
return True
def __exit__(self, exc_type, exc_val, exc_tb):
if exc_val is not None:
logging.exception( "StateMachine exception in context, remaining in state: %s\n%s:%s",
self.state_machine.current_state(), exc_type.__name__, exc_val )
elif not self._timeout:
logging.debug(' ==== TRANSITION %s -> %s',
self.state_machine.current_state(), self.to_state)
self.state_machine._set_state( self.to_state )
self.state_machine.lock.notify_all()
self.state_machine.lock.release()
return False # re-raise any exception

View File

@@ -1,60 +1,19 @@
"""
SleekXMPP: The Sleek XMPP Library
Copyright (C) 2010 Nathanael C. Fritz
This file is part of SleekXMPP.
class ToString(object):
def __str__(self, xml=None, xmlns='', stringbuffer=''):
if xml is None:
xml = self.xml
newoutput = [stringbuffer]
#TODO respect ET mapped namespaces
itag = xml.tag.split('}', 1)[-1]
if '}' in xml.tag:
ixmlns = xml.tag.split('}', 1)[0][1:]
else:
ixmlns = ''
nsbuffer = ''
if xmlns != ixmlns and ixmlns != '' and ixmlns != self.namespace:
if self.stream is not None and ixmlns in self.stream.namespace_map:
if self.stream.namespace_map[ixmlns] != '':
itag = "%s:%s" % (self.stream.namespace_map[ixmlns], itag)
else:
nsbuffer = """ xmlns="%s\"""" % ixmlns
if ixmlns not in ('', xmlns, self.namespace):
nsbuffer = """ xmlns="%s\"""" % ixmlns
newoutput.append("<%s" % itag)
newoutput.append(nsbuffer)
for attrib in xml.attrib:
if '{' not in attrib:
newoutput.append(""" %s="%s\"""" % (attrib, self.xmlesc(xml.attrib[attrib])))
if len(xml) or xml.text or xml.tail:
newoutput.append(">")
if xml.text:
newoutput.append(self.xmlesc(xml.text))
if len(xml):
for child in xml.getchildren():
newoutput.append(self.__str__(child, ixmlns))
newoutput.append("</%s>" % (itag, ))
if xml.tail:
newoutput.append(self.xmlesc(xml.tail))
elif xml.text:
newoutput.append(">%s</%s>" % (self.xmlesc(xml.text), itag))
else:
newoutput.append(" />")
return ''.join(newoutput)
See the file LICENSE for copying permission.
"""
def xmlesc(self, text):
text = list(text)
cc = 0
matches = ('&', '<', '"', '>', "'")
for c in text:
if c in matches:
if c == '&':
text[cc] = '&amp;'
elif c == '<':
text[cc] = '&lt;'
elif c == '>':
text[cc] = '&gt;'
elif c == "'":
text[cc] = '&apos;'
else:
text[cc] = '&quot;'
cc += 1
return ''.join(text)
import sys
# Import the correct tostring and xml_escape functions based on the Python
# version in order to properly handle Unicode.
if sys.version_info < (3, 0):
from sleekxmpp.xmlstream.tostring.tostring26 import tostring, xml_escape
else:
from sleekxmpp.xmlstream.tostring.tostring import tostring, xml_escape
__all__ = ['tostring', 'xml_escape']

View File

@@ -0,0 +1,95 @@
"""
SleekXMPP: The Sleek XMPP Library
Copyright (C) 2010 Nathanael C. Fritz
This file is part of SleekXMPP.
See the file LICENSE for copying permission.
"""
def tostring(xml=None, xmlns='', stanza_ns='', stream=None, outbuffer=''):
"""
Serialize an XML object to a Unicode string.
If namespaces are provided using xmlns or stanza_ns, then elements
that use those namespaces will not include the xmlns attribute in
the output.
Arguments:
xml -- The XML object to serialize. If the value is None,
then the XML object contained in this stanza
object will be used.
xmlns -- Optional namespace of an element wrapping the XML
object.
stanza_ns -- The namespace of the stanza object that contains
the XML object.
stream -- The XML stream that generated the XML object.
outbuffer -- Optional buffer for storing serializations during
recursive calls.
"""
# Add previous results to the start of the output.
output = [outbuffer]
# Extract the element's tag name.
tag_name = xml.tag.split('}', 1)[-1]
# Extract the element's namespace if it is defined.
if '}' in xml.tag:
tag_xmlns = xml.tag.split('}', 1)[0][1:]
else:
tag_xmlns = ''
# Output the tag name and derived namespace of the element.
namespace = ''
if tag_xmlns not in ['', xmlns, stanza_ns]:
namespace = ' xmlns="%s"' % tag_xmlns
if stream and tag_xmlns in stream.namespace_map:
mapped_namespace = stream.namespace_map[tag_xmlns]
if mapped_namespace:
tag_name = "%s:%s" % (mapped_namespace, tag_name)
output.append("<%s" % tag_name)
output.append(namespace)
# Output escaped attribute values.
for attrib, value in xml.attrib.items():
if '{' not in attrib:
value = xml_escape(value)
output.append(' %s="%s"' % (attrib, value))
if len(xml) or xml.text:
# If there are additional child elements to serialize.
output.append(">")
if xml.text:
output.append(xml_escape(xml.text))
if len(xml):
for child in xml.getchildren():
output.append(tostring(child, tag_xmlns, stanza_ns, stream))
output.append("</%s>" % tag_name)
elif xml.text:
# If we only have text content.
output.append(">%s</%s>" % (xml_escape(xml.text), tag_name))
else:
# Empty element.
output.append(" />")
if xml.tail:
# If there is additional text after the element.
output.append(xml_escape(xml.tail))
return ''.join(output)
def xml_escape(text):
"""
Convert special characters in XML to escape sequences.
Arguments:
text -- The XML text to convert.
"""
text = list(text)
escapes = {'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
"'": '&apos;',
'"': '&quot;'}
for i, c in enumerate(text):
text[i] = escapes.get(c, c)
return ''.join(text)

View File

@@ -0,0 +1,101 @@
"""
SleekXMPP: The Sleek XMPP Library
Copyright (C) 2010 Nathanael C. Fritz
This file is part of SleekXMPP.
See the file LICENSE for copying permission.
"""
from __future__ import unicode_literals
import types
def tostring(xml=None, xmlns='', stanza_ns='', stream=None, outbuffer=''):
"""
Serialize an XML object to a Unicode string.
If namespaces are provided using xmlns or stanza_ns, then elements
that use those namespaces will not include the xmlns attribute in
the output.
Arguments:
xml -- The XML object to serialize. If the value is None,
then the XML object contained in this stanza
object will be used.
xmlns -- Optional namespace of an element wrapping the XML
object.
stanza_ns -- The namespace of the stanza object that contains
the XML object.
stream -- The XML stream that generated the XML object.
outbuffer -- Optional buffer for storing serializations during
recursive calls.
"""
# Add previous results to the start of the output.
output = [outbuffer]
# Extract the element's tag name.
tag_name = xml.tag.split('}', 1)[-1]
# Extract the element's namespace if it is defined.
if '}' in xml.tag:
tag_xmlns = xml.tag.split('}', 1)[0][1:]
else:
tag_xmlns = u''
# Output the tag name and derived namespace of the element.
namespace = u''
if tag_xmlns not in ['', xmlns, stanza_ns]:
namespace = u' xmlns="%s"' % tag_xmlns
if stream and tag_xmlns in stream.namespace_map:
mapped_namespace = stream.namespace_map[tag_xmlns]
if mapped_namespace:
tag_name = u"%s:%s" % (mapped_namespace, tag_name)
output.append(u"<%s" % tag_name)
output.append(namespace)
# Output escaped attribute values.
for attrib, value in xml.attrib.items():
if '{' not in attrib:
value = xml_escape(value)
output.append(u' %s="%s"' % (attrib, value))
if len(xml) or xml.text:
# If there are additional child elements to serialize.
output.append(u">")
if xml.text:
output.append(xml_escape(xml.text))
if len(xml):
for child in xml.getchildren():
output.append(tostring(child, tag_xmlns, stanza_ns, stream))
output.append(u"</%s>" % tag_name)
elif xml.text:
# If we only have text content.
output.append(u">%s</%s>" % (xml_escape(xml.text), tag_name))
else:
# Empty element.
output.append(u" />")
if xml.tail:
# If there is additional text after the element.
output.append(xml_escape(xml.tail))
return u''.join(output)
def xml_escape(text):
"""
Convert special characters in XML to escape sequences.
Arguments:
text -- The XML text to convert.
"""
if type(text) != types.UnicodeType:
text = list(unicode(text, 'utf-8', 'ignore'))
else:
text = list(text)
escapes = {u'&': u'&amp;',
u'<': u'&lt;',
u'>': u'&gt;',
u"'": u'&apos;',
u'"': u'&quot;'}
for i, c in enumerate(text):
text[i] = escapes.get(c, c)
return u''.join(text)

View File

@@ -1,65 +0,0 @@
import types
class ToString(object):
def __str__(self, xml=None, xmlns='', stringbuffer=''):
if xml is None:
xml = self.xml
newoutput = [stringbuffer]
#TODO respect ET mapped namespaces
itag = xml.tag.split('}', 1)[-1]
if '}' in xml.tag:
ixmlns = xml.tag.split('}', 1)[0][1:]
else:
ixmlns = ''
nsbuffer = ''
if xmlns != ixmlns and ixmlns != u'' and ixmlns != self.namespace:
if self.stream is not None and ixmlns in self.stream.namespace_map:
if self.stream.namespace_map[ixmlns] != u'':
itag = "%s:%s" % (self.stream.namespace_map[ixmlns], itag)
else:
nsbuffer = """ xmlns="%s\"""" % ixmlns
if ixmlns not in ('', xmlns, self.namespace):
nsbuffer = """ xmlns="%s\"""" % ixmlns
newoutput.append("<%s" % itag)
newoutput.append(nsbuffer)
for attrib in xml.attrib:
if '{' not in attrib:
newoutput.append(""" %s="%s\"""" % (attrib, self.xmlesc(xml.attrib[attrib])))
if len(xml) or xml.text or xml.tail:
newoutput.append(u">")
if xml.text:
newoutput.append(self.xmlesc(xml.text))
if len(xml):
for child in xml.getchildren():
newoutput.append(self.__str__(child, ixmlns))
newoutput.append(u"</%s>" % (itag, ))
if xml.tail:
newoutput.append(self.xmlesc(xml.tail))
elif xml.text:
newoutput.append(">%s</%s>" % (self.xmlesc(xml.text), itag))
else:
newoutput.append(" />")
return u''.join(newoutput)
def xmlesc(self, text):
if type(text) != types.UnicodeType:
text = list(unicode(text, 'utf-8', 'ignore'))
else:
text = list(text)
cc = 0
matches = (u'&', u'<', u'"', u'>', u"'")
for c in text:
if c in matches:
if c == u'&':
text[cc] = u'&amp;'
elif c == u'<':
text[cc] = u'&lt;'
elif c == u'>':
text[cc] = u'&gt;'
elif c == u"'":
text[cc] = u'&apos;'
else:
text[cc] = u'&quot;'
cc += 1
return ''.join(text)

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,4 @@
#!/usr/bin/python2.6
#!/usr/bin/env python
import unittest
import logging
import sys
@@ -13,7 +13,7 @@ class testoverall(unittest.TestCase):
if sys.version_info < (3,0):
self.failUnless(compileall.compile_dir('.' + os.sep + 'sleekxmpp', rx=re.compile('/[.]svn'), quiet=True))
else:
self.failUnless(compileall.compile_dir('.' + os.sep + 'sleekxmpp', rx=re.compile('/[.]svn|.*26\Z'), quiet=True))
self.failUnless(compileall.compile_dir('.' + os.sep + 'sleekxmpp', rx=re.compile('/[.]svn|.*26.*'), quiet=True))
def testTabNanny(self):
"""Invoking the tabnanny"""
@@ -21,7 +21,7 @@ class testoverall(unittest.TestCase):
self.failIf(tabnanny.check("." + os.sep + 'sleekxmpp'))
#raise "Help!"
def testMethodLength(self):
def disabled_testMethodLength(self):
"""Testing for excessive method lengths"""
import re
dirs = os.walk(sys.path[0] + os.sep + 'sleekxmpp')

110
tests/live_test.py Normal file
View File

@@ -0,0 +1,110 @@
from sleekxmpp.test import *
import sleekxmpp.plugins.xep_0033 as xep_0033
class TestLiveStream(SleekTest):
"""
Test that we can test a live stanza stream.
"""
def tearDown(self):
self.stream_close()
def testClientConnection(self):
"""Test that we can interact with a live ClientXMPP instance."""
self.stream_start(mode='client',
socket='live',
skip=False,
jid='user@localhost/test',
password='user')
# Use sid=None to ignore any id sent by the server since
# we can't know it in advance.
self.recv_header(sfrom='localhost', sid=None)
self.send_header(sto='localhost')
self.recv_feature("""
<stream:features>
<starttls xmlns="urn:ietf:params:xml:ns:xmpp-tls" />
<mechanisms xmlns="urn:ietf:params:xml:ns:xmpp-sasl">
<mechanism>DIGEST-MD5</mechanism>
<mechanism>PLAIN</mechanism>
</mechanisms>
<c xmlns="http://jabber.org/protocol/caps"
node="http://www.process-one.net/en/ejabberd/"
ver="TQ2JFyRoSa70h2G1bpgjzuXb2sU=" hash="sha-1" />
<register xmlns="http://jabber.org/features/iq-register" />
</stream:features>
""")
self.send_feature("""
<starttls xmlns="urn:ietf:params:xml:ns:xmpp-tls" />
""")
self.recv_feature("""
<proceed xmlns="urn:ietf:params:xml:ns:xmpp-tls" />
""")
self.send_header(sto='localhost')
self.recv_header(sfrom='localhost', sid=None)
self.recv_feature("""
<stream:features>
<mechanisms xmlns="urn:ietf:params:xml:ns:xmpp-sasl">
<mechanism>DIGEST-MD5</mechanism>
<mechanism>PLAIN</mechanism>
</mechanisms>
<c xmlns="http://jabber.org/protocol/caps"
node="http://www.process-one.net/en/ejabberd/"
ver="TQ2JFyRoSa70h2G1bpgjzuXb2sU="
hash="sha-1" />
<register xmlns="http://jabber.org/features/iq-register" />
</stream:features>
""")
self.send_feature("""
<auth xmlns="urn:ietf:params:xml:ns:xmpp-sasl"
mechanism="PLAIN">AHVzZXIAdXNlcg==</auth>
""")
self.recv_feature("""
<success xmlns="urn:ietf:params:xml:ns:xmpp-sasl" />
""")
self.send_header(sto='localhost')
self.recv_header(sfrom='localhost', sid=None)
self.recv_feature("""
<stream:features>
<bind xmlns="urn:ietf:params:xml:ns:xmpp-bind" />
<session xmlns="urn:ietf:params:xml:ns:xmpp-session" />
<c xmlns="http://jabber.org/protocol/caps"
node="http://www.process-one.net/en/ejabberd/"
ver="TQ2JFyRoSa70h2G1bpgjzuXb2sU="
hash="sha-1" />
<register xmlns="http://jabber.org/features/iq-register" />
</stream:features>
""")
# Should really use send, but our Iq stanza objects
# can't handle bind element payloads yet.
self.send_feature("""
<iq type="set" id="1">
<bind xmlns="urn:ietf:params:xml:ns:xmpp-bind">
<resource>test</resource>
</bind>
</iq>
""")
self.recv_feature("""
<iq type="result" id="1">
<bind xmlns="urn:ietf:params:xml:ns:xmpp-bind">
<jid>user@localhost/test</jid>
</bind>
</iq>
""")
self.stream_close()
suite = unittest.TestLoader().loadTestsFromTestCase(TestLiveStream)
if __name__ == '__main__':
tests = unittest.TestSuite([suite])
result = unittest.TextTestRunner(verbosity=2).run(tests)
test_ns = 'http://andyet.net/protocol/tests'
print("<tests xmlns='%s' %s %s %s %s />" % (
test_ns,
'ran="%s"' % result.testsRun,
'errors="%s"' % len(result.errors),
'fails="%s"' % len(result.failures),
'success="%s"' % result.wasSuccessful()))

View File

@@ -1,155 +0,0 @@
import unittest
from xml.etree import cElementTree as ET
from sleekxmpp.xmlstream.matcher.stanzapath import StanzaPath
from . import xmlcompare
import sleekxmpp.plugins.xep_0030 as sd
def stanzaPlugin(stanza, plugin):
stanza.plugin_attrib_map[plugin.plugin_attrib] = plugin
stanza.plugin_tag_map["{%s}%s" % (plugin.namespace, plugin.name)] = plugin
class testdisco(unittest.TestCase):
def setUp(self):
self.sd = sd
stanzaPlugin(self.sd.Iq, self.sd.DiscoInfo)
stanzaPlugin(self.sd.Iq, self.sd.DiscoItems)
def try3Methods(self, xmlstring, iq):
iq2 = self.sd.Iq(None, self.sd.ET.fromstring(xmlstring))
values = iq2.getValues()
iq3 = self.sd.Iq()
iq3.setValues(values)
self.failUnless(xmlstring == str(iq) == str(iq2) == str(iq3), str(iq)+"3 methods for creating stanza don't match")
def testCreateInfoQueryNoNode(self):
"""Testing disco#info query with no node."""
iq = self.sd.Iq()
iq['id'] = "0"
iq['disco_info']['node'] = ''
xmlstring = """<iq id="0"><query xmlns="http://jabber.org/protocol/disco#info" /></iq>"""
self.try3Methods(xmlstring, iq)
def testCreateInfoQueryWithNode(self):
"""Testing disco#info query with a node."""
iq = self.sd.Iq()
iq['id'] = "0"
iq['disco_info']['node'] = 'foo'
xmlstring = """<iq id="0"><query xmlns="http://jabber.org/protocol/disco#info" node="foo" /></iq>"""
self.try3Methods(xmlstring, iq)
def testCreateInfoQueryNoNode(self):
"""Testing disco#items query with no node."""
iq = self.sd.Iq()
iq['id'] = "0"
iq['disco_items']['node'] = ''
xmlstring = """<iq id="0"><query xmlns="http://jabber.org/protocol/disco#items" /></iq>"""
self.try3Methods(xmlstring, iq)
def testCreateItemsQueryWithNode(self):
"""Testing disco#items query with a node."""
iq = self.sd.Iq()
iq['id'] = "0"
iq['disco_items']['node'] = 'foo'
xmlstring = """<iq id="0"><query xmlns="http://jabber.org/protocol/disco#items" node="foo" /></iq>"""
self.try3Methods(xmlstring, iq)
def testInfoIdentities(self):
"""Testing adding identities to disco#info."""
iq = self.sd.Iq()
iq['id'] = "0"
iq['disco_info']['node'] = 'foo'
iq['disco_info'].addIdentity('conference', 'text', 'Chatroom')
xmlstring = """<iq id="0"><query xmlns="http://jabber.org/protocol/disco#info" node="foo"><identity category="conference" type="text" name="Chatroom" /></query></iq>"""
self.try3Methods(xmlstring, iq)
def testInfoFeatures(self):
"""Testing adding features to disco#info."""
iq = self.sd.Iq()
iq['id'] = "0"
iq['disco_info']['node'] = 'foo'
iq['disco_info'].addFeature('foo')
iq['disco_info'].addFeature('bar')
xmlstring = """<iq id="0"><query xmlns="http://jabber.org/protocol/disco#info" node="foo"><feature var="foo" /><feature var="bar" /></query></iq>"""
self.try3Methods(xmlstring, iq)
def testItems(self):
"""Testing adding features to disco#info."""
iq = self.sd.Iq()
iq['id'] = "0"
iq['disco_items']['node'] = 'foo'
iq['disco_items'].addItem('user@localhost')
iq['disco_items'].addItem('user@localhost', 'foo')
iq['disco_items'].addItem('user@localhost', 'bar', 'Testing')
xmlstring = """<iq id="0"><query xmlns="http://jabber.org/protocol/disco#items" node="foo"><item jid="user@localhost" /><item node="foo" jid="user@localhost" /><item node="bar" jid="user@localhost" name="Testing" /></query></iq>"""
self.try3Methods(xmlstring, iq)
def testAddRemoveIdentities(self):
"""Test adding and removing identities to disco#info stanza"""
ids = [('automation', 'commands', 'AdHoc'),
('conference', 'text', 'ChatRoom')]
info = self.sd.DiscoInfo()
info.addIdentity(*ids[0])
self.failUnless(info.getIdentities() == [ids[0]])
info.delIdentity('automation', 'commands')
self.failUnless(info.getIdentities() == [])
info.setIdentities(ids)
self.failUnless(info.getIdentities() == ids)
info.delIdentity('automation', 'commands')
self.failUnless(info.getIdentities() == [ids[1]])
info.delIdentities()
self.failUnless(info.getIdentities() == [])
def testAddRemoveFeatures(self):
"""Test adding and removing features to disco#info stanza"""
features = ['foo', 'bar', 'baz']
info = self.sd.DiscoInfo()
info.addFeature(features[0])
self.failUnless(info.getFeatures() == [features[0]])
info.delFeature('foo')
self.failUnless(info.getFeatures() == [])
info.setFeatures(features)
self.failUnless(info.getFeatures() == features)
info.delFeature('bar')
self.failUnless(info.getFeatures() == ['foo', 'baz'])
info.delFeatures()
self.failUnless(info.getFeatures() == [])
def testAddRemoveItems(self):
"""Test adding and removing items to disco#items stanza"""
items = [('user@localhost', None, None),
('user@localhost', 'foo', None),
('user@localhost', 'bar', 'Test')]
info = self.sd.DiscoItems()
self.failUnless(True, ""+str(items[0]))
info.addItem(*(items[0]))
self.failUnless(info.getItems() == [items[0]], info.getItems())
info.delItem('user@localhost')
self.failUnless(info.getItems() == [])
info.setItems(items)
self.failUnless(info.getItems() == items)
info.delItem('user@localhost', 'foo')
self.failUnless(info.getItems() == [items[0], items[2]])
info.delItems()
self.failUnless(info.getItems() == [])
suite = unittest.TestLoader().loadTestsFromTestCase(testdisco)

View File

@@ -1,35 +1,72 @@
import unittest
import time
from sleekxmpp.test import *
class testevents(unittest.TestCase):
def setUp(self):
import sleekxmpp.stanza.presence as p
self.p = p
def testEventHappening(self):
"Test handler working"
import sleekxmpp
c = sleekxmpp.ClientXMPP('crap@wherever', 'password')
happened = []
def handletestevent(event):
happened.append(True)
c.add_event_handler("test_event", handletestevent)
c.event("test_event", {})
c.event("test_event", {})
self.failUnless(happened == [True, True], "event did not get triggered twice")
def testDelEvent(self):
"Test handler working, then deleted and not triggered"
import sleekxmpp
c = sleekxmpp.ClientXMPP('crap@wherever', 'password')
happened = []
def handletestevent(event):
happened.append(True)
c.add_event_handler("test_event", handletestevent)
c.event("test_event", {})
c.del_event_handler("test_event", handletestevent)
c.event("test_event", {}) # should not trigger because it was deleted
self.failUnless(happened == [True], "event did not get triggered the correct number of times")
class TestEvents(SleekTest):
suite = unittest.TestLoader().loadTestsFromTestCase(testevents)
def setUp(self):
self.stream_start()
def tearDown(self):
self.stream_close()
def testEventHappening(self):
"""Test handler working"""
happened = []
def handletestevent(event):
happened.append(True)
self.xmpp.add_event_handler("test_event", handletestevent)
self.xmpp.event("test_event")
self.xmpp.event("test_event")
# Give the event queue time to process.
time.sleep(0.1)
msg = "Event was not triggered the correct number of times: %s"
self.failUnless(happened == [True, True], msg)
def testDelEvent(self):
"""Test handler working, then deleted and not triggered"""
happened = []
def handletestevent(event):
happened.append(True)
self.xmpp.add_event_handler("test_event", handletestevent)
self.xmpp.event("test_event", {})
self.xmpp.del_event_handler("test_event", handletestevent)
# Should not trigger because it was deleted
self.xmpp.event("test_event", {})
# Give the event queue time to process.
time.sleep(0.1)
msg = "Event was not triggered the correct number of times: %s"
self.failUnless(happened == [True], msg % happened)
def testDisposableEvent(self):
"""Test disposable handler working, then not being triggered again."""
happened = []
def handletestevent(event):
happened.append(True)
self.xmpp.add_event_handler("test_event", handletestevent,
disposable=True)
self.xmpp.event("test_event", {})
# Should not trigger because it was deleted
self.xmpp.event("test_event", {})
# Give the event queue time to process.
time.sleep(0.1)
msg = "Event was not triggered the correct number of times: %s"
self.failUnless(happened == [True], msg % happened)
suite = unittest.TestLoader().loadTestsFromTestCase(TestEvents)

128
tests/test_jid.py Normal file
View File

@@ -0,0 +1,128 @@
from sleekxmpp.test import *
from sleekxmpp.xmlstream.jid import JID
class TestJIDClass(SleekTest):
"""Verify that the JID class can parse and manipulate JIDs."""
def testJIDFromFull(self):
"""Test using JID of the form 'user@server/resource/with/slashes'."""
self.check_jid(JID('user@someserver/some/resource'),
'user',
'someserver',
'some/resource',
'user@someserver',
'user@someserver/some/resource',
'user@someserver/some/resource')
def testJIDchange(self):
"""Test changing JID of the form 'user@server/resource/with/slashes'"""
j = JID('user1@someserver1/some1/resource1')
j.user = 'user'
j.domain = 'someserver'
j.resource = 'some/resource'
self.check_jid(j,
'user',
'someserver',
'some/resource',
'user@someserver',
'user@someserver/some/resource',
'user@someserver/some/resource')
def testJIDaliases(self):
"""Test changing JID using aliases for domain."""
j = JID('user@someserver/resource')
j.server = 'anotherserver'
self.check_jid(j, domain='anotherserver')
j.host = 'yetanother'
self.check_jid(j, domain='yetanother')
def testJIDSetFullWithUser(self):
"""Test setting the full JID with a user portion."""
j = JID('user@domain/resource')
j.full = 'otheruser@otherdomain/otherresource'
self.check_jid(j,
'otheruser',
'otherdomain',
'otherresource',
'otheruser@otherdomain',
'otheruser@otherdomain/otherresource',
'otheruser@otherdomain/otherresource')
def testJIDFullNoUserWithResource(self):
"""
Test setting the full JID without a user
portion and with a resource.
"""
j = JID('user@domain/resource')
j.full = 'otherdomain/otherresource'
self.check_jid(j,
'',
'otherdomain',
'otherresource',
'otherdomain',
'otherdomain/otherresource',
'otherdomain/otherresource')
def testJIDFullNoUserNoResource(self):
"""
Test setting the full JID without a user
portion and without a resource.
"""
j = JID('user@domain/resource')
j.full = 'otherdomain'
self.check_jid(j,
'',
'otherdomain',
'',
'otherdomain',
'otherdomain',
'otherdomain')
def testJIDBareUser(self):
"""Test setting the bare JID with a user."""
j = JID('user@domain/resource')
j.bare = 'otheruser@otherdomain'
self.check_jid(j,
'otheruser',
'otherdomain',
'resource',
'otheruser@otherdomain',
'otheruser@otherdomain/resource',
'otheruser@otherdomain/resource')
def testJIDBareNoUser(self):
"""Test setting the bare JID without a user."""
j = JID('user@domain/resource')
j.bare = 'otherdomain'
self.check_jid(j,
'',
'otherdomain',
'resource',
'otherdomain',
'otherdomain/resource',
'otherdomain/resource')
def testJIDNoResource(self):
"""Test using JID of the form 'user@domain'."""
self.check_jid(JID('user@someserver'),
'user',
'someserver',
'',
'user@someserver',
'user@someserver',
'user@someserver')
def testJIDNoUser(self):
"""Test JID of the form 'component.domain.tld'."""
self.check_jid(JID('component.someserver'),
'',
'component.someserver',
'',
'component.someserver',
'component.someserver',
'component.someserver')
suite = unittest.TestLoader().loadTestsFromTestCase(TestJIDClass)

View File

@@ -1,44 +0,0 @@
import unittest
from xml.etree import cElementTree as ET
class testmessagestanzas(unittest.TestCase):
def setUp(self):
import sleekxmpp.stanza.message as m
from sleekxmpp.basexmpp import stanzaPlugin
from sleekxmpp.stanza.htmlim import HTMLIM
stanzaPlugin(m.Message, HTMLIM)
self.m = m
def testGroupchatReplyRegression(self):
"Regression groupchat reply should be to barejid"
msg = self.m.Message()
msg['to'] = 'me@myserver.tld'
msg['from'] = 'room@someservice.someserver.tld/somenick'
msg['type'] = 'groupchat'
msg['body'] = "this is a message"
msg.reply()
self.failUnless(str(msg['to']) == 'room@someservice.someserver.tld')
def testAttribProperty(self):
"Test attrib property returning self"
msg = self.m.Message()
msg.attrib.attrib.attrib['to'] = 'usr@server.tld'
self.failUnless(str(msg['to']) == 'usr@server.tld')
def testHTMLPlugin(self):
"Test message/html/html stanza"
msgtxt = """<message to="fritzy@netflint.net/sleekxmpp" type="chat"><body>this is the plaintext message</body><html xmlns="http://jabber.org/protocol/xhtml-im"><body xmlns="http://www.w3.org/1999/xhtml"><p>This is the htmlim message</p></body></html></message>"""
msg = self.m.Message()
msg['to'] = "fritzy@netflint.net/sleekxmpp"
msg['body'] = "this is the plaintext message"
msg['type'] = 'chat'
p = ET.Element('{http://www.w3.org/1999/xhtml}p')
p.text = "This is the htmlim message"
msg['html']['html'] = p
msg2 = self.m.Message()
values = msg.getValues()
msg2.setValues(values)
self.failUnless(msgtxt == str(msg) == str(msg2))
suite = unittest.TestLoader().loadTestsFromTestCase(testmessagestanzas)

View File

@@ -1,31 +0,0 @@
import unittest
class testpresencestanzas(unittest.TestCase):
def setUp(self):
import sleekxmpp.stanza.presence as p
self.p = p
def testPresenceShowRegression(self):
"Regression check presence['type'] = 'dnd' show value working"
p = self.p.Presence()
p['type'] = 'dnd'
self.failUnless(str(p) == "<presence><show>dnd</show></presence>")
def testPresenceUnsolicitedOffline(self):
"Unsolicted offline presence does not spawn changed_status or update roster"
p = self.p.Presence()
p['type'] = 'unavailable'
p['from'] = 'bill@chadmore.com/gmail15af'
import sleekxmpp
c = sleekxmpp.ClientXMPP('crap@wherever', 'password')
happened = []
def handlechangedpresence(event):
happened.append(True)
c.add_event_handler("changed_status", handlechangedpresence)
c._handlePresence(p)
self.failUnless(happened == [], "changed_status event triggered for superfulous unavailable presence")
self.failUnless(c.roster == {}, "Roster updated for superfulous unavailable presence")
suite = unittest.TestLoader().loadTestsFromTestCase(testpresencestanzas)

View File

@@ -1,314 +0,0 @@
import unittest
from xml.etree import cElementTree as ET
from sleekxmpp.xmlstream.matcher.stanzapath import StanzaPath
from . import xmlcompare
class testpubsubstanzas(unittest.TestCase):
def setUp(self):
import sleekxmpp.plugins.stanza_pubsub as ps
self.ps = ps
def testAffiliations(self):
"Testing iq/pubsub/affiliations/affiliation stanzas"
iq = self.ps.Iq()
aff1 = self.ps.Affiliation()
aff1['node'] = 'testnode'
aff1['affiliation'] = 'owner'
aff2 = self.ps.Affiliation()
aff2['node'] = 'testnode2'
aff2['affiliation'] = 'publisher'
iq['pubsub']['affiliations'].append(aff1)
iq['pubsub']['affiliations'].append(aff2)
xmlstring = """<iq id="0"><pubsub xmlns="http://jabber.org/protocol/pubsub"><affiliations><affiliation node="testnode" affiliation="owner" /><affiliation node="testnode2" affiliation="publisher" /></affiliations></pubsub></iq>"""
iq2 = self.ps.Iq(None, self.ps.ET.fromstring(xmlstring))
iq3 = self.ps.Iq()
values = iq2.getValues()
iq3.setValues(values)
self.failUnless(xmlstring == str(iq) == str(iq2) == str(iq3), "3 methods for creating stanza don't match")
self.failUnless(iq.match('iq@id=0/pubsub/affiliations/affiliation@node=testnode2@affiliation=publisher'), 'Match path failed')
def testSubscriptions(self):
"Testing iq/pubsub/subscriptions/subscription stanzas"
iq = self.ps.Iq()
sub1 = self.ps.Subscription()
sub1['node'] = 'testnode'
sub1['jid'] = 'steve@myserver.tld/someresource'
sub2 = self.ps.Subscription()
sub2['node'] = 'testnode2'
sub2['jid'] = 'boogers@bork.top/bill'
sub2['subscription'] = 'subscribed'
iq['pubsub']['subscriptions'].append(sub1)
iq['pubsub']['subscriptions'].append(sub2)
xmlstring = """<iq id="0"><pubsub xmlns="http://jabber.org/protocol/pubsub"><subscriptions><subscription node="testnode" jid="steve@myserver.tld/someresource" /><subscription node="testnode2" jid="boogers@bork.top/bill" subscription="subscribed" /></subscriptions></pubsub></iq>"""
iq2 = self.ps.Iq(None, self.ps.ET.fromstring(xmlstring))
iq3 = self.ps.Iq()
values = iq2.getValues()
iq3.setValues(values)
self.failUnless(xmlstring == str(iq) == str(iq2) == str(iq3))
def testOptionalSettings(self):
"Testing iq/pubsub/subscription/subscribe-options stanzas"
iq = self.ps.Iq()
iq['pubsub']['subscription']['suboptions']['required'] = True
iq['pubsub']['subscription']['node'] = 'testnode alsdkjfas'
iq['pubsub']['subscription']['jid'] = "fritzy@netflint.net/sleekxmpp"
iq['pubsub']['subscription']['subscription'] = 'unconfigured'
xmlstring = """<iq id="0"><pubsub xmlns="http://jabber.org/protocol/pubsub"><subscription node="testnode alsdkjfas" jid="fritzy@netflint.net/sleekxmpp" subscription="unconfigured"><subscribe-options><required /></subscribe-options></subscription></pubsub></iq>"""
iq2 = self.ps.Iq(None, self.ps.ET.fromstring(xmlstring))
iq3 = self.ps.Iq()
values = iq2.getValues()
iq3.setValues(values)
self.failUnless(xmlstring == str(iq) == str(iq2) == str(iq3))
def testItems(self):
"Testing iq/pubsub/items stanzas"
iq = self.ps.Iq()
iq['pubsub']['items']
payload = ET.fromstring("""<thinger xmlns="http://andyet.net/protocol/thinger" x="1" y='2'><child1 /><child2 normandy='cheese' foo='bar' /></thinger>""")
payload2 = ET.fromstring("""<thinger2 xmlns="http://andyet.net/protocol/thinger2" x="12" y='22'><child12 /><child22 normandy='cheese2' foo='bar2' /></thinger2>""")
item = self.ps.Item()
item['id'] = 'asdf'
item['payload'] = payload
item2 = self.ps.Item()
item2['id'] = 'asdf2'
item2['payload'] = payload2
iq['pubsub']['items'].append(item)
iq['pubsub']['items'].append(item2)
xmlstring = """<iq id="0"><pubsub xmlns="http://jabber.org/protocol/pubsub"><items><item id="asdf"><thinger xmlns="http://andyet.net/protocol/thinger" y="2" x="1"><child1 /><child2 foo="bar" normandy="cheese" /></thinger></item><item id="asdf2"><thinger2 xmlns="http://andyet.net/protocol/thinger2" y="22" x="12"><child12 /><child22 foo="bar2" normandy="cheese2" /></thinger2></item></items></pubsub></iq>"""
iq2 = self.ps.Iq(None, self.ps.ET.fromstring(xmlstring))
iq3 = self.ps.Iq()
values = iq2.getValues()
iq3.setValues(values)
self.failUnless(xmlstring == str(iq) == str(iq2) == str(iq3))
def testCreate(self):
"Testing iq/pubsub/create&configure stanzas"
from sleekxmpp.plugins import xep_0004
iq = self.ps.Iq()
iq['pubsub']['create']['node'] = 'mynode'
form = xep_0004.Form()
form.addField('pubsub#title', ftype='text-single', value='This thing is awesome')
iq['pubsub']['configure']['config'] = form
xmlstring = """<iq id="0"><pubsub xmlns="http://jabber.org/protocol/pubsub"><create node="mynode" /><configure><x xmlns="jabber:x:data" type="form"><field var="pubsub#title" type="text-single"><value>This thing is awesome</value></field></x></configure></pubsub></iq>"""
iq2 = self.ps.Iq(None, self.ps.ET.fromstring(xmlstring))
iq3 = self.ps.Iq()
values = iq2.getValues()
iq3.setValues(values)
self.failUnless(xmlstring == str(iq) == str(iq2) == str(iq3))
def testState(self):
"Testing iq/psstate stanzas"
from sleekxmpp.plugins import xep_0004
iq = self.ps.Iq()
iq['psstate']['node']= 'mynode'
iq['psstate']['item']= 'myitem'
pl = ET.Element('{http://andyet.net/protocol/pubsubqueue}claimed')
iq['psstate']['payload'] = pl
xmlstring = """<iq id="0"><state xmlns="http://jabber.org/protocol/psstate" node="mynode" item="myitem"><claimed xmlns="http://andyet.net/protocol/pubsubqueue" /></state></iq>"""
iq2 = self.ps.Iq(None, self.ps.ET.fromstring(xmlstring))
iq3 = self.ps.Iq()
values = iq2.getValues()
iq3.setValues(values)
self.failUnless(xmlstring == str(iq) == str(iq2) == str(iq3))
def testDefault(self):
"Testing iq/pubsub_owner/default stanzas"
from sleekxmpp.plugins import xep_0004
iq = self.ps.Iq()
iq['pubsub_owner']['default']
iq['pubsub_owner']['default']['node'] = 'mynode'
form = xep_0004.Form()
form.addField('pubsub#title', ftype='text-single', value='This thing is awesome')
iq['pubsub_owner']['default']['config'] = form
xmlstring = """<iq id="0"><pubsub xmlns="http://jabber.org/protocol/pubsub#owner"><default node="mynode"><x xmlns="jabber:x:data" type="form"><field var="pubsub#title" type="text-single"><value>This thing is awesome</value></field></x></default></pubsub></iq>"""
iq2 = self.ps.Iq(None, self.ps.ET.fromstring(xmlstring))
iq3 = self.ps.Iq()
values = iq2.getValues()
iq3.setValues(values)
self.failUnless(xmlstring == str(iq) == str(iq2) == str(iq3))
def testSubscribe(self):
"Testing iq/pubsub/subscribe stanzas"
from sleekxmpp.plugins import xep_0004
iq = self.ps.Iq()
iq['pubsub']['subscribe']['options']
iq['pubsub']['subscribe']['node'] = 'cheese'
iq['pubsub']['subscribe']['jid'] = 'fritzy@netflint.net/sleekxmpp'
iq['pubsub']['subscribe']['options']['node'] = 'cheese'
iq['pubsub']['subscribe']['options']['jid'] = 'fritzy@netflint.net/sleekxmpp'
form = xep_0004.Form()
form.addField('pubsub#title', ftype='text-single', value='This thing is awesome')
iq['pubsub']['subscribe']['options']['options'] = form
xmlstring = """<iq id="0"><pubsub xmlns="http://jabber.org/protocol/pubsub"><subscribe node="cheese" jid="fritzy@netflint.net/sleekxmpp"><options node="cheese" jid="fritzy@netflint.net/sleekxmpp"><x xmlns="jabber:x:data" type="form"><field var="pubsub#title" type="text-single"><value>This thing is awesome</value></field></x></options></subscribe></pubsub></iq>"""
iq2 = self.ps.Iq(None, self.ps.ET.fromstring(xmlstring))
iq3 = self.ps.Iq()
values = iq2.getValues()
iq3.setValues(values)
self.failUnless(xmlstring == str(iq) == str(iq2) == str(iq3))
def testPublish(self):
"Testing iq/pubsub/publish stanzas"
iq = self.ps.Iq()
iq['pubsub']['publish']['node'] = 'thingers'
payload = ET.fromstring("""<thinger xmlns="http://andyet.net/protocol/thinger" x="1" y='2'><child1 /><child2 normandy='cheese' foo='bar' /></thinger>""")
payload2 = ET.fromstring("""<thinger2 xmlns="http://andyet.net/protocol/thinger2" x="12" y='22'><child12 /><child22 normandy='cheese2' foo='bar2' /></thinger2>""")
item = self.ps.Item()
item['id'] = 'asdf'
item['payload'] = payload
item2 = self.ps.Item()
item2['id'] = 'asdf2'
item2['payload'] = payload2
iq['pubsub']['publish'].append(item)
iq['pubsub']['publish'].append(item2)
xmlstring = """<iq id="0"><pubsub xmlns="http://jabber.org/protocol/pubsub"><publish node="thingers"><item id="asdf"><thinger xmlns="http://andyet.net/protocol/thinger" y="2" x="1"><child1 /><child2 foo="bar" normandy="cheese" /></thinger></item><item id="asdf2"><thinger2 xmlns="http://andyet.net/protocol/thinger2" y="22" x="12"><child12 /><child22 foo="bar2" normandy="cheese2" /></thinger2></item></publish></pubsub></iq>"""
iq2 = self.ps.Iq(None, self.ps.ET.fromstring(xmlstring))
iq3 = self.ps.Iq()
values = iq2.getValues()
iq3.setValues(values)
self.failUnless(xmlstring == str(iq) == str(iq2) == str(iq3))
def testDelete(self):
"Testing iq/pubsub_owner/delete stanzas"
iq = self.ps.Iq()
iq['pubsub_owner']['delete']['node'] = 'thingers'
xmlstring = """<iq id="0"><pubsub xmlns="http://jabber.org/protocol/pubsub#owner"><delete node="thingers" /></pubsub></iq>"""
iq2 = self.ps.Iq(None, self.ps.ET.fromstring(xmlstring))
iq3 = self.ps.Iq()
iq3.setValues(iq2.getValues())
self.failUnless(xmlstring == str(iq) == str(iq2) == str(iq3))
def testCreateConfigGet(self):
"""Testing getting config from full create"""
xml = """<iq to="pubsub.asdf" type="set" id="E" from="fritzy@asdf/87292ede-524d-4117-9076-d934ed3db8e7"><pubsub xmlns="http://jabber.org/protocol/pubsub"><create node="testnode2" /><configure><x xmlns="jabber:x:data" type="submit"><field var="FORM_TYPE" type="hidden"><value>http://jabber.org/protocol/pubsub#node_config</value></field><field var="pubsub#node_type" type="list-single" label="Select the node type"><value>leaf</value></field><field var="pubsub#title" type="text-single" label="A friendly name for the node" /><field var="pubsub#deliver_notifications" type="boolean" label="Deliver event notifications"><value>1</value></field><field var="pubsub#deliver_payloads" type="boolean" label="Deliver payloads with event notifications"><value>1</value></field><field var="pubsub#notify_config" type="boolean" label="Notify subscribers when the node configuration changes" /><field var="pubsub#notify_delete" type="boolean" label="Notify subscribers when the node is deleted" /><field var="pubsub#notify_retract" type="boolean" label="Notify subscribers when items are removed from the node"><value>1</value></field><field var="pubsub#notify_sub" type="boolean" label="Notify owners about new subscribers and unsubscribes" /><field var="pubsub#persist_items" type="boolean" label="Persist items in storage" /><field var="pubsub#max_items" type="text-single" label="Max # of items to persist"><value>10</value></field><field var="pubsub#subscribe" type="boolean" label="Whether to allow subscriptions"><value>1</value></field><field var="pubsub#access_model" type="list-single" label="Specify the subscriber model"><value>open</value></field><field var="pubsub#publish_model" type="list-single" label="Specify the publisher model"><value>publishers</value></field><field var="pubsub#send_last_published_item" type="list-single" label="Send last published item"><value>never</value></field><field var="pubsub#presence_based_delivery" type="boolean" label="Deliver notification only to available users" /></x></configure></pubsub></iq>"""
iq = self.ps.Iq(None, self.ps.ET.fromstring(xml))
config = iq['pubsub']['configure']['config']
self.failUnless(config.getValues() != {})
def testItemEvent(self):
"""Testing message/pubsub_event/items/item"""
msg = self.ps.Message()
item = self.ps.EventItem()
pl = ET.Element('{http://netflint.net/protocol/test}test', {'failed':'3', 'passed':'24'})
item['payload'] = pl
item['id'] = 'abc123'
msg['pubsub_event']['items'].append(item)
msg['pubsub_event']['items']['node'] = 'cheese'
msg['type'] = 'normal'
xmlstring = """<message type="normal"><event xmlns="http://jabber.org/protocol/pubsub#event"><items node="cheese"><item id="abc123"><test xmlns="http://netflint.net/protocol/test" failed="3" passed="24" /></item></items></event></message>"""
msg2 = self.ps.Message(None, self.ps.ET.fromstring(xmlstring))
msg3 = self.ps.Message()
msg3.setValues(msg2.getValues())
self.failUnless(xmlstring == str(msg) == str(msg2) == str(msg3))
def testItemsEvent(self):
"""Testing multiple message/pubsub_event/items/item"""
msg = self.ps.Message()
item = self.ps.EventItem()
item2 = self.ps.EventItem()
pl = ET.Element('{http://netflint.net/protocol/test}test', {'failed':'3', 'passed':'24'})
pl2 = ET.Element('{http://netflint.net/protocol/test-other}test', {'total':'27', 'failed':'3'})
item2['payload'] = pl2
item['payload'] = pl
item['id'] = 'abc123'
item2['id'] = '123abc'
msg['pubsub_event']['items'].append(item)
msg['pubsub_event']['items'].append(item2)
msg['pubsub_event']['items']['node'] = 'cheese'
msg['type'] = 'normal'
xmlstring = """<message type="normal"><event xmlns="http://jabber.org/protocol/pubsub#event"><items node="cheese"><item id="abc123"><test xmlns="http://netflint.net/protocol/test" failed="3" passed="24" /></item><item id="123abc"><test xmlns="http://netflint.net/protocol/test-other" failed="3" total="27" /></item></items></event></message>"""
msg2 = self.ps.Message(None, self.ps.ET.fromstring(xmlstring))
msg3 = self.ps.Message()
msg3.setValues(msg2.getValues())
self.failUnless(xmlstring == str(msg) == str(msg2) == str(msg3))
def testItemsEvent(self):
"""Testing message/pubsub_event/items/item & retract mix"""
msg = self.ps.Message()
item = self.ps.EventItem()
item2 = self.ps.EventItem()
pl = ET.Element('{http://netflint.net/protocol/test}test', {'failed':'3', 'passed':'24'})
pl2 = ET.Element('{http://netflint.net/protocol/test-other}test', {'total':'27', 'failed':'3'})
item2['payload'] = pl2
retract = self.ps.EventRetract()
retract['id'] = 'aabbcc'
item['payload'] = pl
item['id'] = 'abc123'
item2['id'] = '123abc'
msg['pubsub_event']['items'].append(item)
msg['pubsub_event']['items'].append(retract)
msg['pubsub_event']['items'].append(item2)
msg['pubsub_event']['items']['node'] = 'cheese'
msg['type'] = 'normal'
xmlstring = """<message type="normal"><event xmlns="http://jabber.org/protocol/pubsub#event"><items node="cheese"><item id="abc123"><test xmlns="http://netflint.net/protocol/test" failed="3" passed="24" /></item><retract id="aabbcc" /><item id="123abc"><test xmlns="http://netflint.net/protocol/test-other" failed="3" total="27" /></item></items></event></message>"""
msg2 = self.ps.Message(None, self.ps.ET.fromstring(xmlstring))
msg3 = self.ps.Message()
msg3.setValues(msg2.getValues())
self.failUnless(xmlstring == str(msg) == str(msg2) == str(msg3))
def testCollectionAssociate(self):
"""Testing message/pubsub_event/collection/associate"""
msg = self.ps.Message()
msg['pubsub_event']['collection']['associate']['node'] = 'cheese'
msg['pubsub_event']['collection']['node'] = 'cheeseburger'
msg['type'] = 'headline'
xmlstring = """<message type="headline"><event xmlns="http://jabber.org/protocol/pubsub#event"><collection node="cheeseburger"><associate node="cheese" /></collection></event></message>"""
msg2 = self.ps.Message(None, self.ps.ET.fromstring(xmlstring))
msg3 = self.ps.Message()
msg3.setValues(msg2.getValues())
self.failUnless(xmlstring == str(msg) == str(msg2) == str(msg3))
def testCollectionDisassociate(self):
"""Testing message/pubsub_event/collection/disassociate"""
msg = self.ps.Message()
msg['pubsub_event']['collection']['disassociate']['node'] = 'cheese'
msg['pubsub_event']['collection']['node'] = 'cheeseburger'
msg['type'] = 'headline'
xmlstring = """<message type="headline"><event xmlns="http://jabber.org/protocol/pubsub#event"><collection node="cheeseburger"><disassociate node="cheese" /></collection></event></message>"""
msg2 = self.ps.Message(None, self.ps.ET.fromstring(xmlstring))
msg3 = self.ps.Message()
msg3.setValues(msg2.getValues())
self.failUnless(xmlstring == str(msg) == str(msg2) == str(msg3))
def testEventConfiguration(self):
"""Testing message/pubsub_event/configuration/config"""
msg = self.ps.Message()
from sleekxmpp.plugins import xep_0004
form = xep_0004.Form()
form.addField('pubsub#title', ftype='text-single', value='This thing is awesome')
msg['pubsub_event']['configuration']['node'] = 'cheese'
msg['pubsub_event']['configuration']['config'] = form
msg['type'] = 'headline'
xmlstring = """<message type="headline"><event xmlns="http://jabber.org/protocol/pubsub#event"><configuration node="cheese"><x xmlns="jabber:x:data" type="form"><field var="pubsub#title" type="text-single"><value>This thing is awesome</value></field></x></configuration></event></message>"""
msg2 = self.ps.Message(None, self.ps.ET.fromstring(xmlstring))
msg3 = self.ps.Message()
msg3.setValues(msg2.getValues())
self.failUnless(xmlstring == str(msg) == str(msg2) == str(msg3))
def testEventPurge(self):
"""Testing message/pubsub_event/purge"""
msg = self.ps.Message()
msg['pubsub_event']['purge']['node'] = 'pickles'
msg['type'] = 'headline'
xmlstring = """<message type="headline"><event xmlns="http://jabber.org/protocol/pubsub#event"><purge node="pickles" /></event></message>"""
msg2 = self.ps.Message(None, self.ps.ET.fromstring(xmlstring))
msg3 = self.ps.Message()
msg3.setValues(msg2.getValues())
self.failUnless(xmlstring == str(msg) == str(msg2) == str(msg3))
def testEventSubscription(self):
"""Testing message/pubsub_event/subscription"""
msg = self.ps.Message()
msg['pubsub_event']['subscription']['node'] = 'pickles'
msg['pubsub_event']['subscription']['jid'] = 'fritzy@netflint.net/test'
msg['pubsub_event']['subscription']['subid'] = 'aabb1122'
msg['pubsub_event']['subscription']['subscription'] = 'subscribed'
msg['pubsub_event']['subscription']['expiry'] = 'presence'
msg['type'] = 'headline'
xmlstring = """<message type="headline"><event xmlns="http://jabber.org/protocol/pubsub#event"><subscription node="pickles" subid="aabb1122" jid="fritzy@netflint.net/test" subscription="subscribed" expiry="presence" /></event></message>"""
msg2 = self.ps.Message(None, self.ps.ET.fromstring(xmlstring))
msg3 = self.ps.Message()
msg3.setValues(msg2.getValues())
self.failUnless(xmlcompare.comparemany([xmlstring, str(msg), str(msg2), str(msg3)]))
suite = unittest.TestLoader().loadTestsFromTestCase(testpubsubstanzas)

79
tests/test_stanza_base.py Normal file
View File

@@ -0,0 +1,79 @@
from sleekxmpp.test import *
from sleekxmpp.xmlstream.stanzabase import ET, StanzaBase
class TestStanzaBase(SleekTest):
def testTo(self):
"""Test the 'to' interface of StanzaBase."""
stanza = StanzaBase()
stanza['to'] = 'user@example.com'
self.failUnless(str(stanza['to']) == 'user@example.com',
"Setting and retrieving stanza 'to' attribute did not work.")
def testFrom(self):
"""Test the 'from' interface of StanzaBase."""
stanza = StanzaBase()
stanza['from'] = 'user@example.com'
self.failUnless(str(stanza['from']) == 'user@example.com',
"Setting and retrieving stanza 'from' attribute did not work.")
def testPayload(self):
"""Test the 'payload' interface of StanzaBase."""
stanza = StanzaBase()
self.failUnless(stanza['payload'] == [],
"Empty stanza does not have an empty payload.")
stanza['payload'] = ET.Element("{foo}foo")
self.failUnless(len(stanza['payload']) == 1,
"Stanza contents and payload do not match.")
stanza['payload'] = ET.Element('{bar}bar')
self.failUnless(len(stanza['payload']) == 2,
"Stanza payload was not appended.")
del stanza['payload']
self.failUnless(stanza['payload'] == [],
"Stanza payload not cleared after deletion.")
stanza['payload'] = [ET.Element('{foo}foo'),
ET.Element('{bar}bar')]
self.failUnless(len(stanza['payload']) == 2,
"Adding multiple elements to stanza's payload did not work.")
def testClear(self):
"""Test clearing a stanza."""
stanza = StanzaBase()
stanza['to'] = 'user@example.com'
stanza['payload'] = ET.Element("{foo}foo")
stanza.clear()
self.failUnless(stanza['payload'] == [],
"Stanza payload was not cleared after calling .clear()")
self.failUnless(str(stanza['to']) == "user@example.com",
"Stanza attributes were not preserved after calling .clear()")
def testReply(self):
"""Test creating a reply stanza."""
stanza = StanzaBase()
stanza['to'] = "recipient@example.com"
stanza['from'] = "sender@example.com"
stanza['payload'] = ET.Element("{foo}foo")
stanza.reply()
self.failUnless(str(stanza['to'] == "sender@example.com"),
"Stanza reply did not change 'to' attribute.")
self.failUnless(stanza['payload'] == [],
"Stanza reply did not empty stanza payload.")
def testError(self):
"""Test marking a stanza as an error."""
stanza = StanzaBase()
stanza['type'] = 'get'
stanza.error()
self.failUnless(stanza['type'] == 'error',
"Stanza type is not 'error' after calling error()")
suite = unittest.TestLoader().loadTestsFromTestCase(TestStanzaBase)

View File

@@ -0,0 +1,660 @@
from sleekxmpp.test import *
from sleekxmpp.xmlstream.stanzabase import ElementBase
class TestElementBase(SleekTest):
def testFixNs(self):
"""Test fixing namespaces in an XPath expression."""
e = ElementBase()
ns = "http://jabber.org/protocol/disco#items"
result = e._fix_ns("{%s}foo/bar/{abc}baz/{%s}more" % (ns, ns))
expected = "/".join(["{%s}foo" % ns,
"{%s}bar" % ns,
"{abc}baz",
"{%s}more" % ns])
self.failUnless(expected == result,
"Incorrect namespace fixing result: %s" % str(result))
def testExtendedName(self):
"""Test element names of the form tag1/tag2/tag3."""
class TestStanza(ElementBase):
name = "foo/bar/baz"
namespace = "test"
stanza = TestStanza()
self.check(stanza, """
<foo xmlns="test">
<bar>
<baz />
</bar>
</foo>
""")
def testGetStanzaValues(self):
"""Test getStanzaValues using plugins and substanzas."""
class TestStanzaPlugin(ElementBase):
name = "foo2"
namespace = "foo"
interfaces = set(('bar', 'baz'))
plugin_attrib = "foo2"
class TestSubStanza(ElementBase):
name = "subfoo"
namespace = "foo"
interfaces = set(('bar', 'baz'))
class TestStanza(ElementBase):
name = "foo"
namespace = "foo"
interfaces = set(('bar', 'baz'))
subitem = set((TestSubStanza,))
register_stanza_plugin(TestStanza, TestStanzaPlugin)
stanza = TestStanza()
stanza['bar'] = 'a'
stanza['foo2']['baz'] = 'b'
substanza = TestSubStanza()
substanza['bar'] = 'c'
stanza.append(substanza)
values = stanza.getStanzaValues()
expected = {'bar': 'a',
'baz': '',
'foo2': {'bar': '',
'baz': 'b'},
'substanzas': [{'__childtag__': '{foo}subfoo',
'bar': 'c',
'baz': ''}]}
self.failUnless(values == expected,
"Unexpected stanza values:\n%s\n%s" % (str(expected), str(values)))
def testSetStanzaValues(self):
"""Test using setStanzaValues with substanzas and plugins."""
class TestStanzaPlugin(ElementBase):
name = "pluginfoo"
namespace = "foo"
interfaces = set(('bar', 'baz'))
plugin_attrib = "plugin_foo"
class TestStanzaPlugin2(ElementBase):
name = "pluginfoo2"
namespace = "foo"
interfaces = set(('bar', 'baz'))
plugin_attrib = "plugin_foo2"
class TestSubStanza(ElementBase):
name = "subfoo"
namespace = "foo"
interfaces = set(('bar', 'baz'))
class TestStanza(ElementBase):
name = "foo"
namespace = "foo"
interfaces = set(('bar', 'baz'))
subitem = set((TestSubStanza,))
register_stanza_plugin(TestStanza, TestStanzaPlugin)
register_stanza_plugin(TestStanza, TestStanzaPlugin2)
stanza = TestStanza()
values = {'bar': 'a',
'baz': '',
'plugin_foo': {'bar': '',
'baz': 'b'},
'plugin_foo2': {'bar': 'd',
'baz': 'e'},
'substanzas': [{'__childtag__': '{foo}subfoo',
'bar': 'c',
'baz': ''}]}
stanza.setStanzaValues(values)
self.check(stanza, """
<foo xmlns="foo" bar="a">
<pluginfoo baz="b" />
<pluginfoo2 bar="d" baz="e" />
<subfoo bar="c" />
</foo>
""")
def testGetItem(self):
"""Test accessing stanza interfaces."""
class TestStanza(ElementBase):
name = "foo"
namespace = "foo"
interfaces = set(('bar', 'baz', 'qux'))
sub_interfaces = set(('baz',))
def getQux(self):
return 'qux'
class TestStanzaPlugin(ElementBase):
name = "foobar"
namespace = "foo"
plugin_attrib = "foobar"
interfaces = set(('fizz',))
TestStanza.subitem = (TestStanza,)
register_stanza_plugin(TestStanza, TestStanzaPlugin)
stanza = TestStanza()
substanza = TestStanza()
stanza.append(substanza)
stanza.setStanzaValues({'bar': 'a',
'baz': 'b',
'qux': 42,
'foobar': {'fizz': 'c'}})
# Test non-plugin interfaces
expected = {'substanzas': [substanza],
'bar': 'a',
'baz': 'b',
'qux': 'qux',
'meh': ''}
for interface, value in expected.items():
result = stanza[interface]
self.failUnless(result == value,
"Incorrect stanza interface access result: %s" % result)
# Test plugin interfaces
self.failUnless(isinstance(stanza['foobar'], TestStanzaPlugin),
"Incorrect plugin object result.")
self.failUnless(stanza['foobar']['fizz'] == 'c',
"Incorrect plugin subvalue result.")
def testSetItem(self):
"""Test assigning to stanza interfaces."""
class TestStanza(ElementBase):
name = "foo"
namespace = "foo"
interfaces = set(('bar', 'baz', 'qux'))
sub_interfaces = set(('baz',))
def setQux(self, value):
pass
class TestStanzaPlugin(ElementBase):
name = "foobar"
namespace = "foo"
plugin_attrib = "foobar"
interfaces = set(('foobar',))
register_stanza_plugin(TestStanza, TestStanzaPlugin)
stanza = TestStanza()
stanza['bar'] = 'attribute!'
stanza['baz'] = 'element!'
stanza['qux'] = 'overridden'
stanza['foobar'] = 'plugin'
self.check(stanza, """
<foo xmlns="foo" bar="attribute!">
<baz>element!</baz>
<foobar foobar="plugin" />
</foo>
""")
def testDelItem(self):
"""Test deleting stanza interface values."""
class TestStanza(ElementBase):
name = "foo"
namespace = "foo"
interfaces = set(('bar', 'baz', 'qux'))
sub_interfaces = set(('bar',))
def delQux(self):
pass
class TestStanzaPlugin(ElementBase):
name = "foobar"
namespace = "foo"
plugin_attrib = "foobar"
interfaces = set(('foobar',))
register_stanza_plugin(TestStanza, TestStanzaPlugin)
stanza = TestStanza()
stanza['bar'] = 'a'
stanza['baz'] = 'b'
stanza['qux'] = 'c'
stanza['foobar']['foobar'] = 'd'
self.check(stanza, """
<foo xmlns="foo" baz="b" qux="c">
<bar>a</bar>
<foobar foobar="d" />
</foo>
""")
del stanza['bar']
del stanza['baz']
del stanza['qux']
del stanza['foobar']
self.check(stanza, """
<foo xmlns="foo" qux="c" />
""")
def testModifyingAttributes(self):
"""Test modifying top level attributes of a stanza's XML object."""
class TestStanza(ElementBase):
name = "foo"
namespace = "foo"
interfaces = set(('bar', 'baz'))
stanza = TestStanza()
self.check(stanza, """
<foo xmlns="foo" />
""")
self.failUnless(stanza._get_attr('bar') == '',
"Incorrect value returned for an unset XML attribute.")
stanza._set_attr('bar', 'a')
stanza._set_attr('baz', 'b')
self.check(stanza, """
<foo xmlns="foo" bar="a" baz="b" />
""")
self.failUnless(stanza._get_attr('bar') == 'a',
"Retrieved XML attribute value is incorrect.")
stanza._set_attr('bar', None)
stanza._del_attr('baz')
self.check(stanza, """
<foo xmlns="foo" />
""")
self.failUnless(stanza._get_attr('bar', 'c') == 'c',
"Incorrect default value returned for an unset XML attribute.")
def testGetSubText(self):
"""Test retrieving the contents of a sub element."""
class TestStanza(ElementBase):
name = "foo"
namespace = "foo"
interfaces = set(('bar',))
def setBar(self, value):
wrapper = ET.Element("{foo}wrapper")
bar = ET.Element("{foo}bar")
bar.text = value
wrapper.append(bar)
self.xml.append(wrapper)
def getBar(self):
return self._get_sub_text("wrapper/bar", default="not found")
stanza = TestStanza()
self.failUnless(stanza['bar'] == 'not found',
"Default _get_sub_text value incorrect.")
stanza['bar'] = 'found'
self.check(stanza, """
<foo xmlns="foo">
<wrapper>
<bar>found</bar>
</wrapper>
</foo>
""")
self.failUnless(stanza['bar'] == 'found',
"_get_sub_text value incorrect: %s." % stanza['bar'])
def testSubElement(self):
"""Test setting the contents of a sub element."""
class TestStanza(ElementBase):
name = "foo"
namespace = "foo"
interfaces = set(('bar', 'baz'))
def setBaz(self, value):
self._set_sub_text("wrapper/baz", text=value)
def getBaz(self):
return self._get_sub_text("wrapper/baz")
def setBar(self, value):
self._set_sub_text("wrapper/bar", text=value)
def getBar(self):
return self._get_sub_text("wrapper/bar")
stanza = TestStanza()
stanza['bar'] = 'a'
stanza['baz'] = 'b'
self.check(stanza, """
<foo xmlns="foo">
<wrapper>
<bar>a</bar>
<baz>b</baz>
</wrapper>
</foo>
""")
stanza._set_sub_text('wrapper/bar', text='', keep=True)
self.check(stanza, """
<foo xmlns="foo">
<wrapper>
<bar />
<baz>b</baz>
</wrapper>
</foo>
""", use_values=False)
stanza['bar'] = 'a'
stanza._set_sub_text('wrapper/bar', text='')
self.check(stanza, """
<foo xmlns="foo">
<wrapper>
<baz>b</baz>
</wrapper>
</foo>
""")
def testDelSub(self):
"""Test removing sub elements."""
class TestStanza(ElementBase):
name = "foo"
namespace = "foo"
interfaces = set(('bar', 'baz'))
def setBar(self, value):
self._set_sub_text("path/to/only/bar", value);
def getBar(self):
return self._get_sub_text("path/to/only/bar")
def delBar(self):
self._del_sub("path/to/only/bar")
def setBaz(self, value):
self._set_sub_text("path/to/just/baz", value);
def getBaz(self):
return self._get_sub_text("path/to/just/baz")
def delBaz(self):
self._del_sub("path/to/just/baz")
stanza = TestStanza()
stanza['bar'] = 'a'
stanza['baz'] = 'b'
self.check(stanza, """
<foo xmlns="foo">
<path>
<to>
<only>
<bar>a</bar>
</only>
<just>
<baz>b</baz>
</just>
</to>
</path>
</foo>
""")
del stanza['bar']
del stanza['baz']
self.check(stanza, """
<foo xmlns="foo">
<path>
<to>
<only />
<just />
</to>
</path>
</foo>
""", use_values=False)
stanza['bar'] = 'a'
stanza['baz'] = 'b'
stanza._del_sub('path/to/only/bar', all=True)
self.check(stanza, """
<foo xmlns="foo">
<path>
<to>
<just>
<baz>b</baz>
</just>
</to>
</path>
</foo>
""")
def testMatch(self):
"""Test matching a stanza against an XPath expression."""
class TestSubStanza(ElementBase):
name = "sub"
namespace = "baz"
interfaces = set(('attrib',))
class TestStanza(ElementBase):
name = "foo"
namespace = "foo"
interfaces = set(('bar','baz', 'qux'))
sub_interfaces = set(('qux',))
subitem = (TestSubStanza,)
def setQux(self, value):
self._set_sub_text('qux', text=value)
def getQux(self):
return self._get_sub_text('qux')
class TestStanzaPlugin(ElementBase):
name = "plugin"
namespace = "http://test/slash/bar"
interfaces = set(('attrib',))
register_stanza_plugin(TestStanza, TestStanzaPlugin)
stanza = TestStanza()
self.failUnless(stanza.match("foo"),
"Stanza did not match its own tag name.")
self.failUnless(stanza.match("{foo}foo"),
"Stanza did not match its own namespaced name.")
stanza['bar'] = 'a'
self.failUnless(stanza.match("foo@bar=a"),
"Stanza did not match its own name with attribute value check.")
stanza['baz'] = 'b'
self.failUnless(stanza.match("foo@bar=a@baz=b"),
"Stanza did not match its own name with multiple attributes.")
stanza['qux'] = 'c'
self.failUnless(stanza.match("foo/qux"),
"Stanza did not match with subelements.")
stanza['qux'] = ''
self.failUnless(stanza.match("foo/qux") == False,
"Stanza matched missing subinterface element.")
self.failUnless(stanza.match("foo/bar") == False,
"Stanza matched nonexistent element.")
stanza['plugin']['attrib'] = 'c'
self.failUnless(stanza.match("foo/plugin@attrib=c"),
"Stanza did not match with plugin and attribute.")
self.failUnless(stanza.match("foo/{http://test/slash/bar}plugin"),
"Stanza did not match with namespaced plugin.")
substanza = TestSubStanza()
substanza['attrib'] = 'd'
stanza.append(substanza)
self.failUnless(stanza.match("foo/sub@attrib=d"),
"Stanza did not match with substanzas and attribute.")
self.failUnless(stanza.match("foo/{baz}sub"),
"Stanza did not match with namespaced substanza.")
def testComparisons(self):
"""Test comparing ElementBase objects."""
class TestStanza(ElementBase):
name = "foo"
namespace = "foo"
interfaces = set(('bar', 'baz'))
stanza1 = TestStanza()
stanza1['bar'] = 'a'
self.failUnless(stanza1,
"Stanza object does not evaluate to True")
stanza2 = TestStanza()
stanza2['baz'] = 'b'
self.failUnless(stanza1 != stanza2,
"Different stanza objects incorrectly compared equal.")
stanza1['baz'] = 'b'
stanza2['bar'] = 'a'
self.failUnless(stanza1 == stanza2,
"Equal stanzas incorrectly compared inequal.")
def testKeys(self):
"""Test extracting interface names from a stanza object."""
class TestStanza(ElementBase):
name = "foo"
namespace = "foo"
interfaces = set(('bar', 'baz'))
plugin_attrib = 'qux'
register_stanza_plugin(TestStanza, TestStanza)
stanza = TestStanza()
self.failUnless(set(stanza.keys()) == set(('bar', 'baz')),
"Returned set of interface keys does not match expected.")
stanza.enable('qux')
self.failUnless(set(stanza.keys()) == set(('bar', 'baz', 'qux')),
"Incorrect set of interface and plugin keys.")
def testGet(self):
"""Test accessing stanza interfaces using get()."""
class TestStanza(ElementBase):
name = "foo"
namespace = "foo"
interfaces = set(('bar', 'baz'))
stanza = TestStanza()
stanza['bar'] = 'a'
self.failUnless(stanza.get('bar') == 'a',
"Incorrect value returned by stanza.get")
self.failUnless(stanza.get('baz', 'b') == 'b',
"Incorrect default value returned by stanza.get")
def testSubStanzas(self):
"""Test manipulating substanzas of a stanza object."""
class TestSubStanza(ElementBase):
name = "foobar"
namespace = "foo"
interfaces = set(('qux',))
class TestStanza(ElementBase):
name = "foo"
namespace = "foo"
interfaces = set(('bar', 'baz'))
subitem = (TestSubStanza,)
stanza = TestStanza()
substanza1 = TestSubStanza()
substanza2 = TestSubStanza()
substanza1['qux'] = 'a'
substanza2['qux'] = 'b'
# Test appending substanzas
self.failUnless(len(stanza) == 0,
"Incorrect empty stanza size.")
stanza.append(substanza1)
self.check(stanza, """
<foo xmlns="foo">
<foobar qux="a" />
</foo>
""", use_values=False)
self.failUnless(len(stanza) == 1,
"Incorrect stanza size with 1 substanza.")
stanza.append(substanza2)
self.check(stanza, """
<foo xmlns="foo">
<foobar qux="a" />
<foobar qux="b" />
</foo>
""", use_values=False)
self.failUnless(len(stanza) == 2,
"Incorrect stanza size with 2 substanzas.")
# Test popping substanzas
stanza.pop(0)
self.check(stanza, """
<foo xmlns="foo">
<foobar qux="b" />
</foo>
""", use_values=False)
# Test iterating over substanzas
stanza.append(substanza1)
results = []
for substanza in stanza:
results.append(substanza['qux'])
self.failUnless(results == ['b', 'a'],
"Iteration over substanzas failed: %s." % str(results))
def testCopy(self):
"""Test copying stanza objects."""
class TestStanza(ElementBase):
name = "foo"
namespace = "foo"
interfaces = set(('bar', 'baz'))
stanza1 = TestStanza()
stanza1['bar'] = 'a'
stanza2 = stanza1.__copy__()
self.failUnless(stanza1 == stanza2,
"Copied stanzas are not equal to each other.")
stanza1['baz'] = 'b'
self.failUnless(stanza1 != stanza2,
"Divergent stanza copies incorrectly compared equal.")
suite = unittest.TestLoader().loadTestsFromTestCase(TestElementBase)

View File

@@ -0,0 +1,76 @@
from sleekxmpp.test import *
class TestErrorStanzas(SleekTest):
def testSetup(self):
"""Test setting initial values in error stanza."""
msg = self.Message()
msg.enable('error')
self.check(msg, """
<message type="error">
<error type="cancel">
<feature-not-implemented xmlns="urn:ietf:params:xml:ns:xmpp-stanzas" />
</error>
</message>
""")
def testCondition(self):
"""Test modifying the error condition."""
msg = self.Message()
msg['error']['condition'] = 'item-not-found'
self.check(msg, """
<message type="error">
<error type="cancel">
<item-not-found xmlns="urn:ietf:params:xml:ns:xmpp-stanzas" />
</error>
</message>
""")
self.failUnless(msg['error']['condition'] == 'item-not-found', "Error condition doesn't match.")
msg['error']['condition'] = 'resource-constraint'
self.check(msg, """
<message type="error">
<error type="cancel">
<resource-constraint xmlns="urn:ietf:params:xml:ns:xmpp-stanzas" />
</error>
</message>
""")
def testDelCondition(self):
"""Test that deleting error conditions doesn't remove extra elements."""
msg = self.Message()
msg['error']['text'] = 'Error!'
msg['error']['condition'] = 'internal-server-error'
del msg['error']['condition']
self.check(msg, """
<message type="error">
<error type="cancel">
<text xmlns="urn:ietf:params:xml:ns:xmpp-stanzas">Error!</text>
</error>
</message>
""", use_values=False)
def testDelText(self):
"""Test deleting the text of an error."""
msg = self.Message()
msg['error']['test'] = 'Error!'
msg['error']['condition'] = 'internal-server-error'
del msg['error']['text']
self.check(msg, """
<message type="error">
<error type="cancel">
<internal-server-error xmlns="urn:ietf:params:xml:ns:xmpp-stanzas" />
</error>
</message>
""")
suite = unittest.TestLoader().loadTestsFromTestCase(TestErrorStanzas)

View File

@@ -0,0 +1,88 @@
from sleekxmpp.test import *
import sleekxmpp.plugins.gmail_notify as gmail
class TestGmail(SleekTest):
def setUp(self):
register_stanza_plugin(Iq, gmail.GmailQuery)
register_stanza_plugin(Iq, gmail.MailBox)
register_stanza_plugin(Iq, gmail.NewMail)
def testCreateQuery(self):
"""Testing querying Gmail for emails."""
iq = self.Iq()
iq['type'] = 'get'
iq['gmail']['search'] = 'is:starred'
iq['gmail']['newer-than-time'] = '1140638252542'
iq['gmail']['newer-than-tid'] = '11134623426430234'
self.check(iq, """
<iq type="get">
<query xmlns="google:mail:notify"
newer-than-time="1140638252542"
newer-than-tid="11134623426430234"
q="is:starred" />
</iq>
""", use_values=False)
def testMailBox(self):
"""Testing reading from Gmail mailbox result"""
# Use the example from Google's documentation at
# http://code.google.com/apis/talk/jep_extensions/gmail.html#notifications
xml = ET.fromstring("""
<iq type="result">
<mailbox xmlns="google:mail:notify"
result-time='1118012394209'
url='http://mail.google.com/mail'
total-matched='95'
total-estimate='0'>
<mail-thread-info tid='1172320964060972012'
participation='1'
messages='28'
date='1118012394209'
url='http://mail.google.com/mail?view=cv'>
<senders>
<sender name='Me' address='romeo@gmail.com' originator='1' />
<sender name='Benvolio' address='benvolio@gmail.com' />
<sender name='Mercutio' address='mercutio@gmail.com' unread='1'/>
</senders>
<labels>act1scene3</labels>
<subject>Put thy rapier up.</subject>
<snippet>Ay, ay, a scratch, a scratch; marry, 'tis enough.</snippet>
</mail-thread-info>
</mailbox>
</iq>
""")
iq = self.Iq(xml=xml)
mailbox = iq['mailbox']
self.failUnless(mailbox['result-time'] == '1118012394209', "result-time doesn't match")
self.failUnless(mailbox['url'] == 'http://mail.google.com/mail', "url doesn't match")
self.failUnless(mailbox['matched'] == '95', "total-matched incorrect")
self.failUnless(mailbox['estimate'] == False, "total-estimate incorrect")
self.failUnless(len(mailbox['threads']) == 1, "could not extract message threads")
thread = mailbox['threads'][0]
self.failUnless(thread['tid'] == '1172320964060972012', "thread tid doesn't match")
self.failUnless(thread['participation'] == '1', "thread participation incorrect")
self.failUnless(thread['messages'] == '28', "thread message count incorrect")
self.failUnless(thread['date'] == '1118012394209', "thread date doesn't match")
self.failUnless(thread['url'] == 'http://mail.google.com/mail?view=cv', "thread url doesn't match")
self.failUnless(thread['labels'] == 'act1scene3', "thread labels incorrect")
self.failUnless(thread['subject'] == 'Put thy rapier up.', "thread subject doesn't match")
self.failUnless(thread['snippet'] == "Ay, ay, a scratch, a scratch; marry, 'tis enough.", "snippet doesn't match")
self.failUnless(len(thread['senders']) == 3, "could not extract senders")
sender1 = thread['senders'][0]
self.failUnless(sender1['name'] == 'Me', "sender name doesn't match")
self.failUnless(sender1['address'] == 'romeo@gmail.com', "sender address doesn't match")
self.failUnless(sender1['originator'] == True, "sender originator incorrect")
self.failUnless(sender1['unread'] == False, "sender unread incorrectly True")
sender2 = thread['senders'][2]
self.failUnless(sender2['unread'] == True, "sender unread incorrectly False")
suite = unittest.TestLoader().loadTestsFromTestCase(TestGmail)

90
tests/test_stanza_iq.py Normal file
View File

@@ -0,0 +1,90 @@
from sleekxmpp.test import *
from sleekxmpp.xmlstream.stanzabase import ET
class TestIqStanzas(SleekTest):
def tearDown(self):
"""Shutdown the XML stream after testing."""
self.stream_close()
def testSetup(self):
"""Test initializing default Iq values."""
iq = self.Iq()
self.check(iq, """
<iq id="0" />
""")
def testPayload(self):
"""Test setting Iq stanza payload."""
iq = self.Iq()
iq.setPayload(ET.Element('{test}tester'))
self.check(iq, """
<iq id="0">
<tester xmlns="test" />
</iq>
""", use_values=False)
def testUnhandled(self):
"""Test behavior for Iq.unhandled."""
self.stream_start()
self.recv("""
<iq id="test" type="get">
<query xmlns="test" />
</iq>
""")
iq = self.Iq()
iq['id'] = 'test'
iq['error']['condition'] = 'feature-not-implemented'
iq['error']['text'] = 'No handlers registered for this request.'
self.send(iq, """
<iq id="test" type="error">
<error type="cancel">
<feature-not-implemented xmlns="urn:ietf:params:xml:ns:xmpp-stanzas" />
<text xmlns="urn:ietf:params:xml:ns:xmpp-stanzas">
No handlers registered for this request.
</text>
</error>
</iq>
""")
def testQuery(self):
"""Test modifying query element of Iq stanzas."""
iq = self.Iq()
iq['query'] = 'query_ns'
self.check(iq, """
<iq id="0">
<query xmlns="query_ns" />
</iq>
""")
iq['query'] = 'query_ns2'
self.check(iq, """
<iq id="0">
<query xmlns="query_ns2" />
</iq>
""")
self.failUnless(iq['query'] == 'query_ns2', "Query namespace doesn't match")
del iq['query']
self.check(iq, """
<iq id="0" />
""")
def testReply(self):
"""Test setting proper result type in Iq replies."""
iq = self.Iq()
iq['to'] = 'user@localhost'
iq['type'] = 'get'
iq.reply()
self.check(iq, """
<iq id="0" type="result" />
""")
suite = unittest.TestLoader().loadTestsFromTestCase(TestIqStanzas)

View File

@@ -0,0 +1,57 @@
from sleekxmpp.test import *
from sleekxmpp.stanza.message import Message
from sleekxmpp.stanza.htmlim import HTMLIM
class TestMessageStanzas(SleekTest):
def setUp(self):
register_stanza_plugin(Message, HTMLIM)
def testGroupchatReplyRegression(self):
"Regression groupchat reply should be to barejid"
msg = self.Message()
msg['to'] = 'me@myserver.tld'
msg['from'] = 'room@someservice.someserver.tld/somenick'
msg['type'] = 'groupchat'
msg['body'] = "this is a message"
msg.reply()
self.failUnless(str(msg['to']) == 'room@someservice.someserver.tld')
def testAttribProperty(self):
"Test attrib property returning self"
msg = self.Message()
msg.attrib.attrib.attrib['to'] = 'usr@server.tld'
self.failUnless(str(msg['to']) == 'usr@server.tld')
def testHTMLPlugin(self):
"Test message/html/body stanza"
msg = self.Message()
msg['to'] = "fritzy@netflint.net/sleekxmpp"
msg['body'] = "this is the plaintext message"
msg['type'] = 'chat'
p = ET.Element('{http://www.w3.org/1999/xhtml}p')
p.text = "This is the htmlim message"
msg['html']['body'] = p
self.check(msg, """
<message to="fritzy@netflint.net/sleekxmpp" type="chat">
<body>this is the plaintext message</body>
<html xmlns="http://jabber.org/protocol/xhtml-im">
<body xmlns="http://www.w3.org/1999/xhtml">
<p>This is the htmlim message</p>
</body>
</html>
</message>""")
def testNickPlugin(self):
"Test message/nick/nick stanza."
msg = self.Message()
msg['nick']['nick'] = 'A nickname!'
self.check(msg, """
<message>
<nick xmlns="http://jabber.org/nick/nick">A nickname!</nick>
</message>
""")
suite = unittest.TestLoader().loadTestsFromTestCase(TestMessageStanzas)

View File

@@ -0,0 +1,66 @@
from sleekxmpp.test import *
from sleekxmpp.stanza.presence import Presence
class TestPresenceStanzas(SleekTest):
def testPresenceShowRegression(self):
"""Regression check presence['type'] = 'dnd' show value working"""
p = self.Presence()
p['type'] = 'dnd'
self.check(p, "<presence><show>dnd</show></presence>")
def testPresenceType(self):
"""Test manipulating presence['type']"""
p = self.Presence()
p['type'] = 'available'
self.check(p, "<presence />")
self.failUnless(p['type'] == 'available',
"Incorrect presence['type'] for type 'available': %s" % p['type'])
for showtype in ['away', 'chat', 'dnd', 'xa']:
p['type'] = showtype
self.check(p, """
<presence><show>%s</show></presence>
""" % showtype)
self.failUnless(p['type'] == showtype,
"Incorrect presence['type'] for type '%s'" % showtype)
p['type'] = None
self.check(p, "<presence />")
def testPresenceUnsolicitedOffline(self):
"""
Unsolicted offline presence does not spawn changed_status
or update the roster.
"""
p = self.Presence()
p['type'] = 'unavailable'
p['from'] = 'bill@chadmore.com/gmail15af'
c = sleekxmpp.ClientXMPP('crap@wherever', 'password')
happened = []
def handlechangedpresence(event):
happened.append(True)
c.add_event_handler("changed_status", handlechangedpresence)
c._handle_presence(p)
self.failUnless(happened == [],
"changed_status event triggered for extra unavailable presence")
self.failUnless(c.roster == {},
"Roster updated for superfulous unavailable presence")
def testNickPlugin(self):
"""Test presence/nick/nick stanza."""
p = self.Presence()
p['nick']['nick'] = 'A nickname!'
self.check(p, """
<presence>
<nick xmlns="http://jabber.org/nick/nick">A nickname!</nick>
</presence>
""")
suite = unittest.TestLoader().loadTestsFromTestCase(TestPresenceStanzas)

View File

@@ -0,0 +1,84 @@
from sleekxmpp.test import *
from sleekxmpp.stanza.roster import Roster
class TestRosterStanzas(SleekTest):
def testAddItems(self):
"""Test adding items to a roster stanza."""
iq = self.Iq()
iq['roster'].setItems({
'user@example.com': {
'name': 'User',
'subscription': 'both',
'groups': ['Friends', 'Coworkers']},
'otheruser@example.com': {
'name': 'Other User',
'subscription': 'both',
'groups': []}})
self.check(iq, """
<iq>
<query xmlns="jabber:iq:roster">
<item jid="user@example.com" name="User" subscription="both">
<group>Friends</group>
<group>Coworkers</group>
</item>
<item jid="otheruser@example.com" name="Other User"
subscription="both" />
</query>
</iq>
""")
def testGetItems(self):
"""Test retrieving items from a roster stanza."""
xml_string = """
<iq>
<query xmlns="jabber:iq:roster">
<item jid="user@example.com" name="User" subscription="both">
<group>Friends</group>
<group>Coworkers</group>
</item>
<item jid="otheruser@example.com" name="Other User"
subscription="both" />
</query>
</iq>
"""
iq = self.Iq(ET.fromstring(xml_string))
expected = {
'user@example.com': {
'name': 'User',
'subscription': 'both',
'groups': ['Friends', 'Coworkers']},
'otheruser@example.com': {
'name': 'Other User',
'subscription': 'both',
'groups': []}}
debug = "Roster items don't match after retrieval."
debug += "\nReturned: %s" % str(iq['roster']['items'])
debug += "\nExpected: %s" % str(expected)
self.failUnless(iq['roster']['items'] == expected, debug)
def testDelItems(self):
"""Test clearing items from a roster stanza."""
xml_string = """
<iq>
<query xmlns="jabber:iq:roster">
<item jid="user@example.com" name="User" subscription="both">
<group>Friends</group>
<group>Coworkers</group>
</item>
<item jid="otheruser@example.com" name="Other User"
subscription="both" />
</query>
</iq>
"""
iq = self.Iq(ET.fromstring(xml_string))
del iq['roster']['items']
self.check(iq, """
<iq>
<query xmlns="jabber:iq:roster" />
</iq>
""")
suite = unittest.TestLoader().loadTestsFromTestCase(TestRosterStanzas)

View File

@@ -0,0 +1,115 @@
from sleekxmpp.test import *
import sleekxmpp.plugins.xep_0004 as xep_0004
class TestDataForms(SleekTest):
def setUp(self):
register_stanza_plugin(Message, xep_0004.Form)
register_stanza_plugin(xep_0004.Form, xep_0004.FormField)
register_stanza_plugin(xep_0004.FormField, xep_0004.FieldOption)
def testMultipleInstructions(self):
"""Testing using multiple instructions elements in a data form."""
msg = self.Message()
msg['form']['instructions'] = "Instructions\nSecond batch"
self.check(msg, """
<message>
<x xmlns="jabber:x:data" type="form">
<instructions>Instructions</instructions>
<instructions>Second batch</instructions>
</x>
</message>
""")
def testAddField(self):
"""Testing adding fields to a data form."""
msg = self.Message()
form = msg['form']
form.addField(var='f1',
ftype='text-single',
label='Text',
desc='A text field',
required=True,
value='Some text!')
self.check(msg, """
<message>
<x xmlns="jabber:x:data" type="form">
<field var="f1" type="text-single" label="Text">
<desc>A text field</desc>
<required />
<value>Some text!</value>
</field>
</x>
</message>
""")
form['fields'] = [('f1', {'type': 'text-single',
'label': 'Username',
'required': True}),
('f2', {'type': 'text-private',
'label': 'Password',
'required': True}),
('f3', {'type': 'text-multi',
'label': 'Message',
'value': 'Enter message.\nA long one even.'}),
('f4', {'type': 'list-single',
'label': 'Message Type',
'options': [{'label': 'Cool!',
'value': 'cool'},
{'label': 'Urgh!',
'value': 'urgh'}]})]
self.check(msg, """
<message>
<x xmlns="jabber:x:data" type="form">
<field var="f1" type="text-single" label="Username">
<required />
</field>
<field var="f2" type="text-private" label="Password">
<required />
</field>
<field var="f3" type="text-multi" label="Message">
<value>Enter message.</value>
<value>A long one even.</value>
</field>
<field var="f4" type="list-single" label="Message Type">
<option label="Cool!">
<value>cool</value>
</option>
<option label="Urgh!">
<value>urgh</value>
</option>
</field>
</x>
</message>
""")
def testSetValues(self):
"""Testing setting form values"""
msg = self.Message()
form = msg['form']
form.setFields([
('foo', {'type': 'text-single'}),
('bar', {'type': 'list-multi'})])
form.setValues({'foo': 'Foo!',
'bar': ['a', 'b']})
self.check(msg, """
<message>
<x xmlns="jabber:x:data" type="form">
<field var="foo" type="text-single">
<value>Foo!</value>
</field>
<field var="bar" type="list-multi">
<value>a</value>
<value>b</value>
</field>
</x>
</message>""")
suite = unittest.TestLoader().loadTestsFromTestCase(TestDataForms)

View File

@@ -0,0 +1,176 @@
from sleekxmpp.test import *
import sleekxmpp.plugins.xep_0030 as xep_0030
class TestDisco(SleekTest):
def setUp(self):
register_stanza_plugin(Iq, xep_0030.DiscoInfo)
register_stanza_plugin(Iq, xep_0030.DiscoItems)
def testCreateInfoQueryNoNode(self):
"""Testing disco#info query with no node."""
iq = self.Iq()
iq['id'] = "0"
iq['disco_info']['node'] = ''
self.check(iq, """
<iq id="0">
<query xmlns="http://jabber.org/protocol/disco#info" />
</iq>
""")
def testCreateInfoQueryWithNode(self):
"""Testing disco#info query with a node."""
iq = self.Iq()
iq['id'] = "0"
iq['disco_info']['node'] = 'foo'
self.check(iq, """
<iq id="0">
<query xmlns="http://jabber.org/protocol/disco#info" node="foo" />
</iq>
""")
def testCreateInfoQueryNoNode(self):
"""Testing disco#items query with no node."""
iq = self.Iq()
iq['id'] = "0"
iq['disco_items']['node'] = ''
self.check(iq, """
<iq id="0">
<query xmlns="http://jabber.org/protocol/disco#items" />
</iq>
""")
def testCreateItemsQueryWithNode(self):
"""Testing disco#items query with a node."""
iq = self.Iq()
iq['id'] = "0"
iq['disco_items']['node'] = 'foo'
self.check(iq, """
<iq id="0">
<query xmlns="http://jabber.org/protocol/disco#items" node="foo" />
</iq>
""")
def testInfoIdentities(self):
"""Testing adding identities to disco#info."""
iq = self.Iq()
iq['id'] = "0"
iq['disco_info']['node'] = 'foo'
iq['disco_info'].addIdentity('conference', 'text', 'Chatroom')
self.check(iq, """
<iq id="0">
<query xmlns="http://jabber.org/protocol/disco#info" node="foo">
<identity category="conference" type="text" name="Chatroom" />
</query>
</iq>
""")
def testInfoFeatures(self):
"""Testing adding features to disco#info."""
iq = self.Iq()
iq['id'] = "0"
iq['disco_info']['node'] = 'foo'
iq['disco_info'].addFeature('foo')
iq['disco_info'].addFeature('bar')
self.check(iq, """
<iq id="0">
<query xmlns="http://jabber.org/protocol/disco#info" node="foo">
<feature var="foo" />
<feature var="bar" />
</query>
</iq>
""")
def testItems(self):
"""Testing adding features to disco#info."""
iq = self.Iq()
iq['id'] = "0"
iq['disco_items']['node'] = 'foo'
iq['disco_items'].addItem('user@localhost')
iq['disco_items'].addItem('user@localhost', 'foo')
iq['disco_items'].addItem('user@localhost', 'bar', 'Testing')
self.check(iq, """
<iq id="0">
<query xmlns="http://jabber.org/protocol/disco#items" node="foo">
<item jid="user@localhost" />
<item node="foo" jid="user@localhost" />
<item node="bar" jid="user@localhost" name="Testing" />
</query>
</iq>
""")
def testAddRemoveIdentities(self):
"""Test adding and removing identities to disco#info stanza"""
ids = [('automation', 'commands', 'AdHoc'),
('conference', 'text', 'ChatRoom')]
info = xep_0030.DiscoInfo()
info.addIdentity(*ids[0])
self.failUnless(info.getIdentities() == [ids[0]])
info.delIdentity('automation', 'commands')
self.failUnless(info.getIdentities() == [])
info.setIdentities(ids)
self.failUnless(info.getIdentities() == ids)
info.delIdentity('automation', 'commands')
self.failUnless(info.getIdentities() == [ids[1]])
info.delIdentities()
self.failUnless(info.getIdentities() == [])
def testAddRemoveFeatures(self):
"""Test adding and removing features to disco#info stanza"""
features = ['foo', 'bar', 'baz']
info = xep_0030.DiscoInfo()
info.addFeature(features[0])
self.failUnless(info.getFeatures() == [features[0]])
info.delFeature('foo')
self.failUnless(info.getFeatures() == [])
info.setFeatures(features)
self.failUnless(info.getFeatures() == features)
info.delFeature('bar')
self.failUnless(info.getFeatures() == ['foo', 'baz'])
info.delFeatures()
self.failUnless(info.getFeatures() == [])
def testAddRemoveItems(self):
"""Test adding and removing items to disco#items stanza"""
items = [('user@localhost', None, None),
('user@localhost', 'foo', None),
('user@localhost', 'bar', 'Test')]
info = xep_0030.DiscoItems()
self.failUnless(True, ""+str(items[0]))
info.addItem(*(items[0]))
self.failUnless(info.getItems() == [items[0]], info.getItems())
info.delItem('user@localhost')
self.failUnless(info.getItems() == [])
info.setItems(items)
self.failUnless(info.getItems() == items)
info.delItem('user@localhost', 'foo')
self.failUnless(info.getItems() == [items[0], items[2]])
info.delItems()
self.failUnless(info.getItems() == [])
suite = unittest.TestLoader().loadTestsFromTestCase(TestDisco)

View File

@@ -0,0 +1,111 @@
from sleekxmpp.test import *
import sleekxmpp.plugins.xep_0033 as xep_0033
class TestAddresses(SleekTest):
def setUp(self):
register_stanza_plugin(Message, xep_0033.Addresses)
def testAddAddress(self):
"""Testing adding extended stanza address."""
msg = self.Message()
msg['addresses'].addAddress(atype='to', jid='to@header1.org')
self.check(msg, """
<message>
<addresses xmlns="http://jabber.org/protocol/address">
<address jid="to@header1.org" type="to" />
</addresses>
</message>
""")
msg = self.Message()
msg['addresses'].addAddress(atype='replyto',
jid='replyto@header1.org',
desc='Reply address')
self.check(msg, """
<message>
<addresses xmlns="http://jabber.org/protocol/address">
<address jid="replyto@header1.org" type="replyto" desc="Reply address" />
</addresses>
</message>
""")
def testAddAddresses(self):
"""Testing adding multiple extended stanza addresses."""
xmlstring = """
<message>
<addresses xmlns="http://jabber.org/protocol/address">
<address jid="replyto@header1.org" type="replyto" desc="Reply address" />
<address jid="cc@header2.org" type="cc" />
<address jid="bcc@header2.org" type="bcc" />
</addresses>
</message>
"""
msg = self.Message()
msg['addresses'].setAddresses([
{'type':'replyto',
'jid':'replyto@header1.org',
'desc':'Reply address'},
{'type':'cc',
'jid':'cc@header2.org'},
{'type':'bcc',
'jid':'bcc@header2.org'}])
self.check(msg, xmlstring)
msg = self.Message()
msg['addresses']['replyto'] = [{'jid':'replyto@header1.org',
'desc':'Reply address'}]
msg['addresses']['cc'] = [{'jid':'cc@header2.org'}]
msg['addresses']['bcc'] = [{'jid':'bcc@header2.org'}]
self.check(msg, xmlstring)
def testAddURI(self):
"""Testing adding URI attribute to extended stanza address."""
msg = self.Message()
addr = msg['addresses'].addAddress(atype='to',
jid='to@header1.org',
node='foo')
self.check(msg, """
<message>
<addresses xmlns="http://jabber.org/protocol/address">
<address node="foo" jid="to@header1.org" type="to" />
</addresses>
</message>
""")
addr['uri'] = 'mailto:to@header2.org'
self.check(msg, """
<message>
<addresses xmlns="http://jabber.org/protocol/address">
<address type="to" uri="mailto:to@header2.org" />
</addresses>
</message>
""")
def testDelivered(self):
"""Testing delivered attribute of extended stanza addresses."""
xmlstring = """
<message>
<addresses xmlns="http://jabber.org/protocol/address">
<address %s jid="to@header1.org" type="to" />
</addresses>
</message>
"""
msg = self.Message()
addr = msg['addresses'].addAddress(jid='to@header1.org', atype='to')
self.check(msg, xmlstring % '')
addr['delivered'] = True
self.check(msg, xmlstring % 'delivered="true"')
addr['delivered'] = False
self.check(msg, xmlstring % '')
suite = unittest.TestLoader().loadTestsFromTestCase(TestAddresses)

View File

@@ -0,0 +1,511 @@
from sleekxmpp.test import *
import sleekxmpp.plugins.xep_0004 as xep_0004
import sleekxmpp.plugins.stanza_pubsub as pubsub
class TestPubsubStanzas(SleekTest):
def testAffiliations(self):
"Testing iq/pubsub/affiliations/affiliation stanzas"
iq = self.Iq()
aff1 = pubsub.Affiliation()
aff1['node'] = 'testnode'
aff1['affiliation'] = 'owner'
aff2 = pubsub.Affiliation()
aff2['node'] = 'testnode2'
aff2['affiliation'] = 'publisher'
iq['pubsub']['affiliations'].append(aff1)
iq['pubsub']['affiliations'].append(aff2)
self.check(iq, """
<iq id="0">
<pubsub xmlns="http://jabber.org/protocol/pubsub">
<affiliations>
<affiliation node="testnode" affiliation="owner" />
<affiliation node="testnode2" affiliation="publisher" />
</affiliations>
</pubsub>
</iq>""")
def testSubscriptions(self):
"Testing iq/pubsub/subscriptions/subscription stanzas"
iq = self.Iq()
sub1 = pubsub.Subscription()
sub1['node'] = 'testnode'
sub1['jid'] = 'steve@myserver.tld/someresource'
sub2 = pubsub.Subscription()
sub2['node'] = 'testnode2'
sub2['jid'] = 'boogers@bork.top/bill'
sub2['subscription'] = 'subscribed'
iq['pubsub']['subscriptions'].append(sub1)
iq['pubsub']['subscriptions'].append(sub2)
self.check(iq, """
<iq id="0">
<pubsub xmlns="http://jabber.org/protocol/pubsub">
<subscriptions>
<subscription node="testnode" jid="steve@myserver.tld/someresource" />
<subscription node="testnode2" jid="boogers@bork.top/bill" subscription="subscribed" />
</subscriptions>
</pubsub>
</iq>""")
def testOptionalSettings(self):
"Testing iq/pubsub/subscription/subscribe-options stanzas"
iq = self.Iq()
iq['pubsub']['subscription']['suboptions']['required'] = True
iq['pubsub']['subscription']['node'] = 'testnode alsdkjfas'
iq['pubsub']['subscription']['jid'] = "fritzy@netflint.net/sleekxmpp"
iq['pubsub']['subscription']['subscription'] = 'unconfigured'
self.check(iq, """
<iq id="0">
<pubsub xmlns="http://jabber.org/protocol/pubsub">
<subscription node="testnode alsdkjfas" jid="fritzy@netflint.net/sleekxmpp" subscription="unconfigured">
<subscribe-options>
<required />
</subscribe-options>
</subscription>
</pubsub>
</iq>""")
def testItems(self):
"Testing iq/pubsub/items stanzas"
iq = self.Iq()
iq['pubsub']['items']['node'] = 'crap'
payload = ET.fromstring("""
<thinger xmlns="http://andyet.net/protocol/thinger" x="1" y='2'>
<child1 />
<child2 normandy='cheese' foo='bar' />
</thinger>""")
payload2 = ET.fromstring("""
<thinger2 xmlns="http://andyet.net/protocol/thinger2" x="12" y='22'>
<child12 />
<child22 normandy='cheese2' foo='bar2' />
</thinger2>""")
item = pubsub.Item()
item['id'] = 'asdf'
item['payload'] = payload
item2 = pubsub.Item()
item2['id'] = 'asdf2'
item2['payload'] = payload2
iq['pubsub']['items'].append(item)
iq['pubsub']['items'].append(item2)
self.check(iq, """
<iq id="0">
<pubsub xmlns="http://jabber.org/protocol/pubsub">
<items node="crap">
<item id="asdf">
<thinger xmlns="http://andyet.net/protocol/thinger" y="2" x="1">
<child1 />
<child2 foo="bar" normandy="cheese" />
</thinger>
</item>
<item id="asdf2">
<thinger2 xmlns="http://andyet.net/protocol/thinger2" y="22" x="12">
<child12 />
<child22 foo="bar2" normandy="cheese2" />
</thinger2>
</item>
</items>
</pubsub>
</iq>""")
def testCreate(self):
"Testing iq/pubsub/create&configure stanzas"
iq = self.Iq()
iq['pubsub']['create']['node'] = 'mynode'
iq['pubsub']['configure']['form'].addField('pubsub#title',
ftype='text-single',
value='This thing is awesome')
self.check(iq, """
<iq id="0">
<pubsub xmlns="http://jabber.org/protocol/pubsub">
<create node="mynode" />
<configure>
<x xmlns="jabber:x:data" type="form">
<field var="pubsub#title" type="text-single">
<value>This thing is awesome</value>
</field>
</x>
</configure>
</pubsub>
</iq>""")
def testState(self):
"Testing iq/psstate stanzas"
iq = self.Iq()
iq['psstate']['node']= 'mynode'
iq['psstate']['item']= 'myitem'
pl = ET.Element('{http://andyet.net/protocol/pubsubqueue}claimed')
iq['psstate']['payload'] = pl
self.check(iq, """
<iq id="0">
<state xmlns="http://jabber.org/protocol/psstate" node="mynode" item="myitem">
<claimed xmlns="http://andyet.net/protocol/pubsubqueue" />
</state>
</iq>""")
def testDefault(self):
"Testing iq/pubsub_owner/default stanzas"
iq = self.Iq()
iq['pubsub_owner']['default']
iq['pubsub_owner']['default']['node'] = 'mynode'
iq['pubsub_owner']['default']['type'] = 'leaf'
iq['pubsub_owner']['default']['form'].addField('pubsub#title',
ftype='text-single',
value='This thing is awesome')
self.check(iq, """
<iq id="0">
<pubsub xmlns="http://jabber.org/protocol/pubsub#owner">
<default node="mynode" type="leaf">
<x xmlns="jabber:x:data" type="form">
<field var="pubsub#title" type="text-single">
<value>This thing is awesome</value>
</field>
</x>
</default>
</pubsub>
</iq>""", use_values=False)
def testSubscribe(self):
"testing iq/pubsub/subscribe stanzas"
iq = self.Iq()
iq['pubsub']['subscribe']['options']
iq['pubsub']['subscribe']['node'] = 'cheese'
iq['pubsub']['subscribe']['jid'] = 'fritzy@netflint.net/sleekxmpp'
iq['pubsub']['subscribe']['options']['node'] = 'cheese'
iq['pubsub']['subscribe']['options']['jid'] = 'fritzy@netflint.net/sleekxmpp'
form = xep_0004.Form()
form.addField('pubsub#title', ftype='text-single', value='this thing is awesome')
iq['pubsub']['subscribe']['options']['options'] = form
self.check(iq, """
<iq id="0">
<pubsub xmlns="http://jabber.org/protocol/pubsub">
<subscribe node="cheese" jid="fritzy@netflint.net/sleekxmpp">
<options node="cheese" jid="fritzy@netflint.net/sleekxmpp">
<x xmlns="jabber:x:data" type="form">
<field var="pubsub#title" type="text-single">
<value>this thing is awesome</value>
</field>
</x>
</options>
</subscribe>
</pubsub>
</iq>""", use_values=False)
def testPublish(self):
"Testing iq/pubsub/publish stanzas"
iq = self.Iq()
iq['pubsub']['publish']['node'] = 'thingers'
payload = ET.fromstring("""
<thinger xmlns="http://andyet.net/protocol/thinger" x="1" y='2'>
<child1 />
<child2 normandy='cheese' foo='bar' />
</thinger>""")
payload2 = ET.fromstring("""
<thinger2 xmlns="http://andyet.net/protocol/thinger2" x="12" y='22'>
<child12 />
<child22 normandy='cheese2' foo='bar2' />
</thinger2>""")
item = pubsub.Item()
item['id'] = 'asdf'
item['payload'] = payload
item2 = pubsub.Item()
item2['id'] = 'asdf2'
item2['payload'] = payload2
iq['pubsub']['publish'].append(item)
iq['pubsub']['publish'].append(item2)
self.check(iq, """
<iq id="0">
<pubsub xmlns="http://jabber.org/protocol/pubsub">
<publish node="thingers">
<item id="asdf">
<thinger xmlns="http://andyet.net/protocol/thinger" y="2" x="1">
<child1 />
<child2 foo="bar" normandy="cheese" />
</thinger>
</item>
<item id="asdf2">
<thinger2 xmlns="http://andyet.net/protocol/thinger2" y="22" x="12">
<child12 />
<child22 foo="bar2" normandy="cheese2" />
</thinger2>
</item>
</publish>
</pubsub>
</iq>""")
def testDelete(self):
"Testing iq/pubsub_owner/delete stanzas"
iq = self.Iq()
iq['pubsub_owner']['delete']['node'] = 'thingers'
self.check(iq, """
<iq id="0">
<pubsub xmlns="http://jabber.org/protocol/pubsub#owner">
<delete node="thingers" />
</pubsub>
</iq>""")
def testCreateConfigGet(self):
"""Testing getting config from full create"""
iq = self.Iq()
iq['to'] = 'pubsub.asdf'
iq['from'] = 'fritzy@asdf/87292ede-524d-4117-9076-d934ed3db8e7'
iq['type'] = 'set'
iq['id'] = 'E'
pub = iq['pubsub']
pub['create']['node'] = 'testnode2'
pub['configure']['form']['type'] = 'submit'
pub['configure']['form'].setFields([
('FORM_TYPE', {'type': 'hidden',
'value': 'http://jabber.org/protocol/pubsub#node_config'}),
('pubsub#node_type', {'type': 'list-single',
'label': 'Select the node type',
'value': 'leaf'}),
('pubsub#title', {'type': 'text-single',
'label': 'A friendly name for the node'}),
('pubsub#deliver_notifications', {'type': 'boolean',
'label': 'Deliver event notifications',
'value': True}),
('pubsub#deliver_payloads', {'type': 'boolean',
'label': 'Deliver payloads with event notifications',
'value': True}),
('pubsub#notify_config', {'type': 'boolean',
'label': 'Notify subscribers when the node configuration changes'}),
('pubsub#notify_delete', {'type': 'boolean',
'label': 'Notify subscribers when the node is deleted'}),
('pubsub#notify_retract', {'type': 'boolean',
'label': 'Notify subscribers when items are removed from the node',
'value': True}),
('pubsub#notify_sub', {'type': 'boolean',
'label': 'Notify owners about new subscribers and unsubscribes'}),
('pubsub#persist_items', {'type': 'boolean',
'label': 'Persist items in storage'}),
('pubsub#max_items', {'type': 'text-single',
'label': 'Max # of items to persist',
'value': '10'}),
('pubsub#subscribe', {'type': 'boolean',
'label': 'Whether to allow subscriptions',
'value': True}),
('pubsub#access_model', {'type': 'list-single',
'label': 'Specify the subscriber model',
'value': 'open'}),
('pubsub#publish_model', {'type': 'list-single',
'label': 'Specify the publisher model',
'value': 'publishers'}),
('pubsub#send_last_published_item', {'type': 'list-single',
'label': 'Send last published item',
'value': 'never'}),
('pubsub#presence_based_delivery', {'type': 'boolean',
'label': 'Deliver notification only to available users'}),
])
self.check(iq, """
<iq to="pubsub.asdf" type="set" id="E" from="fritzy@asdf/87292ede-524d-4117-9076-d934ed3db8e7">
<pubsub xmlns="http://jabber.org/protocol/pubsub">
<create node="testnode2" />
<configure>
<x xmlns="jabber:x:data" type="submit">
<field var="FORM_TYPE" type="hidden">
<value>http://jabber.org/protocol/pubsub#node_config</value>
</field>
<field var="pubsub#node_type" type="list-single" label="Select the node type">
<value>leaf</value>
</field>
<field var="pubsub#title" type="text-single" label="A friendly name for the node" />
<field var="pubsub#deliver_notifications" type="boolean" label="Deliver event notifications">
<value>1</value>
</field>
<field var="pubsub#deliver_payloads" type="boolean" label="Deliver payloads with event notifications">
<value>1</value>
</field>
<field var="pubsub#notify_config" type="boolean" label="Notify subscribers when the node configuration changes" />
<field var="pubsub#notify_delete" type="boolean" label="Notify subscribers when the node is deleted" />
<field var="pubsub#notify_retract" type="boolean" label="Notify subscribers when items are removed from the node">
<value>1</value>
</field>
<field var="pubsub#notify_sub" type="boolean" label="Notify owners about new subscribers and unsubscribes" />
<field var="pubsub#persist_items" type="boolean" label="Persist items in storage" />
<field var="pubsub#max_items" type="text-single" label="Max # of items to persist">
<value>10</value>
</field>
<field var="pubsub#subscribe" type="boolean" label="Whether to allow subscriptions">
<value>1</value>
</field>
<field var="pubsub#access_model" type="list-single" label="Specify the subscriber model">
<value>open</value>
</field>
<field var="pubsub#publish_model" type="list-single" label="Specify the publisher model">
<value>publishers</value>
</field>
<field var="pubsub#send_last_published_item" type="list-single" label="Send last published item">
<value>never</value>
</field>
<field var="pubsub#presence_based_delivery" type="boolean" label="Deliver notification only to available users" />
</x>
</configure>
</pubsub>
</iq>""")
def testItemEvent(self):
"""Testing message/pubsub_event/items/item"""
msg = self.Message()
item = pubsub.EventItem()
pl = ET.Element('{http://netflint.net/protocol/test}test', {'failed':'3', 'passed':'24'})
item['payload'] = pl
item['id'] = 'abc123'
msg['pubsub_event']['items'].append(item)
msg['pubsub_event']['items']['node'] = 'cheese'
msg['type'] = 'normal'
self.check(msg, """
<message type="normal">
<event xmlns="http://jabber.org/protocol/pubsub#event">
<items node="cheese">
<item id="abc123">
<test xmlns="http://netflint.net/protocol/test" failed="3" passed="24" />
</item>
</items>
</event>
</message>""")
def testItemsEvent(self):
"""Testing multiple message/pubsub_event/items/item"""
msg = self.Message()
item = pubsub.EventItem()
item2 = pubsub.EventItem()
pl = ET.Element('{http://netflint.net/protocol/test}test', {'failed':'3', 'passed':'24'})
pl2 = ET.Element('{http://netflint.net/protocol/test-other}test', {'total':'27', 'failed':'3'})
item2['payload'] = pl2
item['payload'] = pl
item['id'] = 'abc123'
item2['id'] = '123abc'
msg['pubsub_event']['items'].append(item)
msg['pubsub_event']['items'].append(item2)
msg['pubsub_event']['items']['node'] = 'cheese'
msg['type'] = 'normal'
self.check(msg, """
<message type="normal">
<event xmlns="http://jabber.org/protocol/pubsub#event">
<items node="cheese">
<item id="abc123">
<test xmlns="http://netflint.net/protocol/test" failed="3" passed="24" />
</item>
<item id="123abc">
<test xmlns="http://netflint.net/protocol/test-other" failed="3" total="27" />
</item>
</items>
</event>
</message>""")
def testItemsEvent(self):
"""Testing message/pubsub_event/items/item & retract mix"""
msg = self.Message()
item = pubsub.EventItem()
item2 = pubsub.EventItem()
pl = ET.Element('{http://netflint.net/protocol/test}test', {'failed':'3', 'passed':'24'})
pl2 = ET.Element('{http://netflint.net/protocol/test-other}test', {'total':'27', 'failed':'3'})
item2['payload'] = pl2
retract = pubsub.EventRetract()
retract['id'] = 'aabbcc'
item['payload'] = pl
item['id'] = 'abc123'
item2['id'] = '123abc'
msg['pubsub_event']['items'].append(item)
msg['pubsub_event']['items'].append(retract)
msg['pubsub_event']['items'].append(item2)
msg['pubsub_event']['items']['node'] = 'cheese'
msg['type'] = 'normal'
self.check(msg, """
<message type="normal">
<event xmlns="http://jabber.org/protocol/pubsub#event">
<items node="cheese">
<item id="abc123">
<test xmlns="http://netflint.net/protocol/test" failed="3" passed="24" />
</item><retract id="aabbcc" />
<item id="123abc">
<test xmlns="http://netflint.net/protocol/test-other" failed="3" total="27" />
</item>
</items>
</event>
</message>""")
def testCollectionAssociate(self):
"""Testing message/pubsub_event/collection/associate"""
msg = self.Message()
msg['pubsub_event']['collection']['associate']['node'] = 'cheese'
msg['pubsub_event']['collection']['node'] = 'cheeseburger'
msg['type'] = 'headline'
self.check(msg, """
<message type="headline">
<event xmlns="http://jabber.org/protocol/pubsub#event">
<collection node="cheeseburger">
<associate node="cheese" />
</collection>
</event>
</message>""")
def testCollectionDisassociate(self):
"""Testing message/pubsub_event/collection/disassociate"""
msg = self.Message()
msg['pubsub_event']['collection']['disassociate']['node'] = 'cheese'
msg['pubsub_event']['collection']['node'] = 'cheeseburger'
msg['type'] = 'headline'
self.check(msg, """
<message type="headline">
<event xmlns="http://jabber.org/protocol/pubsub#event">
<collection node="cheeseburger">
<disassociate node="cheese" />
</collection>
</event>
</message>""")
def testEventConfiguration(self):
"""Testing message/pubsub_event/configuration/config"""
msg = self.Message()
msg['pubsub_event']['configuration']['node'] = 'cheese'
msg['pubsub_event']['configuration']['form'].addField('pubsub#title',
ftype='text-single',
value='This thing is awesome')
msg['type'] = 'headline'
self.check(msg, """
<message type="headline">
<event xmlns="http://jabber.org/protocol/pubsub#event">
<configuration node="cheese">
<x xmlns="jabber:x:data" type="form">
<field var="pubsub#title" type="text-single">
<value>This thing is awesome</value>
</field>
</x>
</configuration>
</event>
</message>""")
def testEventPurge(self):
"""Testing message/pubsub_event/purge"""
msg = self.Message()
msg['pubsub_event']['purge']['node'] = 'pickles'
msg['type'] = 'headline'
self.check(msg, """
<message type="headline">
<event xmlns="http://jabber.org/protocol/pubsub#event">
<purge node="pickles" />
</event>
</message>""")
def testEventSubscription(self):
"""Testing message/pubsub_event/subscription"""
msg = self.Message()
msg['pubsub_event']['subscription']['node'] = 'pickles'
msg['pubsub_event']['subscription']['jid'] = 'fritzy@netflint.net/test'
msg['pubsub_event']['subscription']['subid'] = 'aabb1122'
msg['pubsub_event']['subscription']['subscription'] = 'subscribed'
msg['pubsub_event']['subscription']['expiry'] = 'presence'
msg['type'] = 'headline'
self.check(msg, """
<message type="headline">
<event xmlns="http://jabber.org/protocol/pubsub#event">
<subscription node="pickles" subid="aabb1122" jid="fritzy@netflint.net/test" subscription="subscribed" expiry="presence" />
</event>
</message>""")
suite = unittest.TestLoader().loadTestsFromTestCase(TestPubsubStanzas)

View File

@@ -0,0 +1,44 @@
from sleekxmpp.test import *
import sleekxmpp.plugins.xep_0085 as xep_0085
class TestChatStates(SleekTest):
def setUp(self):
register_stanza_plugin(Message, xep_0085.Active)
register_stanza_plugin(Message, xep_0085.Composing)
register_stanza_plugin(Message, xep_0085.Gone)
register_stanza_plugin(Message, xep_0085.Inactive)
register_stanza_plugin(Message, xep_0085.Paused)
def testCreateChatState(self):
"""Testing creating chat states."""
xmlstring = """
<message>
<%s xmlns="http://jabber.org/protocol/chatstates" />
</message>
"""
msg = self.Message()
msg['chat_state'].active()
self.check(msg, xmlstring % 'active',
use_values=False)
msg['chat_state'].composing()
self.check(msg, xmlstring % 'composing',
use_values=False)
msg['chat_state'].gone()
self.check(msg, xmlstring % 'gone',
use_values=False)
msg['chat_state'].inactive()
self.check(msg, xmlstring % 'inactive',
use_values=False)
msg['chat_state'].paused()
self.check(msg, xmlstring % 'paused',
use_values=False)
suite = unittest.TestLoader().loadTestsFromTestCase(TestChatStates)

View File

@@ -1,261 +0,0 @@
import unittest
import time, threading, random, functools
if __name__ == '__main__':
import sys, os
sys.path.insert(0, os.getcwd())
import sleekxmpp.xmlstream.statemachine as sm
class testStateMachine(unittest.TestCase):
def setUp(self): pass
def testDefaults(self):
"Test ensure transitions occur correctly in a single thread"
s = sm.StateMachine(('one','two','three'))
self.assertTrue(s['one'])
self.failIf(s['two'])
try:
s['booga']
self.fail('s.booga is an invalid state and should throw an exception!')
except: pass #expected exception
# just make sure __str__ works, no reason to test its exact value:
print str(s)
def testTransitions(self):
"Test ensure transitions occur correctly in a single thread"
s = sm.StateMachine(('one','two','three'))
self.assertTrue( s.transition('one', 'two') )
self.assertTrue( s['two'] )
self.failIf( s['one'] )
self.assertTrue( s.transition('two', 'three') )
self.assertTrue( s['three'] )
self.failIf( s['two'] )
self.assertTrue( s.transition('three', 'one') )
self.assertTrue( s['one'] )
self.failIf( s['three'] )
# should return False immediately w/ no wait:
self.failIf( s.transition('three', 'one') )
self.assertTrue( s['one'] )
self.failIf( s['three'] )
# test fail condition w/ a short delay:
self.failIf( s.transition('two', 'three') )
# Ensure bad states are weeded out:
try:
s.transition('blah', 'three')
s.fail('Exception expected')
except: pass
try:
s.transition('one', 'blahblah')
s.fail('Exception expected')
except: pass
def testTransitionsBlocking(self):
"Test that transitions block from more than one thread"
s = sm.StateMachine(('one','two','three'))
self.assertTrue(s['one'])
now = time.time()
self.failIf( s.transition('two', 'one', wait=5.0) )
self.assertTrue( time.time() > now + 4 )
self.assertTrue( time.time() < now + 7 )
def testThreadedTransitions(self):
"Test that transitions are atomic in > one thread"
s = sm.StateMachine(('one','two','three'))
self.assertTrue(s['one'])
thread_state = {'ready': False, 'transitioned': False}
def t1():
if s['two']:
print 'thread has already transitioned!'
self.fail()
thread_state['ready'] = True
print 'Thread is ready'
# this will block until the main thread transitions to 'two'
self.assertTrue( s.transition('two','three', wait=20) )
print 'transitioned to three!'
thread_state['transitioned'] = True
thread = threading.Thread(target=t1)
thread.daemon = True
thread.start()
start = time.time()
while not thread_state['ready']:
print 'not ready'
if time.time() > start+10: self.fail('Timeout waiting for thread to init!')
time.sleep(0.1)
time.sleep(0.2) # the thread should be blocking on the 'transition' call at this point.
self.failIf( thread_state['transitioned'] ) # ensure it didn't 'go' yet.
print 'transitioning to two!'
self.assertTrue( s.transition('one','two') )
time.sleep(0.2) # second thread should have transitioned now:
self.assertTrue( thread_state['transitioned'] )
def testForRaceCondition(self):
"""Attempt to allow two threads to perform the same transition;
only one should ever make it."""
s = sm.StateMachine(('one','two','three'))
def t1(num):
while True:
if not trigger['go'] or thread_state[num] in (True,False):
time.sleep( random.random()/100 ) # < .01s
if thread_state[num] == 'quit': break
continue
thread_state[num] = s.transition('one','two' )
# print '-',
thread_count = 20
threads = []
thread_state = {}
def reset():
for c in range(thread_count): thread_state[c] = "reset"
trigger = {'go':False} # use of a plain boolean seems to be non-volatile between threads.
for c in range(thread_count):
thread_state[c] = "reset"
thread = threading.Thread( target= functools.partial(t1,c) )
threads.append( thread )
thread.daemon = True
thread.start()
for x in range(100): # this will take 10s to execute
# print "+",
trigger['go'] = True
time.sleep(.1)
trigger['go'] = False
winners = 0
for (num, state) in thread_state.items():
if state == True: winners = winners +1
elif state != False: raise Exception( "!%d!%s!" % (num,state) )
self.assertEqual( 1, winners, "Expected one winner! %d" % winners )
self.assertTrue( s.ensure('two') )
self.assertTrue( s.transition('two','one') ) # return to the first state.
reset()
# now let the threads quit gracefully:
for c in range(thread_count): thread_state[c] = 'quit'
time.sleep(2)
def testTransitionFunctions(self):
"test that a `func` argument allows or blocks the transition correctly."
s = sm.StateMachine(('one','two','three'))
def alwaysFalse(): return False
def alwaysTrue(): return True
self.failIf( s.transition('one','two', func=alwaysFalse) )
self.assertTrue(s['one'])
self.failIf(s['two'])
self.assertTrue( s.transition('one','two', func=alwaysTrue) )
self.failIf(s['one'])
self.assertTrue(s['two'])
def testTransitionFuncException(self):
"if a transition function throws an exeption, ensure we're in a sane state"
s = sm.StateMachine(('one','two','three'))
def alwaysException(): raise Exception('whups!')
try:
self.failIf( s.transition('one','two', func=alwaysException) )
self.fail("exception should have been thrown")
except: pass #expected exception
self.assertTrue(s['one'])
self.failIf(s['two'])
# ensure a subsequent attempt completes normally:
self.assertTrue( s.transition('one','two') )
self.failIf(s['one'])
self.assertTrue(s['two'])
def testContextManager(self):
s = sm.StateMachine(('one','two','three'))
with s.transition_ctx('one','two'):
self.assertTrue( s['one'] )
self.failIf( s['two'] )
#successful transition b/c no exception was thrown
self.assertTrue( s['two'] )
self.failIf( s['one'] )
# failed transition because exception is thrown:
try:
with s.transition_ctx('two','three'):
raise Exception("boom!")
self.fail('exception expected')
except: pass
self.failIf( s.current_state() in ('one','three') )
self.assertTrue( s['two'] )
def testCtxManagerTransitionFailure(self):
s = sm.StateMachine(('one','two','three'))
with s.transition_ctx('two','three') as result:
self.failIf( result )
self.assertTrue( s['one'] )
self.failIf( s.current_state in ('two','three') )
self.assertTrue( s['one'] )
def r1():
print 'thread 1 started'
self.assertTrue( s.transition('one','two') )
print 'thread 1 transitioned'
def r2():
print 'thread 2 started'
self.failIf( s['two'] )
with s.transition_ctx('two','three', 10) as result:
self.assertTrue( result )
self.assertTrue( s['two'] )
print 'thread 2 will transition on exit from the context manager...'
self.assertTrue( s['three'] )
print 'transitioned to %s' % s.current_state()
t1 = threading.Thread(target=r1)
t2 = threading.Thread(target=r2)
t2.start() # this should block until r1 goes
time.sleep(1)
t1.start()
t1.join()
t2.join()
self.assertTrue( s['three'] )
suite = unittest.TestLoader().loadTestsFromTestCase(testStateMachine)
if __name__ == '__main__': unittest.main()

60
tests/test_stream.py Normal file
View File

@@ -0,0 +1,60 @@
from sleekxmpp.test import *
import sleekxmpp.plugins.xep_0033 as xep_0033
class TestStreamTester(SleekTest):
"""
Test that we can simulate and test a stanza stream.
"""
def tearDown(self):
self.stream_close()
def testClientEcho(self):
"""Test that we can interact with a ClientXMPP instance."""
self.stream_start(mode='client')
def echo(msg):
msg.reply('Thanks for sending: %(body)s' % msg).send()
self.xmpp.add_event_handler('message', echo)
self.recv("""
<message to="tester@localhost" from="user@localhost">
<body>Hi!</body>
</message>
""")
self.send("""
<message to="user@localhost">
<body>Thanks for sending: Hi!</body>
</message>
""")
def testComponentEcho(self):
"""Test that we can interact with a ComponentXMPP instance."""
self.stream_start(mode='component')
def echo(msg):
msg.reply('Thanks for sending: %(body)s' % msg).send()
self.xmpp.add_event_handler('message', echo)
self.recv("""
<message to="tester.localhost" from="user@localhost">
<body>Hi!</body>
</message>
""")
self.send("""
<message to="user@localhost" from="tester.localhost">
<body>Thanks for sending: Hi!</body>
</message>
""")
def testSendStreamHeader(self):
"""Test that we can check a sent stream header."""
self.stream_start(mode='client', skip=False)
self.send_header(sto='localhost')
suite = unittest.TestLoader().loadTestsFromTestCase(TestStreamTester)

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