Compare commits

...

404 Commits

Author SHA1 Message Date
Lance Stout
e4b4c67637 Bump version to 1.1.10 2012-07-30 09:04:15 -07:00
Lance Stout
422e77ae40 Don't wait to retry connection if out of DNS records. 2012-07-29 17:26:04 -07:00
Lance Stout
5ae6c8f8fa Add support for XEP-0131: Standard Headers and Internet Metadata 2012-07-28 01:06:21 -07:00
Lance Stout
54656b331a Restrict caps updates to available presences (not subscriptions, etc). 2012-07-27 15:51:35 -07:00
Lance Stout
9047b627a4 Only broadcast vCard hashes for available presences (not subscriptions, etc). 2012-07-27 15:48:15 -07:00
Lance Stout
6645a3be40 Compile JID pattern regex. 2012-07-27 11:24:01 -07:00
Jonas Wielicki
e3fab66dfb Allow tasks to remove themselves during execution
The scheduler class is now capable with dealing with tasks which remove
themselves from the scheduler during execution.

Additionally, some optimizations were applied by use of iterators and
some functions better suited for the purpose.

Please peer-review, all tests pass.
2012-07-27 10:45:23 -07:00
Lance Stout
5867f08bf1 Improve docs and fix typo in stringprep profiles. 2012-07-26 23:35:23 -07:00
Lance Stout
a06fa2de67 Enhance plugin config with attribute accessors.
This makes updating the config after plugin initialization much easier.
2012-07-26 23:04:16 -07:00
Lance Stout
35396d2977 Don't include a 'from' JID when requesting vCards as a client. 2012-07-26 11:55:54 -07:00
Lance Stout
3bff743d9f Fix logging statement for MUC invitations. 2012-07-26 11:53:07 -07:00
Lance Stout
5a878f829b Fix error with session binding in components. 2012-07-26 11:50:59 -07:00
Lance Stout
26dc6e90ea Add example for setting an avatar. 2012-07-25 01:37:03 -07:00
Lance Stout
94c749fd5a Fix avatar hash advertising. 2012-07-25 01:36:31 -07:00
Lance Stout
7b80ed0807 Substitute a blank JID for the boundjid in API calls. 2012-07-25 01:33:44 -07:00
Lance Stout
98b7e8b10a Fix initializing plugins in stanzas with a language set. 2012-07-25 01:33:17 -07:00
Lance Stout
9d8de7fc15 Fix publish vcard avatars, and PEP avatar metadata. 2012-07-24 19:43:39 -07:00
Lance Stout
70883086b7 Modify update_roster() to only change the information provided.
Before: Not specifying the groups, name, etc would remove them from the
        roster entry.

After: Any parameters not specified are populated with the current
       roster entry's values.
2012-07-24 16:48:24 -07:00
Lance Stout
9a08dfc7d4 Add support for using CDATA for escaping.
CDATA escaping is disabled by default, but may be enabled by setting:

    self.use_cdata = True

Closes issue #114
2012-07-24 03:25:55 -07:00
Lance Stout
3e43b36a9d Standardize importing of queue class.
This will make it easier to enable gevent support.
2012-07-24 02:39:54 -07:00
Lance Stout
352ee2f2fd Fix JID validation bugs, add lots of tests. 2012-07-24 01:43:20 -07:00
Lance Stout
78aa5c3dfa Add more validation for 0 length JID components. 2012-07-24 01:43:20 -07:00
Lance Stout
613323b5fb Finish docstrings for jid.py 2012-07-24 01:43:20 -07:00
Lance Stout
6c4b01db8a Add plugin for advertising XEP-0106 support. 2012-07-24 01:43:20 -07:00
Lance Stout
d06897a635 Add backwards compatibility shim for the old jid.py location. 2012-07-24 01:43:20 -07:00
Lance Stout
1600bb0aaf Cleanup and docs. 2012-07-24 01:43:20 -07:00
Lance Stout
b5c9c98a8b Add JID escaping support. 2012-07-24 01:43:20 -07:00
Lance Stout
e4e18a416f Add validation for JIDs. 2012-07-24 01:43:20 -07:00
Lance Stout
01cc0e6def Add 'by' attribute for error stanzas. 2012-07-23 21:48:19 -07:00
ekini
d571d691a7 old clients still support xep-184/1.0 version
Now psi (and probably miranda) correctly receive delivery receipts.
2012-07-23 01:52:45 -07:00
Lance Stout
fb221a8dc0 Add XEP-0133 support, which just makes the appropriate XEP-0050 calls. 2012-07-22 13:58:23 -07:00
Lance Stout
459e1ed345 Handle Windows newlines in XEP-0027.
Closes issue #184
2012-07-22 12:15:46 -07:00
Lance Stout
6680c244f5 Fix deprecation warning for setting self.resource 2012-07-20 22:04:36 -07:00
Lance Stout
06423964ec Fix description of XEP-0222 plugin. 2012-07-20 22:03:17 -07:00
Lance Stout
474390fa00 Add example for retrieving avatars. 2012-07-20 18:10:14 -07:00
Lance Stout
81d3723084 Add event for vCard avatar update. 2012-07-20 18:07:27 -07:00
Lance Stout
32e798967e Fix see-other-host handling if no host is actually given. Also, limit number of consecutive redirection attempts. 2012-07-20 15:28:18 -07:00
Lance Stout
acd9c32a9f Bump version to 1.1.9 2012-07-20 00:17:53 -07:00
Lance Stout
b8581b0278 Of course Peter goes and changes the XEP title the day after I implement it. 2012-07-19 23:59:35 -07:00
Lance Stout
917faecdcb Fix issue of roster data being split across multiple rosters.
Resolved by always normalizing JIDs to bare form, regardless of if they
are JID objects or strings.

Also simplified related code to prefer use of JID objects instead of
strings so they don't need to be parsed multiple times.
2012-07-19 23:54:18 -07:00
Lance Stout
f6edaa56a6 Add plugin for XEP-0191: Simple Communications Blocking 2012-07-16 20:10:14 -07:00
Lance Stout
51fee28bf4 Add a warning log if dnspython is not found for SRV lookup.
Closes issue #183
2012-07-16 19:38:50 -07:00
Lance Stout
e8a3e92ceb Update plugins to use session_bind handler for disco, and use plugin_end 2012-07-10 01:37:44 -07:00
Lance Stout
5df3839b7a Add method to remove a filter. 2012-07-10 01:37:23 -07:00
Lance Stout
8dcb441f44 Add default plugin session_bind handler.
All plugins may now simply define a session_bind method where disco
features and other actions which require the bound JID may be done.
2012-07-10 01:36:21 -07:00
Lance Stout
a347cf625a Add session_bind_event threading event. 2012-07-10 01:35:57 -07:00
Lance Stout
46f49c7a12 Add method to unregister stream features. 2012-07-10 01:35:25 -07:00
Lance Stout
99701c947e Prevent None from being added to the schedule from a timing issue. 2012-07-09 22:59:26 -07:00
Lance Stout
1baae1b81e Fix issues of disco info leaking between entities with the same bare JIDs.
To ensure that disco info, or any settings which depend on the bound
JID, are correct, only set such information on or after the
session_bound event has fired.
2012-07-09 22:22:05 -07:00
Lance Stout
7d20f0e9a6 Fix missing import in xep_0256 plugin. 2012-07-09 22:21:40 -07:00
Lance Stout
fbad22a1cd Merge pull request #181 from whooo/upstream
Fix for the RSM iterator
2012-07-09 09:25:09 -07:00
Erik Larsson
5af2f62c04 Make sure that the last RSM stanza is returned from the iterator 2012-07-08 23:27:13 +02:00
Jay Farrimond
4a4a03858e dereference iq stanza only once for roster processing 2012-07-06 14:03:41 -07:00
Lance Stout
6819b57353 Handle converting None to byte data (b''). 2012-07-06 11:05:47 -07:00
Jay Farrimond
88b5e60807 only log cert errors if not handled by user 2012-07-05 13:38:26 -07:00
Lance Stout
a26a8bd79c Bump version to 1.1.8 2012-06-30 17:40:11 -07:00
Lance Stout
9307a6915f Add notes to echo_client.py example on working with Facebook and MSN. 2012-06-23 22:30:24 -07:00
Lance Stout
85ef2d8d0b Add support for reconnecting based on see-other-host stream errors. 2012-06-22 23:13:16 -07:00
Lance Stout
c2c7cc032b Fix plugin registration for single file plugins. 2012-06-22 21:58:50 -07:00
Lance Stout
e4911e9391 Add meta plugin for XEP-0302 for the 2012 compliance suite.
There are still a few remaining items in the RFCs to add support for,
but the current plugin support matches the advanced client profile.
2012-06-22 21:52:39 -07:00
Lance Stout
b11e1ee92d Add meta plugin for XEP-0270, 2010 compliance suite.
Registering this plugin will load the plugins required for advanced
client compliance status.
2012-06-22 21:26:25 -07:00
Lance Stout
5027d00c10 Change packaging for XEP-0256 to just a single file. 2012-06-22 21:26:01 -07:00
Lance Stout
69ddeceb49 Add support for XEP-0256: Last Activity in Presence 2012-06-22 21:13:30 -07:00
Lance Stout
82698672bb Add 'thread' and 'parent_thread' interfaces to message stanzas.
These values are perisisted across replies.
2012-06-22 20:05:34 -07:00
Lance Stout
9cec284947 Mark presence status as language aware. 2012-06-22 20:05:17 -07:00
Lance Stout
dc501d1902 Mark message body and subject as language aware interfaces. 2012-06-22 19:08:51 -07:00
Lance Stout
100e504b7f Resolve xml:lang issue with duplicated elements depending on ordering. 2012-06-22 18:19:17 -07:00
Lance Stout
8a745c5e81 Bump version to 1.1.7 2012-06-20 23:45:14 -07:00
Lance Stout
bf0a157c5d Add support for XEP-0221: Data Forms Media Element 2012-06-20 23:38:30 -07:00
Lance Stout
f49818be06 Add support for XEP-0186: Invisible Command 2012-06-20 23:37:39 -07:00
Lance Stout
1ad171dfe5 Fix issue with setting subelements values with default langs. 2012-06-20 23:19:52 -07:00
Lance Stout
2a78570d65 Fix setting IPv6 default configuration option. 2012-06-20 22:21:34 -07:00
Lance Stout
7a112f2523 Bump version to 1.1.6 2012-06-20 21:08:43 -07:00
Lance Stout
e86444e5fb Make the use of IPv6 configurable.
Set self.use_ipv6 = False before connecting.

Fixes issue #175
2012-06-20 19:39:24 -07:00
Lance Stout
36c11ad9de Ordering fixes for Python3.3 2012-06-19 18:19:44 -07:00
Lance Stout
019a4b20ae Fix assigning values to error stanzas.
The new data interfaces were deleting the actual error conditions if
they were set afterward with falsy data.
2012-06-19 16:21:34 -07:00
Lance Stout
433ee08687 Allow message and presence stanzas to be embedded as substanzas. 2012-06-19 16:20:54 -07:00
Lance Stout
7858d969d8 Remove usage of deprecated getchildren() method. 2012-06-19 09:47:31 -07:00
Lance Stout
8119551049 Don't compare against booleans using ==. 2012-06-19 01:38:36 -07:00
Lance Stout
061489f03a Limit except clause to just ImportErrors when loading plugins. 2012-06-19 01:38:12 -07:00
Lance Stout
d92aa05b5c PEP8 formatting updates. 2012-06-19 01:29:48 -07:00
Lance Stout
f7a74d960e Simplify send_presence_subscription() 2012-06-19 00:06:31 -07:00
Lance Stout
95a0e51b41 Add example for dealing with GTalk custom domain certificates. 2012-06-19 00:02:36 -07:00
Lance Stout
110e45e187 Add examples for using IBB. 2012-06-19 00:02:17 -07:00
Lance Stout
534aaf2b2a Properly handle certs with no extensions. 2012-06-19 00:01:02 -07:00
Lance Stout
4cc20fdd05 Use plugin_multi_attrib values to make vcards nicer. 2012-06-18 23:19:38 -07:00
Lance Stout
f3fae192a8 Fix plugin_multi_attrib value for avatar pointers. 2012-06-18 23:05:02 -07:00
Paulo Freitas
7d59a8a0ad Fixed typo in _handle_get_vcard() 2012-06-18 22:54:30 -07:00
Lance Stout
8da387a38a Add support for error conditions that include data. 2012-06-18 22:19:04 -07:00
Lance Stout
ff6fc44215 Simplify tracking last sent presence using outgoing filters. 2012-06-18 22:15:21 -07:00
Lance Stout
62391a895a Update plugin list, fix syntax error. 2012-06-18 22:08:38 -07:00
Lance Stout
9bcdd7d18f Add initial support for XEPS 222 and 223. 2012-06-18 22:08:38 -07:00
Lance Stout
5c4f7bfe8b Initial support for XEP-0258 2012-06-18 22:07:39 -07:00
Lance Stout
0b7f134021 Add initial XEP-0084 support.
It does not auto-retrieve and store avatars yet, but everything is there
to do so.
2012-06-18 22:07:17 -07:00
Lance Stout
378a42889f Simplify and update XEP-0033 to latest plugin format. 2012-06-18 22:03:03 -07:00
Lance Stout
f824950552 Enable using xml:lang with normal interfaces.
Using the special language value '*' will return a dictionary of all
such elements keyed by language.

    >>> msg = Message()
    >>> msg['body'] = 'Hi!'
    >>> msg['body|sv'] = 'Hej!'
    >>> print(msg)
    '<message xmlns="jabber:client">
      <body>Hi!</body>
      <body xml:lang="sv">Hej!</body>
    </message>'
    >>> print(msg['body|*'])
    OrderedDict(
        ('', 'Hi!'),
        ('sv', 'Hej!'))

Remaining items:

- Stanza path matching does not support language specifiers for normal
  interfaces, only for plugins.
2012-06-18 22:00:33 -07:00
Lance Stout
3d2d11f169 Update stream features stanza to work with new plugin keys. 2012-06-18 22:00:33 -07:00
Lance Stout
181aea737d Add initial support for xml:lang for streams and stanza plugins.
Remaining items are suitable default actions for language supporting
interfaces.
2012-06-18 22:00:33 -07:00
Lance Stout
ee702f4071 Bump version to 1.1.5 2012-06-15 15:36:01 -07:00
Lance Stout
a08c2161a7 Ensure that ssl_invalid_cert returns PEM formatted certifcate data. 2012-06-15 15:29:53 -07:00
Lance Stout
0e36a01354 Bump version to 1.1.4 2012-06-13 09:17:08 -07:00
Lance Stout
c39ad7dfbb Prevent duplicate certificate expiration timers. 2012-06-13 09:13:33 -07:00
Lance Stout
b92ae706e9 Fix loading cached disco identity data. 2012-06-13 09:13:13 -07:00
Lance Stout
6997261c6b Bump version for 1.1.3 2012-06-09 11:32:03 -07:00
Lance Stout
6cfb5cb14c Add extra check for the cert in the expiration handler. 2012-06-09 11:01:45 -07:00
Lance Stout
8567d6034f Use False for use_tls for components.
A log message is shown for those who try to set it to True.

Fixes issue #171
2012-06-09 11:01:35 -07:00
Lance Stout
e06368f8cd Default use_tls to False for components.
Issue #171
2012-06-09 11:01:21 -07:00
Lance Stout
4b37a4706f Fix SSL handshake handling when not using legacy SSL.
Fixes issue #172
2012-06-09 11:01:11 -07:00
Lance Stout
7b1564947d Ensure that all SSL cert error handling is overridable using event handlers.
Relevant events:

    ssl_invalid_cert
    ssl_invalid_chain
    ssl_expired_cert
2012-06-09 11:00:55 -07:00
Lance Stout
f5652a667b Add 'presence' event, raised for all incoming presence stanzas. 2012-06-06 16:10:25 -07:00
Lance Stout
3b2c865a58 Bump version to 1.1.2 2012-06-06 12:26:15 -07:00
Lance Stout
db0e683d01 Don't request registration forms unless the register event is handled.
Some servers end the stream if registration can not be completed
in-band, which means always requesting the form can prevent regular
login.
2012-06-06 12:23:40 -07:00
Lance Stout
e29a9e0394 Bump version for 1.1.1 minor release. 2012-06-04 11:56:53 -07:00
Lance Stout
edf65f4f52 Include the default, unnamed group in self.client_roster.groups() 2012-06-04 11:54:25 -07:00
Lance Stout
98677fd602 Don't add cert expiration timer if no certs are being used. 2012-06-04 11:53:58 -07:00
Lance Stout
61a4f76c8d Update version and README for 1.1 2012-06-01 14:13:17 -07:00
Lance Stout
856a826eea Fix syntax error in line continuation. 2012-06-01 14:09:14 -07:00
Lance Stout
387ef513d6 Check that the session is still alive before sending data.
Fixes issue #168
2012-06-01 13:50:38 -07:00
Lance Stout
2858dbf57f Update development version number to prepare for 1.1 release. 2012-05-31 22:07:36 -07:00
Lance Stout
350a2b8bbc Preemptively mark threads as exited if calling disconnect(). 2012-05-31 22:04:45 -07:00
Lance Stout
c9093c9972 Handle not being able to connect using IPv6 if the host does not support it. 2012-05-27 16:33:21 -07:00
Lance Stout
d1ad31696e Fix X-FACEBOOK-PLATFORM mechanism to work with Python3. 2012-05-25 11:04:46 -07:00
Lance Stout
f49311ef9e Add better certificate handling.
Certificate host names are now matched (using DNS, SRV, XMPPAddr, and
Common Name), along with expiration check.

Scheduled event to reset the stream once the server's cert expires.

Handle invalid cert trust chains gracefully now.
2012-05-22 03:56:06 -07:00
Lance Stout
678e529efc Remove unused xmlstream test client.
It's in the repo history if we need it later.
2012-05-17 22:27:03 -07:00
Lance Stout
6ddb430fef Spell thirdparty correctly. 2012-05-16 12:00:00 -07:00
Lance Stout
74d1f88146 Prune unused conn_test code. 2012-05-16 11:57:55 -07:00
Lance Stout
7842c55da3 Add auth_success event.
The auth_success event is triggered upon successful SASL negotiation.
2012-05-15 14:26:25 -07:00
Lance Stout
f5beac2afa Use SASLPrepFailure as the exception name instead of UnicodeError. 2012-05-14 23:12:54 -07:00
Lance Stout
8a23f28dfa Add an exception handler for SASLprep failures. 2012-05-14 22:26:06 -07:00
Lance Stout
9c4886e746 Remove extra connection info so that examples run without modification.
GTalk users may still need to change the connect() call if dnspython is
not installed, as usual.
2012-05-14 22:17:39 -07:00
Lance Stout
e0bcd5d722 Add more documentation to the custom stanza examples. 2012-05-14 22:12:52 -07:00
Erick Pérez Castellanos
ba854e7d85 Added custom_stanza example 2012-05-14 21:47:43 -07:00
Lance Stout
4ded34ebc9 Add MUC events for room configuration changes.
New events:
    groupchat_config_status
    muc::[room JID]::config_status
2012-05-14 16:10:22 -07:00
Lance Stout
e918a86028 Make the error message better regarding hanged threads.
All event handlers which call disconnect() MUST be registered using
`add_event_handler(..., threaded=True)` in order to prevent temporarily
deadlocking until a timeout occurs.

This is required because disconnect() waits for the main threads to
exit before returning, including the event processing thread. Since
handlers registered without `threaded=True` run in the event processing
thread, the disconnect() call will deadlock.
2012-05-10 10:22:38 -07:00
Lance Stout
24234bf718 Update other examples to use threaded mode for handlers that call disconnect() 2012-05-06 20:19:02 -07:00
Lance Stout
ec99339140 Update send_client.py to call disconnect() from a threaded handler. 2012-05-06 20:07:05 -07:00
Lance Stout
03dedfc871 Windows doesn't support inet_pton. 2012-05-06 12:17:50 -07:00
Lance Stout
9e86a7b357 Tidy up and add tests for multi_attrib plugins. 2012-05-05 14:01:13 -07:00
Lance Stout
6a32417957 Merge pull request #163 from whooo/master
factory for recurring substanzas
2012-05-05 11:34:29 -07:00
Lance Stout
97a7be7dfa Fix loading plugins from custom modules when passing the module itself.
Loading plugins from custom modules when passed as a string still works.
2012-05-04 09:51:02 -07:00
Erik Larsson
fa86f956ef added multifactory and support for it to register_stanza_plugin 2012-04-30 22:19:17 +02:00
Lance Stout
a9acff5294 Collapse initial payload to a single stanza instead of a list if only one stanza is found. 2012-04-30 11:16:10 -07:00
Lance Stout
ad5b61de50 Add full support for initial payloads with adhoc commands, plus test. 2012-04-30 11:07:54 -07:00
Lance Stout
f53b815855 Allow providing initial payload to adhoc commands. 2012-04-30 08:27:10 -07:00
Lance Stout
bf8a9dc20d Add logging note about potential cause of disconnect() deadlock. 2012-04-29 14:48:14 -07:00
Lance Stout
08716c35fd Set a timeout when waiting for threads.
If calling disconnect() from a non-threaded event handler, deadlock can
happen as disconnect() is waiting for threads to close, but the event
runner is blocked by a handler waiting for disconnect() to return.

It is best to specify threaded=True for event handlers which may call
disconnect().
2012-04-29 14:45:00 -07:00
Lance Stout
fd81bab906 Use the correct 'from' jid when requesting vcards for avatars. 2012-04-29 13:33:53 -07:00
Lance Stout
1cf55c14b0 Don't raise errors when receiving an iq error for vcards. 2012-04-29 13:33:30 -07:00
Lance Stout
8b47159788 Populate the to attribute for message and presence stanzas if the server leaves it blank. 2012-04-26 15:46:18 -07:00
Lance Stout
2eeaf4d80c Use provided stanza ID. 2012-04-25 13:55:46 -07:00
Lance Stout
4d89d26a1c Prevent corrupting roster_update event with iq result. 2012-04-25 11:03:33 -07:00
Lance Stout
0cc14cee4d Ensure that SSL errors are handled in Py3.3 2012-04-24 16:11:49 -07:00
Lance Stout
a20a9c505d Track threads to ensure all have exited when disconnecting. 2012-04-22 18:13:36 -07:00
Lance Stout
913738444e Count and track the main threads, so we can delay disconnecting until all have quit. 2012-04-21 10:36:39 -07:00
Lance Stout
8ee30179ea Add _use_daemons flag to XMLStream to run all threads in daemon mode.
This WILL make the Python interpreter produce exceptions on shutdown.
2012-04-20 15:21:31 -07:00
Lance Stout
cb2469322b Handle using provided weakrefs as stanza parent references.
Fixes issue #159
2012-04-14 11:13:38 -04:00
Lance Stout
94aa6673ca Check for the stop event more aggressively in the send thread. 2012-04-13 08:27:11 -04:00
Lance Stout
4b2b2d16b8 Reset attempted SASL mech set after no suitable mechs are found. 2012-04-11 12:53:22 -04:00
Lance Stout
4cd5d3b3b5 Fix DNS resolution results for IP literals. 2012-04-10 14:08:33 -04:00
Lance Stout
e48e50c6ff Update setup.py with the latest plugins. 2012-04-09 21:45:19 -04:00
Lance Stout
01189376e2 Add initial support for XEP-0153. 2012-04-09 21:41:59 -04:00
Lance Stout
60195cf2dc Initial support for XEP-0231. 2012-04-08 23:27:19 -04:00
Lance Stout
15ef273141 Add a prefix to stanza ID values to ensure that they are unique per client. 2012-04-08 21:15:53 -04:00
Lance Stout
eed6da538a Undo the additional Iq result checks until further testing is done.
Revert "Check for Iq results based on both the sender's JID and the ID value."

This reverts commit 9ffde5ab37.
2012-04-08 16:30:52 -04:00
Lance Stout
d3e8993e22 Fix looking up local and cached vcards. 2012-04-08 16:01:47 -04:00
Lance Stout
8a8926c5e8 Fix errors in caps related to unwrapped disco data and full JIDs. 2012-04-08 16:00:36 -04:00
Lance Stout
f9d0ee824b Ensure that wrapped disco results retain requesting iq id. 2012-04-08 16:00:07 -04:00
Lance Stout
af099737ab Ensure that accessing self.api.settings works for plugins. 2012-04-08 15:59:47 -04:00
Lance Stout
9ffde5ab37 Check for Iq results based on both the sender's JID and the ID value. 2012-04-08 15:58:48 -04:00
Lance Stout
272ddf9f01 Add nickname element to the XEP-0054 plugin. 2012-04-07 21:16:36 -04:00
Lance Stout
259c84e99a Add initial XEP-0054 plugin. 2012-04-07 20:50:02 -04:00
Lance Stout
7391288668 Tidy up roster_received event and callbacks. 2012-04-07 17:30:25 -04:00
Lance Stout
7734aee7ad Prevent roster_update from firing twice after retrieving the roster. 2012-04-07 17:22:29 -04:00
Lance Stout
9f855b9679 Trigger got_online after resource information has been saved. 2012-04-07 16:23:24 -04:00
Lance Stout
aedbecd673 Correct the statemachine's ensure_any method.
It had not been updated to use the new condition instead of the old
threading event.
2012-04-06 17:39:51 -04:00
Lance Stout
83c5a4cd2f Pass JID objects to API callbacks and not strings. 2012-04-06 15:22:36 -04:00
Lance Stout
9c61c2882f Add support for XEP-0027 2012-04-06 15:22:23 -04:00
Lance Stout
e0dd9c3618 Simplify registering API handler defaults. 2012-04-06 15:09:26 -04:00
Lance Stout
4921c44d0a Don't break test plugins that use None instead of a stream object. 2012-04-06 15:09:26 -04:00
Lance Stout
3161f104c7 Update XEP-0012 plugin to use new api. 2012-04-06 15:09:26 -04:00
Lance Stout
898f5f4b51 Allow for registering a handler and setting it as default in one step. 2012-04-06 15:09:26 -04:00
Lance Stout
3ee3fdca91 Fix XEP-0115 with the new API registry. 2012-04-06 15:09:26 -04:00
Lance Stout
488f7ed886 Begin experiment with a centralized API callback registry.
The API registry generalizes the node handler system from the xep_0030
plugin so that other plugins can use it.
2012-04-06 15:09:25 -04:00
Lance Stout
51e5aee830 Add default mapping of localhost to ::1 and 127.0.0.1 2012-04-06 15:08:21 -04:00
Lance Stout
af13bea2b8 Fix MUC invite events so that they actually work. 2012-04-03 22:41:37 -07:00
Lance Stout
cdf0b353db Fix memory leak with adhoc command sessions.
Fixes issue #155
2012-04-03 11:02:55 -07:00
Lance Stout
48504ed5e2 Display IPv6 literal addresses in brackets. 2012-04-01 19:32:12 -07:00
Lance Stout
4d4d1e0ee5 Improve connection handling by not delaying until all DNS records are tried. 2012-03-30 10:12:44 -07:00
Lance Stout
c1d36cad46 Add better DNS resolver wrapper. 2012-03-30 10:12:43 -07:00
Lance Stout
aad2eb31fc Fix typo 2012-03-30 09:01:15 -07:00
Lance Stout
1bd7824f24 Tidy up the state machine and use a threading condition instead of an event.
Fixes issue #154
2012-03-28 23:58:38 -07:00
Lance Stout
912463ed6a Fix sending data after </stream>
Clearing the session_started_event before sending </stream> will
pause the send loop so that we don't continue sending data after
the </stream>.
2012-03-28 23:53:55 -07:00
Lance Stout
dda2473d35 Reset stream management state on session_end. 2012-03-27 23:27:24 -07:00
Lance Stout
94923ae898 Improve handling disconnections.
- Add option for disconnecting without sending </stream>:

    self.disconnect(send_close=False)

- Optionally distinguish between session_end and disconnected based
  on if </stream> was sent.

    self.end_session_on_disconnect = False
2012-03-27 23:24:42 -07:00
Lance Stout
f1fde07eb9 Add tests for bool_interfaces. 2012-03-27 21:16:53 -07:00
Lance Stout
a1ddd88208 Add support for a new type of stanza interface: bool
The set of bool_interfaces provides default behaviour for
checking/setting the presence of empty subelements.

The prime example of this would be:

    bool_interfaces = set(['required'])

This would mean that ``stanza['required']`` would return ``True`` for:

    <stanza>
       <required />
    </stanza>

and ``False`` for:

    <stanza />

Likewise, assigning ``stanza['required'] = True`` would add an empty
``<required />`` element, and setting it to ``False`` would remove
such an element if it exists.
2012-03-27 21:05:50 -07:00
Lance Stout
ee6a9b981a Simplify sending whitespace keepalives.
Now that we have the send lock, we can use now=True.
2012-03-27 20:53:27 -07:00
Lance Stout
9879c7af59 Make the XEP-0198 ack debug message less confusing. 2012-03-27 20:52:31 -07:00
Lance Stout
fa4c52e499 Correct handling of acks for XEP-0198 under heavy load. 2012-03-21 13:00:43 -07:00
Lance Stout
d5484808a7 Respect reattempt=False setting when reconnecting. 2012-03-21 10:28:26 -07:00
Lance Stout
1c83391948 Merge remote-tracking branch 'hansent/master' into develop 2012-03-20 11:50:57 -07:00
Lance Stout
59d1b8e131 Correct connect() documentation, don't delay attempts if reattempt=False.
See issue #152
2012-03-20 09:56:39 -07:00
Lance Stout
859822ff05 Fix unicode issues in test cases for Py3+ introduced by issue #150. 2012-03-19 14:24:45 -07:00
Lance Stout
3acc7d0914 Merge pull request #150 from correl/rpc_value_fixes
Updated XEP-0009 to handle unicode strings
2012-03-19 14:06:36 -07:00
Lance Stout
b077ef9150 Fix error in the registration example.
The now=True parameter was not being passed to allow the registration
submission to be submitted while the send queue is paused.
2012-03-19 06:05:15 -07:00
Lance Stout
e2ce5ae222 Add example for using user location.
Uses http://freegeoip.com to get an approximate location based
on the machine's IP address.
2012-03-18 23:42:03 -07:00
Lance Stout
73cabcb6ae Add initial support for XEP-0198 for stream management. 2012-03-18 01:02:19 -07:00
Lance Stout
fbdf2bed49 Add out_sync filter category.
Added option to XMLStream.send() to skip applying filters.

Filters in the out_sync group are synced with placing stanza content
either on the wire directly or into the send queue. Because of this,
out_sync filters should not block.
2012-03-18 00:59:45 -07:00
Lance Stout
33d01fb694 Fix requesting receipts on a message that has not been bound to a stream. 2012-03-16 23:42:55 -07:00
Lance Stout
ab2e43d052 Re-add support for special case of 'presence' expiry value. 2012-03-16 23:42:34 -07:00
Lance Stout
0c24fbdb06 Add pubsub examples.
Run pubsub commands via pubsub_client, and watch events as they come in
with pubsub_events.
2012-03-16 23:18:59 -07:00
Lance Stout
eb25998e72 Update subscription event expiry value to use time objects. 2012-03-16 23:16:17 -07:00
Lance Stout
eafd2aee93 Add events for configuration and subscription notifications.
New events:
    pubsub_config
    pubsub_subscription
2012-03-16 23:12:38 -07:00
Lance Stout
a6f3d740a2 Fix error when assigning form values that include booleans. 2012-03-16 22:02:21 -07:00
Lance Stout
19a6f61b44 Fix requiring receipt request messages to have ID values. 2012-03-16 22:01:56 -07:00
Lance Stout
58e0f1e6c3 Expand support for XEP-0184.
New stanza interfaces:

    Adding a message receipt request:

        msg['request_receipt'] = True

    Adding a message receipt:

        msg['receipt'] = '123-24234'

    Retrieving the acked message ID:

        ack_id = msg['receipt']
        print(ack_id)
        '123-24234'

New configuration options:

    auto_ack:
        If True, auto reply to messages that request receipts.

        Defaults to True

    auto_request:
        If True, auto add receipt requests to appropriate outgoing
        messages.

        Defaults to False
2012-03-16 10:51:25 -07:00
Lance Stout
96ff2d43c0 Explicitly set the desired SASL mech to ANONYMOUS if no username is provided. 2012-03-13 12:24:41 -07:00
Lance Stout
1b00b7e8df Correct handling SASL auth failures when forcing the use of a specific mechanism. 2012-03-13 11:07:14 -07:00
Lance Stout
7284ceb90c Move feature_rosterver to new system. 2012-03-12 20:04:11 -07:00
Lance Stout
24ec448b7f Move feature_starttls to new system. 2012-03-12 19:57:20 -07:00
Lance Stout
ed5a2f400d Move feature_session to new system. 2012-03-12 19:52:20 -07:00
Lance Stout
9596616b42 Move feature_mechanisms to new system. 2012-03-12 19:52:01 -07:00
Lance Stout
8d38fb511b Move feature_bind to new system. 2012-03-12 19:49:43 -07:00
Lance Stout
5a2cbbb731 Move XEP-0172 to new system. 2012-03-12 19:32:20 -07:00
Lance Stout
32d6f85649 Move XEP-0118 to the new system. 2012-03-12 19:32:20 -07:00
Lance Stout
a2b47e5749 Move XEP-0108 to new system. 2012-03-12 19:32:20 -07:00
Lance Stout
14d4062f4a Move XEP-0107 to new system. 2012-03-12 19:32:20 -07:00
Lance Stout
67972c5e84 Move XEP-0080 to the new system. 2012-03-12 19:32:20 -07:00
Lance Stout
3467ac18cc Move XEP-0163 to new system.
Also includes new register_pep() method for doing the necessary stanza
and disco registration, plus pubsub node event mapping.
2012-03-12 19:32:20 -07:00
Lance Stout
cabf27424f Cleanup plugin import logic.
Checking for a 'xep' or 'rfc' attribute is more reliable
for detecting an old style plugin than 'name'.
2012-03-12 19:32:20 -07:00
Lance Stout
162e955bd6 Enable using post_init() to resolve circular dependencies.
We really shouldn't have any. However, we may later introduce one
with XEP-0030 and XEP-0059.
2012-03-12 19:32:20 -07:00
Lance Stout
57d761b8a2 Move XEP-0115 to the new system. 2012-03-12 19:32:20 -07:00
Lance Stout
8b2023225c Remove extra logging statement, add backward compatible references. 2012-03-12 19:32:20 -07:00
Lance Stout
f8f2b541db Handle loading plugins on demand.
Plugins that are referenced as dependencies, but have not been
registered now will be imported. Newer plugins should register
themselves automatically, but older style plugins will be
explicitly registered after import.
2012-03-12 19:32:20 -07:00
Lance Stout
9d645ad5cd Update the list of all stream feature plugins. 2012-03-12 19:32:20 -07:00
Lance Stout
610d366bdb Ensure the adhoc command items node exists.
If the plugin is loaded and no commands are defined, we can at least
return a proper empty response instead of an item-not-found error.
2012-03-12 19:32:20 -07:00
Lance Stout
64c46562d3 Move XEP-0249 to the new system. 2012-03-12 19:32:20 -07:00
Lance Stout
87d6ade06d Move XEP-0224 to new system. 2012-03-12 19:32:20 -07:00
Lance Stout
4a009515c1 Move XEP-0203 to new system. 2012-03-12 19:32:20 -07:00
Lance Stout
6497857495 Move XEP-0202 to new system. 2012-03-12 19:32:20 -07:00
Lance Stout
5a324c01de Move XEP-0199 to new system. 2012-03-12 19:32:19 -07:00
Lance Stout
17279de4a3 Move XEP-0184 to new system. 2012-03-12 19:32:19 -07:00
Lance Stout
34a7a62c35 Move XEP-0128 to new system. 2012-03-12 19:32:19 -07:00
Lance Stout
2305cc61fd Move XEP-0092 to new system. 2012-03-12 19:32:19 -07:00
Lance Stout
2f677c98f8 Move XEP-0086 to new system. 2012-03-12 19:32:19 -07:00
Lance Stout
3fda053606 Move XEP-0085 to the new system.
Optimized handlers so that only one is needed.
2012-03-12 19:32:19 -07:00
Lance Stout
6d855ec06c Move XEP-0082 to new system. 2012-03-12 19:32:19 -07:00
Lance Stout
23cc62fe7c Move XEP-0078 to new system. 2012-03-12 19:32:19 -07:00
Lance Stout
26ea67d211 Move XEP-0045 to new system.
Still needs updating to the new format.
2012-03-12 19:32:19 -07:00
Lance Stout
d43cd9fa54 Move XEP-0033 to new system.
Still needs updating to the new format.
2012-03-12 19:32:19 -07:00
Lance Stout
6f337b5425 Move XEP-0012 to new system.
Still needs to update to the current plugin format though.
2012-03-12 19:32:19 -07:00
Lance Stout
d104a5fe75 Move XEP-0009 to new system. 2012-03-12 19:32:19 -07:00
Lance Stout
cdd69c6842 Move XEP-0077 to the new system. 2012-03-12 19:32:19 -07:00
Lance Stout
4a3a9067d4 Move XEP-0066 to new system. 2012-03-12 19:32:19 -07:00
Lance Stout
1aecb2293a Move XEP-0060 to new system. 2012-03-12 19:32:19 -07:00
Lance Stout
ad8fd91b7a Move XEP-0050 to new system. 2012-03-12 19:32:19 -07:00
Lance Stout
1f5a3a4445 Move XEP-0047 to new system. 2012-03-12 19:32:19 -07:00
Lance Stout
be363e0b46 Move XEP-0004 to new system. 2012-03-12 19:32:19 -07:00
Lance Stout
a104cd6dae Tidy up disco plugin. 2012-03-12 19:32:19 -07:00
Lance Stout
e287282782 Moving backwards compatibility shims to __init__ files. 2012-03-12 19:32:07 -07:00
Lance Stout
8b06d10415 Update XEP-0030 and XEP-0059 to new system. 2012-03-12 16:24:18 -07:00
Lance Stout
1a153487c3 Add tests for new plugin manager. 2012-03-12 16:24:18 -07:00
Lance Stout
01b2499915 Introduce new plugin system.
The new system is backward compatible and will load older style plugins.

The new plugin framework allows plugins to track their dependencies, and
will auto-enable plugins as needed.

Dependencies are tracked via a class-level set named `dependencies` in
each plugin.

Plugin names are no longer tightly coupled with the plugin class name,
Pso EP8 style class names may be used.

Disabling plugins is now allowed, but ensuring proper cleanup is left to
the plugin implementation.

The use of a `post_init()` method is no longer needed for new style
plugins, but plugins following the old style will still require a
`post_init()` method.
2012-03-12 16:24:18 -07:00
Lance Stout
9f43d31bf5 Add setting for maximum number of reconnection attempts.
Setting self.reconnect_max_attempts to a non-None value will limit
the number of times a connection attempt will be made before quiting
and raising a 'connection_failed' event.
2012-03-12 16:19:18 -07:00
Lance Stout
a318beded4 Update plugin list and use correct names. 2012-03-11 16:34:41 -07:00
Lance Stout
5f4b528e6b Ensure that result stanzas are returned, as expected. 2012-03-11 16:13:19 -07:00
Lance Stout
f759b0ada1 Add support for XEP-0108: User Activity. 2012-03-11 12:37:54 -07:00
Lance Stout
7d89fa27a8 Expand support of XEP-0172 (user nickname) to include PEP. 2012-03-11 00:22:28 -08:00
Lance Stout
10ec92f7c6 Add support for XEP-0107, User Mood. 2012-03-10 23:32:20 -08:00
Lance Stout
58d2f317a0 Fix plugin loading logs for XEP-0118 and XEP-0163. 2012-03-10 23:31:54 -08:00
Lance Stout
34b094561f Add support for XEP-0080. 2012-03-10 12:54:31 -08:00
Lance Stout
91155444c0 Resolve plugin dependency chains with XEP-0115.
The post_init() system can only reliably handle a single layer
of dependencies between plugins, but PEP plugins with XEP-0115
exceed that limit and plugins can be post_init'ed out of order. To
resolve this, we will special case XEP-0115 to be post_init'ed
first until the new plugin system with dependency tracking is
stable.
2012-03-10 12:48:35 -08:00
Lance Stout
7f71ac7e0a Add user tune feature to disco, not just notifications. 2012-03-10 10:54:24 -08:00
Lance Stout
e5fc59a4c6 Ensure post init works for XEP-0118. 2012-03-10 10:44:53 -08:00
Lance Stout
549a9ab472 Add support for XEP-0118.
See examples/user_tune.py for a demonstration using the currently
playing song in iTunes.
2012-03-10 10:30:32 -08:00
Lance Stout
09720dcf42 Fix XEP-0163's updating of caps. 2012-03-10 10:20:06 -08:00
Lance Stout
ec044affd4 Only auto-broadcast caps changes after a session has started. 2012-03-10 10:19:43 -08:00
Lance Stout
af39945009 Add XEP-0163 plugin.
This is just a very simple wrapper for XEP-0030, XEP-0115, and XEP-0060
for adding interests to caps information, and publishing.
2012-03-10 09:23:47 -08:00
Lance Stout
78a50d0237 Add support for pubsub notification events.
Publishes, retractions, purges, and deletions now raise the events:

- pubsub_publish
- pubsub_retract
- pubsub_purge
- pubsub_delete

In addition, custom events may be raised based on the node that
generated the notification. For example:

xmpp['xep_0060'].map_node_event('http://jabber.org/protocol/tune',
                                'user_tune')

will allow for using the events:

- user_tune_publish
- user_tune_retract
- user_tune_purge
- user_tune_delete
2012-03-10 00:07:56 -08:00
Lance Stout
861d279b08 Correct missing pubsub#event stanzas and interfaces. 2012-03-10 00:07:15 -08:00
Lance Stout
eb1a32fc90 Fix setup.py to include the rosterver stream feature plugin. 2012-03-08 16:21:22 -08:00
Lance Stout
4610a6615c Add tests for roster versioning. 2012-03-07 16:11:59 -08:00
Lance Stout
4cb8a8d389 Modify the cert event to provide the PEM encoded cert in all cases. 2012-03-07 15:03:35 -08:00
Lance Stout
a71823dc04 Add support for roster versioning.
This was XEP-0237, but is now part of RFC 6121.

Roster backends should now expose two additional methods:

version(jid):
    Return the version of the given JID's roster.
set_version(jid, version):
    Update the version of the given JID's roster.

A new state field will be passed to the backend if an item
has been marked for removal. This is 'removed' which will
be set to True.
2012-03-07 14:55:27 -08:00
Lance Stout
d41ada6b66 Cleanup logging when loading a custom plugin. 2012-03-05 11:30:36 -08:00
Lance Stout
fdfe2cd64f Propagate save option when setting a roster backend. 2012-03-05 11:28:10 -08:00
Lance Stout
7b51c6f5cc Save existing roster content when setting a new backend. 2012-03-05 11:12:13 -08:00
Lance Stout
be7f07ad12 Prevent excess loading from the roster db.
Fixes issue #148
2012-03-05 11:11:35 -08:00
Lance Stout
830db11b41 Ensure that roster nodes aren't empty strings.
This would happen when receiving presence without a 'to' field, which
happens when receiving presence from other resources for the same account.
2012-03-05 11:08:57 -08:00
Lance Stout
53bcd33e1d Let disconnect() wait for its lock for a few seconds.
This should eliminate most debug statements about not being
able to acquire a lock during disconnect.
2012-02-22 07:57:13 -08:00
Lance Stout
e3d596c9fa Update XEP-0085 plugin to work with both ElementTree and cElementTree
Each state element must have its own stanza class now. A stanza class
with an empty name field causes errors in ElementTree, even though
it works fine with cElementTree.
2012-02-19 20:28:31 -08:00
Lance Stout
ecd6ad6930 Fix incompatibility with clearing an element between ElementTree and cElementTree 2012-02-19 20:27:53 -08:00
Lance Stout
c36073b40e xml.etree.ElementTree raises ExpatError instead of SyntaxError or ParseError. 2012-02-19 20:27:19 -08:00
Lance Stout
afe0d16797 Centralize references to ET to make switching implementations easier. 2012-02-19 20:26:40 -08:00
Lance Stout
977fcc0632 Fix instances of using undefined variables. 2012-02-18 11:56:10 -08:00
Lance Stout
94b57d232d More pyflakes cleanup. 2012-02-18 11:44:05 -08:00
Lance Stout
7cdedb2ec0 More import cleanup based on pyflakes. 2012-02-18 11:40:34 -08:00
Lance Stout
676324805e Use JID objects when dealing with roster items. 2012-02-18 11:39:47 -08:00
Lance Stout
7d74a7b027 More extraneous import cleanup. 2012-02-17 14:59:56 -08:00
Lance Stout
9d5eb864d1 More import cleanups based on pyflakes results. 2012-02-17 14:41:31 -08:00
Lance Stout
86a482e032 Fix pyflakes complaints for XEP-0115 plugin. 2012-02-17 11:40:51 -08:00
Lance Stout
c43c7be86c Make last_xml usage a little more explict. 2012-02-17 11:40:07 -08:00
Lance Stout
c58462f154 Fix undeclared variable usage for reconnect. 2012-02-17 11:12:48 -08:00
Correl Roush
31d3e3b2b6 Updated XEP-0009 to handle unicode strings 2012-02-17 12:24:44 -05:00
Lance Stout
fb2582e53b Fix fixing remove_stanza()
Fixes issue #146
2012-02-16 07:25:44 -08:00
Lance Stout
d807613117 Don't retrieve cert until a connection is made. 2012-02-16 07:02:56 -08:00
Lance Stout
6d922d00c3 Fix remove_stanza().
Fixes issue #146
2012-02-16 07:02:19 -08:00
Lance Stout
61ea84093b Don't shutdown completely after handling SyntaxError.
The ``shutdown = True`` line was preventing the stream from reconnecting
after handling the error.

Fixes issues #101, #145
2012-02-10 19:28:12 -08:00
Lance Stout
e76d6a481f Fix undefined variable references when DNS timeouts. 2012-02-10 19:20:17 -08:00
Lance Stout
c1357717d9 Use '=' as base64 value for empty string SASL results. 2012-02-09 22:01:11 -08:00
Lance Stout
ca5145c210 Fix IPv6 query logging. 2012-02-10 06:46:51 +01:00
Lance Stout
1a272fd276 Add support for querying and connecting to IPv6 addresses.
Tested using servers provided by Florian Jensen (flosoft.biz)
during the 2012 FOSDEM XMPP Summit.

Fixes issue #94.
2012-02-09 21:28:28 -08:00
Lance Stout
952260b423 Add ssl_cert event (direct).
The payload is a dictionary of parsed cert data, as provided by
Python's getpeercert() socket method. It unfortunately does not
provide much detail beyond basic info.
2012-02-04 14:16:37 +01:00
Lance Stout
caa967105c Add more XEP-0047 tests. 2012-02-03 16:29:38 +01:00
Lance Stout
d565e4be20 Fix XEP-0184 imports 2012-02-03 16:08:27 +01:00
Lance Stout
85dd005abc Fix infinite callback loop. 2012-02-03 16:03:46 +01:00
Lance Stout
021c57205f Don't assume data is ASCII in saslprep. 2012-02-03 16:01:54 +01:00
Lance Stout
261a501afc Merge remote-tracking branch 'whooo/master' into develop 2012-02-03 15:23:01 +01:00
Erik Larsson
9a38a101d2 Added fritzy to the copyright for xep_0184 2012-02-03 15:17:01 +01:00
Lance Stout
4665c5cf1a Fix data stanza based on test results. 2012-02-02 19:19:50 +01:00
Lance Stout
bd52a5e6c1 Initial, mostly working XEP-0047 plugin.
This is inspired by the version from macdiesel and tomstrummer, but
their version was heavily linked with XEP-0096 and focused solely
on file transfer. This version is a more generic implementation.
2012-02-02 18:27:23 +01:00
Lance Stout
f98e5a03de Fix typo s/is_set/is_set() 2012-02-02 18:14:48 +01:00
Erik Larsson
2217c69757 Added plugin for XEP-0184 2012-02-02 14:29:27 +01:00
Thomas Hansen
5a4df56836 examples: fix rpc examples. __init__ method was wrongly named "__init" causing proxy and handler class to not be initialized. 2012-01-31 12:38:41 -06:00
Lance Stout
3ab7c8bcc3 Make socket_error run as a direct event to ensure that it is handled.
Socket errors that occur before stream processing begins could not be
handled as the event loop would not be running yet.

Resolves issue #142
2012-01-28 18:54:46 -08:00
Lance Stout
8f25acd0f3 Bump version number in develop branch to 1.0.1dev. 2012-01-25 20:44:41 -08:00
Lance Stout
999f1932cc Merge pull request #138 from rhcarvalho/patch-2
Set default argument value.
2012-01-25 20:41:45 -08:00
Lance Stout
69940a8ab9 Fix a few typos. 2012-01-24 00:07:31 -08:00
Lance Stout
13158e3cdf Revert the X-GOOGLE-TOKEN mech to not perform HTTP requests.
Added new example for how to retrieve a Google token, following
the best case, non-browser, workflow. Other thirdparty auth
mechs (Facebook, MSN) follow a similar pattern of using an
access token.
2012-01-23 23:58:40 -08:00
Lance Stout
f06589c913 Merge pull request #140 from rhcarvalho/patch-3
Fix ValueError when line has more than one '='.
2012-01-23 10:53:19 -08:00
Rodolfo Carvalho
2735b680b9 Fix ValueError when line has more than one '='. 2012-01-22 18:32:32 -02:00
Rodolfo Henrique Carvalho
5f1d4ce433 Set default argument value.
Without this features/feature_mechanisms/mechanisms.py throws an error when calling the method `process' without arguments on this mechanism.
2012-01-22 01:53:07 -02:00
Lance Stout
25f87607aa Add support for X-GOOGLE-TOKEN.
This is mainly just useful for authenticating without using TLS.

If an access token is not provided, an attempt will be made to
retrieve one from Google.
2012-01-21 00:44:03 -08:00
Lance Stout
f81fb6af44 Require explicitly setting access_token value.
Silently substituting the password field was nice, but for mechs
that can use either the password or an access token, it makes
things very difficult. This really only affects MSN clients since
Facebook clients should already be setting the api key.
2012-01-21 00:19:59 -08:00
Lance Stout
bb0a5186d6 Handle SASLCancelled and SASLError exceptions. 2012-01-21 00:19:08 -08:00
Lance Stout
baad907422 Add missing SASL <abort /> stanza 2012-01-21 00:17:49 -08:00
Lance Stout
1022fc0060 Make things work with Python3's byte semantics. 2012-01-20 02:27:30 -08:00
Lance Stout
3a22d798f8 Allow attempting multiple SASL mechs during a single stream.
Instead of disconnecting when the first chosen mech fails, we will
try all of them once.
2012-01-20 02:01:08 -08:00
Lance Stout
71ea430c62 Add support for X-FACEBOOK-PLATFORM SASL mechanism.
This requires an extra credential for SASL authentication:

xmpp = ClientXMPP('user@chat.facebook.com', '...access_token...')
xmpp.credentials['api_key'] = '...api_key...'
2012-01-20 01:24:05 -08:00
Lance Stout
0d2125e737 Add an extra config dict to store SASL credentials.
We'll need extra things beyond just a password, such as api_key.
2012-01-20 01:08:25 -08:00
Lance Stout
02f4006153 Add basic start for a client side XEP-0077 plugin. 2012-01-19 02:37:36 -08:00
Lance Stout
b25668b5b7 Fix detecting end of result set paging. 2012-01-18 19:57:49 -08:00
Lance Stout
bb3080e829 Merge branch 'docs' into develop
Conflicts:
	docs/index.rst
2012-01-18 15:36:18 -08:00
Lance Stout
bd85e95398 Gah, too many branch conflicts. 2012-01-18 15:34:49 -08:00
Lance Stout
22cc194ed8 Merge branch 'docs' of github.com:fritzy/SleekXMPP into docs
Conflicts:
	docs/index.rst
2012-01-18 15:34:12 -08:00
Lance Stout
79b71228c1 Fix some more merge conflicts. 2012-01-18 15:31:45 -08:00
Lance Stout
fd515d807c Add example of accessing plugins to the README. 2012-01-18 15:22:19 -08:00
Lance Stout
4f4c121d9b Fix merge errors and bot example. 2012-01-18 15:16:56 -08:00
Lance Stout
72e1ab47fc Merge branch 'docs' into develop
Conflicts:
	docs/_static/haiku.css
	docs/_static/header.png
	docs/conf.py
	docs/getting_started/muc.rst
	docs/index.rst
	examples/muc.py
2012-01-18 15:04:33 -08:00
Lance Stout
3575084640 Update home page to include bot example, and example of using a plugin. 2012-01-18 14:24:23 -08:00
Lance Stout
1e01903072 Revert "Remove stream feature handlers on session_start."
This reverts commit 4274f49ada.

The SASL mech was choking on this, so let's send it back for some
more refining.
2012-01-18 11:51:00 -08:00
Lance Stout
3672856ab4 Fix roster key issue for non-JID keys. 2012-01-17 23:10:14 -08:00
Lance Stout
86d8736dcc Hash JIDs based on full JID string.
This makes JID objects equivalent to strings in dictionaries, etc.

>>> j = JID('foo@example.com')
>>> s = 'foo@example.com'
>>> d = {j: 'yay'}
>>> d[j]
'yay'
>>> d[s]
'yay'
>>> d[s] = 'yay!!'
>>> d[j]
'yay!!'
2012-01-17 23:03:48 -08:00
Lance Stout
2923f56561 Pre-parse StanzaPath paths to speed up matching.
The parsing and namespace cleaning isn't terribly expensive, but it does
add up. It was adding an extra 5sec when processing 100,000 basic
message stanzas.
2012-01-17 22:28:44 -08:00
Lance Stout
4274f49ada Remove stream feature handlers on session_start.
Based on profiling, using around 35 stream handlers quarters the number
of basic message stanzas that can be processed in a second, in
comparison to only using the bare minimum of four handlers.

To help, we can drop handlers for stream features once the session
has started. So that we can re-enable these handlers when a stream
must restart, the 'stream_start' event has been added which fires
whenever a stream header is received.

The 'stream_start' event is a more generic replacement for the
existing start_stream_handler() method.
2012-01-17 22:14:24 -08:00
Lance Stout
a4b27ff031 Merge pull request #137 from rhcarvalho/patch-1
Use jid.bare as a key instead of a JID instance.
2012-01-16 11:44:42 -08:00
Rodolfo Henrique Carvalho
f49b6fa79f Use jid.bare as a key instead of a JID instance. 2012-01-16 16:59:45 -02:00
Lance Stout
7b854a190e Tidy up and update the plugin __init__ file. 2012-01-15 22:51:59 -08:00
Lance Stout
947d1ffbb3 Fix xep_0030 reference warning. 2012-01-14 17:12:39 -08:00
Lance Stout
de35848500 Don't serialize XML unless we need to. 2012-01-14 10:54:48 -08:00
Lance Stout
1ae219025a Don't dump exception logs for XML stream parsing errors.
The exceptions are handled, so we don't need to clutter the output logs.
2012-01-12 22:26:15 -08:00
Lance Stout
e8b2dd6698 Update Roster stanza to use RosterItem substanzas.
get_roster() now returns the Iq result stanza instead of True (stanzas
also evaluate to True).
2012-01-12 17:21:43 -08:00
Lance Stout
c0074f95b1 update_caps() can now do presence broadcasting.
As part of adding this feature:

    - fixed bug in update_caps() not assigning verstrings
    - fixed xep_0004 typo
    - can now use None as a roster key which will map to boundjid.bare
    - fixed using JID objects in disco node handlers
    - fixed failing test related to get_roster

Several of these bugs I've fixed before, so I either didn't push them
earlier, or I clobbered something when merging. *shrug*
2012-01-11 16:39:55 -08:00
Lance Stout
a79ce1c35e Merge branch 'develop' of github.com:fritzy/SleekXMPP into develop 2012-01-10 20:05:25 -08:00
Lance Stout
1eb69f7075 Make the roster easier to inspect.
The __repr__ version now looks like a regular dictionary.
2012-01-10 20:03:22 -08:00
Lance Stout
a86935a42f Make get_roster(block=False) work properly.
Fixes issue #136
2012-01-10 19:57:38 -08:00
Lance Stout
1674bd753e Fix setup.py Unicode issue with README.rst
Fixes issue #135
2012-01-09 15:38:19 -08:00
Lance Stout
6b9a55e62d Sync with Suelta. 2012-01-07 00:19:08 -05:00
Lance Stout
c578ddeb1a Add support for MSN with X-MESSENGER-OAUTH2 SASL support.
NOTE: This requires already having the access token. It does NOT
perform any OAuth requests.
2012-01-06 23:31:58 -05:00
Lance Stout
8ef7188dae Fix client_roster when the bare JID changes after binding.
Adds session_bind event.
2012-01-06 23:30:14 -05:00
Lance Stout
738ec92b8e Fix a few typos. 2012-01-05 13:11:42 -05:00
Lance Stout
be9e26b4a3 Apply Te-Je's MUC guide patch. 2012-01-05 13:09:20 -05:00
Lance Stout
b345c227b2 More &yet branding 2012-01-05 13:07:44 -05:00
Lance Stout
c7e95c8dec Add &yet contact info 2012-01-05 12:28:30 -05:00
Lance Stout
3a4e3d3f51 Update doc settings to new theme, add examples, use 1.0 2012-01-05 12:13:06 -05:00
Lance Stout
8fd2efa2fa Merge branch 'develop-1.1' into develop 2012-01-05 11:33:47 -05:00
Lance Stout
97378998a5 Break the docs out into their own branch. 2012-01-05 11:31:54 -05:00
Lance Stout
6b6995bb0b Merge branch 'develop' into develop-1.1 2011-12-31 21:17:01 -05:00
Lance Stout
27c658922e Fix handing caps in Python3, allow update_caps() call before process() 2011-12-31 21:15:40 -05:00
Lance Stout
35954cdc90 Fix a few holes in caps.
Protip: Don't test using a custom disco handler that always returns the
same feature set :p
2011-12-31 19:18:00 -05:00
Lance Stout
fa912aeb84 Merge branch 'develop' into develop-1.1 2011-12-31 12:34:17 -05:00
Lance Stout
9a5e2ae768 Merge branch 'develop' into develop-1.1 2011-12-31 02:16:08 -05:00
Lance Stout
e0545bf0bc Merge branch 'develop' into develop-1.1 2011-12-31 01:29:12 -05:00
Lance Stout
d817d64c65 Enable caps stream feature. 2011-12-30 22:34:57 -05:00
Lance Stout
8a29ec67ac Add XEP-0115 plugin.
Finally
2011-12-30 21:45:25 -05:00
Lance Stout
6722b0224a Add option to disable condensing and converting form values.
XEP-0115 needs to use the raw XML character data.
2011-12-30 21:43:39 -05:00
Lance Stout
8eb225bdec Add option for disabling identity and feature deduplication.
XEP-0115 requires detecting duplicates, so we can't always silently
ignore them.
2011-12-30 20:53:18 -05:00
Lance Stout
a7df76a275 Add 'supports' and 'has_identity' node handlers. 2011-12-30 20:52:44 -05:00
Lance Stout
efae8f3369 Automatically use local disco based on the JID. 2011-12-30 20:51:41 -05:00
Lance Stout
a11e6c0b77 Be more lenient on required arguments to disco node handlers. 2011-12-30 20:51:02 -05:00
Lance Stout
1bb0b38868 Make the disco logs nicer. 2011-12-30 20:50:15 -05:00
Lance Stout
4df1641689 Add set_info disco handler. 2011-12-28 11:46:13 -05:00
Lance Stout
5ef0b96d5c Fix caching for clients. 2011-12-28 11:37:05 -05:00
Lance Stout
d979b5f2b9 Add caching support to xep_0030.
New plugin configuration options:

    use_cache    - Enable caching disco info results. Defaults to True
    wrap_results - Always return disco results in an Iq stanza. Defaults
                   to False

Node handler changes:

    Handlers now take four arguments: jid, node, ifrom, data

    Most older style handlers will still work, depending on if they
    raise a TypeError for incorrect number of arguments. Handlers that
    used *args may not work.

New get_info options:

    cached - Passing cached=True to get_info() will attempt to load
             results from the cache. If nothing is found, a query
             will be sent as normal. If set to False, the cache
             will be skipped, even if it contains results.

New method:

    supports() - Given a JID/node pair and a feature, return True
                 if the feature is supported, False if not, and
                 None if there was a timeout. By default, the search
                 will use the cache.
2011-12-28 10:16:31 -05:00
Lance Stout
1a61bdb302 Ensure that stanza plugins work as expected if the XML is appended. 2011-12-28 09:53:22 -05:00
Lance Stout
e8545dd2bc Merge branch 'develop-1.1' of github.com:fritzy/SleekXMPP into develop-1.1 2011-12-27 18:05:55 -05:00
Lance Stout
2f2ebb37e4 Merge branch 'develop' into develop-1.1 2011-12-27 18:05:42 -05:00
Lance Stout
8f9d1bcfe0 Merge branch 'develop-1.1' of github.com:fritzy/SleekXMPP into develop-1.1 2011-12-15 12:04:14 -08:00
Lance Stout
a7a2fd1d5b Merge branch 'develop' into develop-1.1 2011-12-15 12:03:52 -08:00
Lance Stout
f6e30edbc4 Log received data AFTER filtering.
This allows applications to filter out sensitive information, such
as passwords, so that it won't appear in the logs.

It does mean that the debug logs will not show the actual received
data, and there will be no indication of tampering, unless the
filter author explicitly logs and notes that a change was made.
2011-12-14 21:14:27 -08:00
Lance Stout
45ed68006f Add tests for filters. 2011-12-13 20:05:31 -08:00
Lance Stout
dcb0d8b00e Merge branch 'develop' into develop-1.1 2011-12-13 09:38:27 -08:00
Lance Stout
cb635dcd5a Add parameter docs for add_filter. 2011-12-12 22:37:19 -08:00
Lance Stout
eff3330e75 Add support for incoming/outgoing filters.
A filter accepts and returns a stanza, but potentially modified.

To prevent sending/receiving a stanza, a filter may return None.

Incoming:
    self.add_filter('in', in_filter)

Outgoing:
    self.add_filter('out', out_filter)

Filters are applied in the order thay are added. However, you may
add an order parameter, which is the place in the list to insert the
filter:

    self.add_filter('in', in_filter, order=0)
2011-12-12 22:17:07 -08:00
262 changed files with 16751 additions and 2946 deletions

1
.gitignore vendored
View File

@@ -6,3 +6,4 @@ docs/_build/
*.swp
.tox/
.coverage
sleekxmpp.egg-info/

29
LICENSE
View File

@@ -138,3 +138,32 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
python-gnupg: A Python wrapper for the GNU Privacy Guard
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Copyright (c) 2008-2012 by Vinay Sajip.
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* The name(s) of the copyright holder(s) may not be used to endorse or
promote products derived from this software without specific prior
written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER(S) "AS IS" AND ANY EXPRESS OR
IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
EVENT SHALL THE COPYRIGHT HOLDER(S) BE LIABLE FOR ANY DIRECT, INDIRECT,
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@@ -45,21 +45,11 @@ The latest source code for SleekXMPP may be found on `Github
``develop`` branch.
**Latest Release**
- `1.0 <http://github.com/fritzy/SleekXMPP/zipball/1.0>`_
- `1.1.10 <http://github.com/fritzy/SleekXMPP/zipball/1.1.10>`_
**Develop Releases**
- `Latest Develop Version <http://github.com/fritzy/SleekXMPP/zipball/develop>`_
**Older Stable Releases**
- `1.0 RC3 <http://github.com/fritzy/SleekXMPP/zipball/1.0-RC3>`_
- `1.0 RC2 <http://github.com/fritzy/SleekXMPP/zipball/1.0-RC2>`_
- `1.0 RC1 <http://github.com/fritzy/SleekXMPP/zipball/1.0-RC1>`_
- `1.0 Beta6.1 <http://github.com/fritzy/SleekXMPP/zipball/1.0-Beta6.1>`_
- `1.0 Beta5 <http://github.com/fritzy/SleekXMPP/zipball/1.0-Beta5>`_
- `1.0 Beta4 <http://github.com/fritzy/SleekXMPP/zipball/1.0-Beta4>`_
- `1.0 Beta3 <http://github.com/fritzy/SleekXMPP/zipball/1.0-Beta3>`_
- `1.0 Beta2 <http://github.com/fritzy/SleekXMPP/zipball/1.0-Beta2>`_
- `1.0 Beta1 <http://github.com/fritzy/SleekXMPP/zipball/1.0-Beta1>`_
Installing DNSPython
---------------------
@@ -82,6 +72,7 @@ help with SleekXMPP.
**Chat**
`sleek@conference.jabber.org <xmpp:sleek@conference.jabber.org?join>`_
Documentation and Testing
-------------------------
Documentation can be found both inline in the code, and as a Sphinx project in ``/docs``.
@@ -118,8 +109,12 @@ SleekXMPP projects::
self.add_event_handler("session_start", self.session_start)
self.add_event_handler("message", self.message)
self.register_plugin('xep_0030') # Service Discovery
self.register_plugin('xep_0199') # XMPP Ping
# If you wanted more functionality, here's how to register plugins:
# self.register_plugin('xep_0030') # Service Discovery
# self.register_plugin('xep_0199') # XMPP Ping
# Here's how to access plugins once you've registered them:
# self['xep_0030'].add_feature('echo_demo')
# If you are working with an OpenFire server, you will
# need to use a different SSL version:
@@ -128,18 +123,20 @@ SleekXMPP projects::
def session_start(self, event):
self.send_presence()
self.get_roster()
# Most get_*/set_* methods from plugins use Iq stanzas, which
# can generate IqError and IqTimeout exceptions
try:
self.get_roster()
except IqError as err:
logging.error('There was an error getting the roster')
logging.error(err.iq['error']['condition'])
self.disconnect()
except IqTimeout:
logging.error('Server is taking too long to respond')
self.disconnect()
#
# try:
# self.get_roster()
# except IqError as err:
# logging.error('There was an error getting the roster')
# logging.error(err.iq['error']['condition'])
# self.disconnect()
# except IqTimeout:
# logging.error('Server is taking too long to respond')
# self.disconnect()
def message(self, msg):
if msg['type'] in ('chat', 'normal'):

View File

@@ -1,171 +0,0 @@
import logging
import sleekxmpp
from optparse import OptionParser
from xml.etree import cElementTree as ET
import os
import time
import sys
import unittest
import sleekxmpp.plugins.xep_0004
from sleekxmpp.xmlstream.matcher.stanzapath import StanzaPath
from sleekxmpp.xmlstream.handler.waiter import Waiter
try:
import configparser
except ImportError:
import ConfigParser as configparser
try:
import queue
except ImportError:
import Queue as queue
class TestClient(sleekxmpp.ClientXMPP):
def __init__(self, jid, password):
sleekxmpp.ClientXMPP.__init__(self, jid, password)
self.add_event_handler("session_start", self.start)
#self.add_event_handler("message", self.message)
self.waitforstart = queue.Queue()
def start(self, event):
self.getRoster()
self.sendPresence()
self.waitforstart.put(True)
class TestPubsubServer(unittest.TestCase):
statev = {}
def __init__(self, *args, **kwargs):
unittest.TestCase.__init__(self, *args, **kwargs)
def setUp(self):
pass
def test001getdefaultconfig(self):
"""Get the default node config"""
self.xmpp1['xep_0060'].deleteNode(self.pshost, 'testnode2')
self.xmpp1['xep_0060'].deleteNode(self.pshost, 'testnode3')
self.xmpp1['xep_0060'].deleteNode(self.pshost, 'testnode4')
self.xmpp1['xep_0060'].deleteNode(self.pshost, 'testnode5')
result = self.xmpp1['xep_0060'].getNodeConfig(self.pshost)
self.statev['defaultconfig'] = result
self.failUnless(isinstance(result, sleekxmpp.plugins.xep_0004.Form))
def test002createdefaultnode(self):
"""Create a node without config"""
self.failUnless(self.xmpp1['xep_0060'].create_node(self.pshost, 'testnode1'))
def test003deletenode(self):
"""Delete recently created node"""
self.failUnless(self.xmpp1['xep_0060'].deleteNode(self.pshost, 'testnode1'))
def test004createnode(self):
"""Create a node with a config"""
self.statev['defaultconfig'].field['pubsub#access_model'].setValue('open')
self.statev['defaultconfig'].field['pubsub#notify_retract'].setValue(True)
self.statev['defaultconfig'].field['pubsub#persist_items'].setValue(True)
self.statev['defaultconfig'].field['pubsub#presence_based_delivery'].setValue(True)
p = self.xmpp2.Presence()
p['to'] = self.pshost
p.send()
self.failUnless(self.xmpp1['xep_0060'].create_node(self.pshost, 'testnode2', self.statev['defaultconfig'], ntype='job'))
def test005reconfigure(self):
"""Retrieving node config and reconfiguring"""
nconfig = self.xmpp1['xep_0060'].getNodeConfig(self.pshost, 'testnode2')
self.failUnless(nconfig, "No configuration returned")
#print("\n%s ==\n %s" % (nconfig.getValues(), self.statev['defaultconfig'].getValues()))
self.failUnless(nconfig.getValues() == self.statev['defaultconfig'].getValues(), "Configuration does not match")
self.failUnless(self.xmpp1['xep_0060'].setNodeConfig(self.pshost, 'testnode2', nconfig))
def test006subscribetonode(self):
"""Subscribe to node from account 2"""
self.failUnless(self.xmpp2['xep_0060'].subscribe(self.pshost, "testnode2"))
def test007publishitem(self):
"""Publishing item"""
item = ET.Element('{http://netflint.net/protocol/test}test')
w = Waiter('wait publish', StanzaPath('message/pubsub_event/items'))
self.xmpp2.registerHandler(w)
#result = self.xmpp1['xep_0060'].setItem(self.pshost, "testnode2", (('test1', item),))
result = self.xmpp1['jobs'].createJob(self.pshost, "testnode2", 'test1', item)
msg = w.wait(5) # got to get a result in 5 seconds
self.failUnless(msg != False, "Account #2 did not get message event")
#result = self.xmpp1['xep_0060'].setItem(self.pshost, "testnode2", (('test2', item),))
result = self.xmpp1['jobs'].createJob(self.pshost, "testnode2", 'test2', item)
w = Waiter('wait publish2', StanzaPath('message/pubsub_event/items'))
self.xmpp2.registerHandler(w)
self.xmpp2['jobs'].claimJob(self.pshost, 'testnode2', 'test1')
msg = w.wait(5) # got to get a result in 5 seconds
self.xmpp2['jobs'].claimJob(self.pshost, 'testnode2', 'test2')
self.xmpp2['jobs'].finishJob(self.pshost, 'testnode2', 'test1')
self.xmpp2['jobs'].finishJob(self.pshost, 'testnode2', 'test2')
print result
#need to add check for update
def test900cleanup(self):
"Cleaning up"
#self.failUnless(self.xmpp1['xep_0060'].deleteNode(self.pshost, 'testnode2'), "Could not delete test node.")
time.sleep(10)
if __name__ == '__main__':
#parse command line arguements
optp = OptionParser()
optp.add_option('-q','--quiet', help='set logging to ERROR', action='store_const', dest='loglevel', const=logging.ERROR, default=logging.INFO)
optp.add_option('-d','--debug', help='set logging to DEBUG', action='store_const', dest='loglevel', const=logging.DEBUG, default=logging.INFO)
optp.add_option('-v','--verbose', help='set logging to COMM', action='store_const', dest='loglevel', const=5, default=logging.INFO)
optp.add_option("-c","--config", dest="configfile", default="config.xml", help="set config file to use")
optp.add_option("-n","--nodenum", dest="nodenum", default="1", help="set node number to use")
optp.add_option("-p","--pubsub", dest="pubsub", default="1", help="set pubsub host to use")
opts,args = optp.parse_args()
logging.basicConfig(level=opts.loglevel, format='%(levelname)-8s %(message)s')
#load xml config
logging.info("Loading config file: %s" , opts.configfile)
config = configparser.RawConfigParser()
config.read(opts.configfile)
#init
logging.info("Account 1 is %s" , config.get('account1', 'jid'))
xmpp1 = TestClient(config.get('account1','jid'), config.get('account1','pass'))
logging.info("Account 2 is %s" , config.get('account2', 'jid'))
xmpp2 = TestClient(config.get('account2','jid'), config.get('account2','pass'))
xmpp1.registerPlugin('xep_0004')
xmpp1.registerPlugin('xep_0030')
xmpp1.registerPlugin('xep_0060')
xmpp1.registerPlugin('xep_0199')
xmpp1.registerPlugin('jobs')
xmpp2.registerPlugin('xep_0004')
xmpp2.registerPlugin('xep_0030')
xmpp2.registerPlugin('xep_0060')
xmpp2.registerPlugin('xep_0199')
xmpp2.registerPlugin('jobs')
if not config.get('account1', 'server'):
# we don't know the server, but the lib can probably figure it out
xmpp1.connect()
else:
xmpp1.connect((config.get('account1', 'server'), 5222))
xmpp1.process(threaded=True)
#init
if not config.get('account2', 'server'):
# we don't know the server, but the lib can probably figure it out
xmpp2.connect()
else:
xmpp2.connect((config.get('account2', 'server'), 5222))
xmpp2.process(threaded=True)
TestPubsubServer.xmpp1 = xmpp1
TestPubsubServer.xmpp2 = xmpp2
TestPubsubServer.pshost = config.get('settings', 'pubsub')
xmpp1.waitforstart.get(True)
xmpp2.waitforstart.get(True)
testsuite = unittest.TestLoader().loadTestsFromTestCase(TestPubsubServer)
alltests_suite = unittest.TestSuite([testsuite])
result = unittest.TextTestRunner(verbosity=2).run(alltests_suite)
xmpp1.disconnect()
xmpp2.disconnect()

View File

@@ -1,233 +0,0 @@
import logging
import sleekxmpp
from optparse import OptionParser
from xml.etree import cElementTree as ET
import os
import time
import sys
import unittest
import sleekxmpp.plugins.xep_0004
from sleekxmpp.xmlstream.matcher.stanzapath import StanzaPath
from sleekxmpp.xmlstream.handler.waiter import Waiter
try:
import configparser
except ImportError:
import ConfigParser as configparser
try:
import queue
except ImportError:
import Queue as queue
class TestClient(sleekxmpp.ClientXMPP):
def __init__(self, jid, password):
sleekxmpp.ClientXMPP.__init__(self, jid, password)
self.add_event_handler("session_start", self.start)
#self.add_event_handler("message", self.message)
self.waitforstart = queue.Queue()
def start(self, event):
self.getRoster()
self.sendPresence()
self.waitforstart.put(True)
class TestPubsubServer(unittest.TestCase):
statev = {}
def __init__(self, *args, **kwargs):
unittest.TestCase.__init__(self, *args, **kwargs)
def setUp(self):
pass
def test001getdefaultconfig(self):
"""Get the default node config"""
self.xmpp1['xep_0060'].deleteNode(self.pshost, 'testnode2')
self.xmpp1['xep_0060'].deleteNode(self.pshost, 'testnode3')
self.xmpp1['xep_0060'].deleteNode(self.pshost, 'testnode4')
self.xmpp1['xep_0060'].deleteNode(self.pshost, 'testnode5')
result = self.xmpp1['xep_0060'].getNodeConfig(self.pshost)
self.statev['defaultconfig'] = result
self.failUnless(isinstance(result, sleekxmpp.plugins.xep_0004.Form))
def test002createdefaultnode(self):
"""Create a node without config"""
self.failUnless(self.xmpp1['xep_0060'].create_node(self.pshost, 'testnode1'))
def test003deletenode(self):
"""Delete recently created node"""
self.failUnless(self.xmpp1['xep_0060'].deleteNode(self.pshost, 'testnode1'))
def test004createnode(self):
"""Create a node with a config"""
self.statev['defaultconfig'].field['pubsub#access_model'].setValue('open')
self.statev['defaultconfig'].field['pubsub#notify_retract'].setValue(True)
self.failUnless(self.xmpp1['xep_0060'].create_node(self.pshost, 'testnode2', self.statev['defaultconfig']))
def test005reconfigure(self):
"""Retrieving node config and reconfiguring"""
nconfig = self.xmpp1['xep_0060'].getNodeConfig(self.pshost, 'testnode2')
self.failUnless(nconfig, "No configuration returned")
#print("\n%s ==\n %s" % (nconfig.getValues(), self.statev['defaultconfig'].getValues()))
self.failUnless(nconfig.getValues() == self.statev['defaultconfig'].getValues(), "Configuration does not match")
self.failUnless(self.xmpp1['xep_0060'].setNodeConfig(self.pshost, 'testnode2', nconfig))
def test006subscribetonode(self):
"""Subscribe to node from account 2"""
self.failUnless(self.xmpp2['xep_0060'].subscribe(self.pshost, "testnode2"))
def test007publishitem(self):
"""Publishing item"""
item = ET.Element('{http://netflint.net/protocol/test}test')
w = Waiter('wait publish', StanzaPath('message/pubsub_event/items'))
self.xmpp2.registerHandler(w)
result = self.xmpp1['xep_0060'].setItem(self.pshost, "testnode2", (('test1', item),))
msg = w.wait(5) # got to get a result in 5 seconds
self.failUnless(msg != False, "Account #2 did not get message event")
self.failUnless(result)
#need to add check for update
def test008updateitem(self):
"""Updating item"""
item = ET.Element('{http://netflint.net/protocol/test}test', {'someattr': 'hi there'})
w = Waiter('wait publish', StanzaPath('message/pubsub_event/items'))
self.xmpp2.registerHandler(w)
result = self.xmpp1['xep_0060'].setItem(self.pshost, "testnode2", (('test1', item),))
msg = w.wait(5) # got to get a result in 5 seconds
self.failUnless(msg != False, "Account #2 did not get message event")
self.failUnless(result)
#need to add check for update
def test009deleteitem(self):
"""Deleting item"""
w = Waiter('wait retract', StanzaPath('message/pubsub_event/items@node=testnode2'))
self.xmpp2.registerHandler(w)
result = self.xmpp1['xep_0060'].deleteItem(self.pshost, "testnode2", "test1")
self.failUnless(result, "Got error when deleting item.")
msg = w.wait(1)
self.failUnless(msg != False, "Did not get retract notice.")
def test010unsubscribenode(self):
"Unsubscribing Account #2"
self.failUnless(self.xmpp2['xep_0060'].unsubscribe(self.pshost, "testnode2"), "Got error response when unsubscribing.")
def test011createcollectionnode(self):
"Create a collection node w/ Account #2"
self.failUnless(self.xmpp2['xep_0060'].create_node(self.pshost, "testnode3", self.statev['defaultconfig'], True), "Could not create collection node")
def test012subscribecollection(self):
"Subscribe Account #1 to collection"
self.failUnless(self.xmpp1['xep_0060'].subscribe(self.pshost, "testnode3"))
def test013assignnodetocollection(self):
"Assign node to collection"
self.failUnless(self.xmpp2['xep_0060'].addNodeToCollection(self.pshost, 'testnode2', 'testnode3'))
def test014publishcollection(self):
"""Publishing item to collection child"""
item = ET.Element('{http://netflint.net/protocol/test}test')
w = Waiter('wait publish2', StanzaPath('message/pubsub_event/items@node=testnode2'))
self.xmpp1.registerHandler(w)
result = self.xmpp2['xep_0060'].setItem(self.pshost, "testnode2", (('test2', item),))
msg = w.wait(5) # got to get a result in 5 seconds
self.failUnless(msg != False, "Account #1 did not get message event: perhaps node was advertised incorrectly?")
self.failUnless(result)
# def test016speedtest(self):
# "Uncached speed test"
# import time
# start = time.time()
# for y in range(0, 50000, 1000):
# start2 = time.time()
# for x in range(y, y+1000):
# self.failUnless(self.xmpp1['xep_0060'].subscribe(self.pshost, "testnode4", subscribee="testuser%s@whatever" % x))
# print time.time() - start2
# seconds = time.time() - start
# print "--", seconds
# print "---------"
# time.sleep(15)
# self.failUnless(self.xmpp1['xep_0060'].deleteNode(self.pshost, 'testnode4'), "Could not delete non-cached test node")
# def test015speedtest(self):
# "cached speed test"
# result = self.xmpp1['xep_0060'].getNodeConfig(self.pshost)
# self.statev['defaultconfig'] = result
# self.statev['defaultconfig'].field['pubsub#node_type'].setValue("leaf")
# self.statev['defaultconfig'].field['sleek#saveonchange'].setValue(True)
# self.failUnless(self.xmpp1['xep_0060'].create_node(self.pshost, 'testnode4', self.statev['defaultconfig']))
# self.statev['defaultconfig'].field['sleek#saveonchange'].setValue(False)
# self.failUnless(self.xmpp1['xep_0060'].create_node(self.pshost, 'testnode5', self.statev['defaultconfig']))
# start = time.time()
# for y in range(0, 50000, 1000):
# start2 = time.time()
# for x in range(y, y+1000):
# self.failUnless(self.xmpp1['xep_0060'].subscribe(self.pshost, "testnode5", subscribee="testuser%s@whatever" % x))
# print time.time() - start2
# seconds = time.time() - start
# print "--", seconds
def test900cleanup(self):
"Cleaning up"
self.failUnless(self.xmpp1['xep_0060'].deleteNode(self.pshost, 'testnode2'), "Could not delete test node.")
self.failUnless(self.xmpp1['xep_0060'].deleteNode(self.pshost, 'testnode3'), "Could not delete collection node")
if __name__ == '__main__':
#parse command line arguements
optp = OptionParser()
optp.add_option('-q','--quiet', help='set logging to ERROR', action='store_const', dest='loglevel', const=logging.ERROR, default=logging.INFO)
optp.add_option('-d','--debug', help='set logging to DEBUG', action='store_const', dest='loglevel', const=logging.DEBUG, default=logging.INFO)
optp.add_option('-v','--verbose', help='set logging to COMM', action='store_const', dest='loglevel', const=5, default=logging.INFO)
optp.add_option("-c","--config", dest="configfile", default="config.xml", help="set config file to use")
optp.add_option("-n","--nodenum", dest="nodenum", default="1", help="set node number to use")
optp.add_option("-p","--pubsub", dest="pubsub", default="1", help="set pubsub host to use")
opts,args = optp.parse_args()
logging.basicConfig(level=opts.loglevel, format='%(levelname)-8s %(message)s')
#load xml config
logging.info("Loading config file: %s" , opts.configfile)
config = configparser.RawConfigParser()
config.read(opts.configfile)
#init
logging.info("Account 1 is %s" , config.get('account1', 'jid'))
xmpp1 = TestClient(config.get('account1','jid'), config.get('account1','pass'))
logging.info("Account 2 is %s" , config.get('account2', 'jid'))
xmpp2 = TestClient(config.get('account2','jid'), config.get('account2','pass'))
xmpp1.registerPlugin('xep_0004')
xmpp1.registerPlugin('xep_0030')
xmpp1.registerPlugin('xep_0060')
xmpp1.registerPlugin('xep_0199')
xmpp2.registerPlugin('xep_0004')
xmpp2.registerPlugin('xep_0030')
xmpp2.registerPlugin('xep_0060')
xmpp2.registerPlugin('xep_0199')
if not config.get('account1', 'server'):
# we don't know the server, but the lib can probably figure it out
xmpp1.connect()
else:
xmpp1.connect((config.get('account1', 'server'), 5222))
xmpp1.process(threaded=True)
#init
if not config.get('account2', 'server'):
# we don't know the server, but the lib can probably figure it out
xmpp2.connect()
else:
xmpp2.connect((config.get('account2', 'server'), 5222))
xmpp2.process(threaded=True)
TestPubsubServer.xmpp1 = xmpp1
TestPubsubServer.xmpp2 = xmpp2
TestPubsubServer.pshost = config.get('settings', 'pubsub')
xmpp1.waitforstart.get(True)
xmpp2.waitforstart.get(True)
testsuite = unittest.TestLoader().loadTestsFromTestCase(TestPubsubServer)
alltests_suite = unittest.TestSuite([testsuite])
result = unittest.TextTestRunner(verbosity=2).run(alltests_suite)
xmpp1.disconnect()
xmpp2.disconnect()

View File

@@ -1,13 +0,0 @@
[settings]
enabled=true
pubsub=pubsub.recon
[account1]
jid=fritzy@recon
pass=testing123
server=
[account2]
jid=fritzy2@recon
pass=testing123
server=

View File

@@ -1,350 +0,0 @@
"""
SleekXMPP: The Sleek XMPP Library
Copyright (C) 2010 Nathanael C. Fritz
This file is part of SleekXMPP.
See the file LICENSE for copying permission.
"""
import logging
import sleekxmpp
from optparse import OptionParser
from xml.etree import cElementTree as ET
import os
import time
import sys
import Queue
import thread
class testps(sleekxmpp.ClientXMPP):
def __init__(self, jid, password, ssl=False, plugin_config = {}, plugin_whitelist=[], nodenum=0, pshost=None):
sleekxmpp.ClientXMPP.__init__(self, jid, password, ssl, plugin_config, plugin_whitelist)
self.registerPlugin('xep_0004')
self.registerPlugin('xep_0030')
self.registerPlugin('xep_0060')
self.registerPlugin('xep_0092')
self.add_handler("<message xmlns='jabber:client'><event xmlns='http://jabber.org/protocol/pubsub#event' /></message>", self.pubsubEventHandler, name='Pubsub Event', threaded=True)
self.add_event_handler("session_start", self.start, threaded=True)
self.add_handler("<iq type='error' />", self.handleError, name='Iq Error')
self.events = Queue.Queue()
self.default_config = None
self.ps = self.plugin['xep_0060']
self.node = "pstestnode_%s"
self.pshost = pshost
if pshost is None:
self.pshost = self.boundjid.host
self.nodenum = int(nodenum)
self.leafnode = self.nodenum + 1
self.collectnode = self.nodenum + 2
self.lasterror = ''
self.sprintchars = 0
self.defaultconfig = None
self.tests = ['test_defaultConfig', 'test_createDefaultNode', 'test_getNodes', 'test_deleteNode', 'test_createWithConfig', 'test_reconfigureNode', 'test_subscribeToNode', 'test_addItem', 'test_updateItem', 'test_deleteItem', 'test_unsubscribeNode', 'test_createCollection', 'test_subscribeCollection', 'test_addNodeCollection', 'test_deleteNodeCollection', 'test_addCollectionNode', 'test_deleteCollectionNode', 'test_unsubscribeNodeCollection', 'test_deleteCollection']
self.passed = 0
self.width = 120
def start(self, event):
#TODO: make this configurable
self.getRoster()
self.sendPresence(ppriority=20)
thread.start_new(self.test_all, tuple())
def sprint(self, msg, end=False, color=False):
length = len(msg)
if color:
if color == "red":
color = "1;31"
elif color == "green":
color = "0;32"
msg = "%s%s%s" % ("\033[%sm" % color, msg, "\033[0m")
if not end:
sys.stdout.write(msg)
self.sprintchars += length
else:
self.sprint("%s%s" % ("." * (self.width - self.sprintchars - length), msg))
print('')
self.sprintchars = 0
sys.stdout.flush()
def pubsubEventHandler(self, xml):
for item in xml.findall('{http://jabber.org/protocol/pubsub#event}event/{http://jabber.org/protocol/pubsub#event}items/{http://jabber.org/protocol/pubsub#event}item'):
self.events.put(item.get('id', '__unknown__'))
for item in xml.findall('{http://jabber.org/protocol/pubsub#event}event/{http://jabber.org/protocol/pubsub#event}items/{http://jabber.org/protocol/pubsub#event}retract'):
self.events.put(item.get('id', '__unknown__'))
for item in xml.findall('{http://jabber.org/protocol/pubsub#event}event/{http://jabber.org/protocol/pubsub#event}collection/{http://jabber.org/protocol/pubsub#event}disassociate'):
self.events.put(item.get('node', '__unknown__'))
for item in xml.findall('{http://jabber.org/protocol/pubsub#event}event/{http://jabber.org/protocol/pubsub#event}collection/{http://jabber.org/protocol/pubsub#event}associate'):
self.events.put(item.get('node', '__unknown__'))
def handleError(self, xml):
error = xml.find('{jabber:client}error')
self.lasterror = error.getchildren()[0].tag.split('}')[-1]
def test_all(self):
print("Running Publish-Subscribe Tests")
version = self.plugin['xep_0092'].getVersion(self.pshost)
if version:
print("%s %s on %s" % (version.get('name', 'Unknown Server'), version.get('version', 'v?'), version.get('os', 'Unknown OS')))
print("=" * self.width)
for test in self.tests:
testfunc = getattr(self, test)
self.sprint("%s" % testfunc.__doc__)
if testfunc():
self.sprint("Passed", True, "green")
self.passed += 1
else:
if not self.lasterror:
self.lasterror = 'No response'
self.sprint("Failed (%s)" % self.lasterror, True, "red")
self.lasterror = ''
print("=" * self.width)
self.sprint("Cleaning up...")
#self.ps.deleteNode(self.pshost, self.node % self.nodenum)
self.ps.deleteNode(self.pshost, self.node % self.leafnode)
#self.ps.deleteNode(self.pshost, self.node % self.collectnode)
self.sprint("Done", True, "green")
self.disconnect()
self.sprint("%s" % self.passed, False, "green")
self.sprint("/%s Passed -- " % len(self.tests))
if len(self.tests) - self.passed:
self.sprint("%s" % (len(self.tests) - self.passed), False, "red")
else:
self.sprint("%s" % (len(self.tests) - self.passed), False, "green")
self.sprint(" Failed Tests")
print
#print "%s/%s Passed -- %s Failed Tests" % (self.passed, len(self.tests), len(self.tests) - self.passed)
def test_defaultConfig(self):
"Retreiving default configuration"
result = self.ps.getNodeConfig(self.pshost)
if result is False or result is None:
return False
else:
self.defaultconfig = result
try:
self.defaultconfig.field['pubsub#access_model'].setValue('open')
except KeyError:
pass
try:
self.defaultconfig.field['pubsub#notify_retract'].setValue(True)
except KeyError:
pass
return True
def test_createDefaultNode(self):
"Creating default node"
return self.ps.create_node(self.pshost, self.node % self.nodenum)
def test_getNodes(self):
"Getting list of nodes"
self.ps.getNodes(self.pshost)
self.ps.getItems(self.pshost, 'blog')
return True
def test_deleteNode(self):
"Deleting node"
return self.ps.deleteNode(self.pshost, self.node % self.nodenum)
def test_createWithConfig(self):
"Creating node with config"
if self.defaultconfig is None:
self.lasterror = "No Avail Config"
return False
return self.ps.create_node(self.pshost, self.node % self.leafnode, self.defaultconfig)
def test_reconfigureNode(self):
"Retrieving node config and reconfiguring"
nconfig = self.ps.getNodeConfig(self.pshost, self.node % self.leafnode)
if nconfig == False:
return False
return self.ps.setNodeConfig(self.pshost, self.node % self.leafnode, nconfig)
def test_subscribeToNode(self):
"Subscribing to node"
return self.ps.subscribe(self.pshost, self.node % self.leafnode)
def test_addItem(self):
"Adding item, waiting for notification"
item = ET.Element('test')
result = self.ps.setItem(self.pshost, self.node % self.leafnode, (('test_node1', item),))
if result == False:
return False
try:
event = self.events.get(True, 10)
except Queue.Empty:
return False
if event == 'test_node1':
return True
return False
def test_updateItem(self):
"Updating item, waiting for notification"
item = ET.Element('test')
item.attrib['crap'] = 'yup, right here'
result = self.ps.setItem(self.pshost, self.node % self.leafnode, (('test_node1', item),))
if result == False:
return False
try:
event = self.events.get(True, 10)
except Queue.Empty:
return False
if event == 'test_node1':
return True
return False
def test_deleteItem(self):
"Deleting item, waiting for notification"
result = self.ps.deleteItem(self.pshost, self.node % self.leafnode, 'test_node1')
if result == False:
return False
try:
event = self.events.get(True, 10)
except Queue.Empty:
self.lasterror = "No Notification"
return False
if event == 'test_node1':
return True
return False
def test_unsubscribeNode(self):
"Unsubscribing from node"
return self.ps.unsubscribe(self.pshost, self.node % self.leafnode)
def test_createCollection(self):
"Creating collection node"
return self.ps.create_node(self.pshost, self.node % self.collectnode, self.defaultconfig, True)
def test_subscribeCollection(self):
"Subscribing to collection node"
return self.ps.subscribe(self.pshost, self.node % self.collectnode)
def test_addNodeCollection(self):
"Assigning node to collection, waiting for notification"
config = self.ps.getNodeConfig(self.pshost, self.node % self.leafnode)
if not config or config is None:
self.lasterror = "Config Error"
return False
try:
config.field['pubsub#collection'].setValue(self.node % self.collectnode)
except KeyError:
self.sprint("...Missing Field...", False, "red")
config.addField('pubsub#collection', value=self.node % self.collectnode)
if not self.ps.setNodeConfig(self.pshost, self.node % self.leafnode, config):
return False
try:
event = self.events.get(True, 10)
except Queue.Empty:
self.lasterror = "No Notification"
return False
if event == self.node % self.leafnode:
return True
return False
def test_deleteNodeCollection(self):
"Removing node assignment to collection, waiting for notification"
config = self.ps.getNodeConfig(self.pshost, self.node % self.leafnode)
if not config or config is None:
self.lasterror = "Config Error"
return False
try:
config.field['pubsub#collection'].delValue(self.node % self.collectnode)
except KeyError:
self.sprint("...Missing Field...", False, "red")
config.addField('pubsub#collection', value='')
if not self.ps.setNodeConfig(self.pshost, self.node % self.leafnode, config):
return False
try:
event = self.events.get(True, 10)
except Queue.Empty:
self.lasterror = "No Notification"
return False
if event == self.node % self.leafnode:
return True
return False
def test_addCollectionNode(self):
"Assigning node from collection, waiting for notification"
config = self.ps.getNodeConfig(self.pshost, self.node % self.collectnode)
if not config or config is None:
self.lasterror = "Config Error"
return False
try:
config.field['pubsub#children'].setValue(self.node % self.leafnode)
except KeyError:
self.sprint("...Missing Field...", False, "red")
config.addField('pubsub#children', value=self.node % self.leafnode)
if not self.ps.setNodeConfig(self.pshost, self.node % self.collectnode, config):
return False
try:
event = self.events.get(True, 10)
except Queue.Empty:
self.lasterror = "No Notification"
return False
if event == self.node % self.leafnode:
return True
return False
def test_deleteCollectionNode(self):
"Removing node from collection, waiting for notification"
config = self.ps.getNodeConfig(self.pshost, self.node % self.collectnode)
if not config or config is None:
self.lasterror = "Config Error"
return False
try:
config.field['pubsub#children'].delValue(self.node % self.leafnode)
except KeyError:
self.sprint("...Missing Field...", False, "red")
config.addField('pubsub#children', value='')
if not self.ps.setNodeConfig(self.pshost, self.node % self.collectnode, config):
return False
try:
event = self.events.get(True, 10)
except Queue.Empty:
self.lasterror = "No Notification"
return False
if event == self.node % self.leafnode:
return True
return False
def test_unsubscribeNodeCollection(self):
"Unsubscribing from collection"
return self.ps.unsubscribe(self.pshost, self.node % self.collectnode)
def test_deleteCollection(self):
"Deleting collection"
return self.ps.deleteNode(self.pshost, self.node % self.collectnode)
if __name__ == '__main__':
#parse command line arguements
optp = OptionParser()
optp.add_option('-q','--quiet', help='set logging to ERROR', action='store_const', dest='loglevel', const=logging.ERROR, default=logging.INFO)
optp.add_option('-d','--debug', help='set logging to DEBUG', action='store_const', dest='loglevel', const=logging.DEBUG, default=logging.INFO)
optp.add_option('-v','--verbose', help='set logging to COMM', action='store_const', dest='loglevel', const=5, default=logging.INFO)
optp.add_option("-c","--config", dest="configfile", default="config.xml", help="set config file to use")
optp.add_option("-n","--nodenum", dest="nodenum", default="1", help="set node number to use")
optp.add_option("-p","--pubsub", dest="pubsub", default="1", help="set pubsub host to use")
opts,args = optp.parse_args()
logging.basicConfig(level=opts.loglevel, format='%(levelname)-8s %(message)s')
#load xml config
logging.info("Loading config file: %s" , opts.configfile)
config = ET.parse(os.path.expanduser(opts.configfile)).find('auth')
#init
logging.info("Logging in as %s" , config.attrib['jid'])
plugin_config = {}
plugin_config['xep_0092'] = {'name': 'SleekXMPP Example', 'version': '0.1-dev'}
plugin_config['xep_0199'] = {'keepalive': True, 'timeout': 30, 'frequency': 300}
con = testps(config.attrib['jid'], config.attrib['pass'], plugin_config=plugin_config, plugin_whitelist=[], nodenum=opts.nodenum, pshost=opts.pubsub)
if not config.get('server', None):
# we don't know the server, but the lib can probably figure it out
con.connect()
else:
con.connect((config.attrib['server'], 5222))
con.process(threaded=False)
print("")

1
docs/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
_build/*

View File

@@ -59,9 +59,10 @@ body {
margin: auto;
padding: 0px;
font-family: "Helvetica Neueu", Helvetica, sans-serif;
min-width: 59em;
min-width: 30em;
max-width: 70em;
color: #444;
text-align: center;
}
div.footer {
@@ -124,21 +125,25 @@ a.headerlink:hover {
/* basic text elements */
div.content {
margin: auto;
margin-top: 20px;
margin-left: 40px;
margin-right: 40px;
margin-bottom: 50px;
font-size: 0.9em;
width: 700px;
text-align: left;
}
/* heading and navigation */
div.header {
position: relative;
margin: auto;
margin-top: 125px;
height: 85px;
padding: 0 40px;
font-family: "Yanone Kaffeesatz";
text-align: left;
width: 750px;
}
div.header h1 {
font-size: 2.6em;
@@ -185,12 +190,12 @@ div.topnav {
z-index: 0;
}
div.topnav p {
margin: auto;
margin-top: 0;
margin-left: 40px;
margin-right: 40px;
margin-bottom: 0px;
text-align: right;
font-size: 0.8em;
width: 750px;
}
div.bottomnav {
background: #eeeeee;
@@ -404,3 +409,23 @@ div.viewcode-block:target {
padding: 0 12px;
}
#from_andyet {
-webkit-box-shadow: #CCC 0px 0px 3px;
background: rgba(255, 255, 255, 1);
bottom: 0px;
right: 17px;
padding: 3px 10px;
position: fixed;
}
#from_andyet h2 {
background-image: url("images/from_&yet.png");
background-repeat: no-repeat;
height: 29px;
line-height: 0;
text-indent: -9999em;
width: 79px;
margin-top: 0;
margin: 0px;
padding: 0px;
}

BIN
docs/_static/images/from_&yet.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

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

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

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

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

View File

@@ -50,7 +50,7 @@ copyright = u'2011, Nathan Fritz, Lance Stout'
# The short X.Y version.
version = '1.0'
# The full version, including alpha/beta/rc tags.
release = '1.0RC3'
release = '1.0'
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
@@ -91,7 +91,7 @@ pygments_style = 'tango'
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
html_theme = 'nature'
html_theme = 'haiku'
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the

View File

@@ -1,2 +1,208 @@
.. _mucbot:
=========================
Mulit-User Chat (MUC) Bot
=========================
.. note::
If you have any issues working through this quickstart guide
or the other tutorials here, please either send a message to the
`mailing list <http://groups.google.com/group/sleekxmpp-discussion>`_
or join the chat room at `sleek@conference.jabber.org
<xmpp:sleek@conference.jabber.org?join>`_.
If you have not yet installed SleekXMPP, do so now by either checking out a version
from `Github <http://github.com/fritzy/SleekXMPP>`_, or installing it using ``pip``
or ``easy_install``.
.. code-block:: sh
pip install sleekxmpp # Or: easy_install sleekxmpp
Now that you've got the basic gist of using SleekXMPP by following the
echobot example (:ref:`echobot`), we can use one of the bundled plugins
to create a very popular XMPP starter project: a `Multi-User Chat`_
(MUC) bot. Our bot will login to an XMPP server, join an MUC chat room
and "lurk" indefinitely, responding with a generic message to anyone
that mentions its nickname. It will also greet members as they join the
chat room.
.. _`multi-user chat`: http://xmpp.org/extensions/xep-0045.html
Joining The Room
----------------
As usual, our code will be based on the pattern explained in :ref:`echobot`.
To start, we create an ``MUCBot`` class based on
:class:`ClientXMPP <sleekxmpp.clientxmpp.ClientXMPP>` and which accepts
parameters for the JID of the MUC room to join, and the nick that the
bot will use inside the chat room. We also register an
:term:`event handler` for the :term:`session_start` event.
.. code-block:: python
import sleekxmpp
class MUCBot(sleekxmpp.ClientXMPP):
def __init__(self, jid, password, room, nick):
sleekxmpp.ClientXMPP.__init__(self, jid, password)
self.room = room
self.nick = nick
self.add_event_handler("session_start", self.start)
After initialization, we also need to register the MUC (XEP-0045) plugin
so that we can make use of the group chat plugin's methods and events.
.. code-block:: python
xmpp.register_plugin('xep_0045')
Finally, we can make our bot join the chat room once an XMPP session
has been established:
.. code-block:: python
def start(self, event):
self.get_roster()
self.send_presence()
self.plugin['xep_0045'].joinMUC(self.room,
self.nick,
wait=True)
Note that as in :ref:`echobot`, we need to include send an initial presence and request
the roster. Next, we want to join the group chat, so we call the
``joinMUC`` method of the MUC plugin.
.. note::
The :attr:`plugin <sleekxmpp.basexmpp.BaseXMPP.plugin>` attribute is
dictionary that maps to instances of plugins that we have previously
registered, by their names.
Adding Functionality
--------------------
Currently, our bot just sits dormantly inside the chat room, but we
would like it to respond to two distinct events by issuing a generic
message in each case to the chat room. In particular, when a member
mentions the bot's nickname inside the chat room, and when a member
joins the chat room.
Responding to Mentions
~~~~~~~~~~~~~~~~~~~~~~
Whenever a user mentions our bot's nickname in chat, our bot will
respond with a generic message resembling *"I heard that, user."* We do
this by examining all of the messages sent inside the chat and looking
for the ones which contain the nickname string.
First, we register an event handler for the :term:`groupchat_message`
event inside the bot's ``__init__`` function.
.. note::
We do not register a handler for the :term:`message` event in this
bot, but if we did, the group chat message would have been sent to
both handlers.
.. code-block:: python
def __init__(self, jid, password, room, nick):
sleekxmpp.ClientXMPP.__init__(self, jid, password)
self.room = room
self.nick = nick
self.add_event_handler("session_start", self.start)
self.add_event_handler("groupchat_message", self.muc_message)
Then, we can send our generic message whenever the bot's nickname gets
mentioned.
.. warning::
Always check that a message is not from yourself,
otherwise you will create an infinite loop responding
to your own messages.
.. code-block:: python
def muc_message(self, msg):
if msg['mucnick'] != self.nick and self.nick in msg['body']:
self.send_message(mto=msg['from'].bare,
mbody="I heard that, %s." % msg['mucnick'],
mtype='groupchat')
Greeting Members
~~~~~~~~~~~~~~~~
Now we want to greet member whenever they join the group chat. To
do this we will use the dynamic ``muc::room@server::got_online`` [1]_
event so it's a good idea to register an event handler for it.
.. note::
The groupchat_presence event is triggered whenever a
presence stanza is received from any chat room, including
any presences you send yourself. To limit event handling
to a single room, use the events ``muc::room@server::presence``,
``muc::room@server::got_online``, or ``muc::room@server::got_offline``.
.. code-block:: python
def __init__(self, jid, password, room, nick):
sleekxmpp.ClientXMPP.__init__(self, jid, password)
self.room = room
self.nick = nick
self.add_event_handler("session_start", self.start)
self.add_event_handler("groupchat_message", self.muc_message)
self.add_event_handler("muc::%s::got_online" % self.room,
self.muc_online)
Now all that's left to do is to greet them:
.. code-block:: python
def muc_online(self, presence):
if presence['muc']['nick'] != self.nick:
self.send_message(mto=presence['from'].bare,
mbody="Hello, %s %s" % (presence['muc']['role'],
presence['muc']['nick']),
mtype='groupchat')
.. [1] this is similar to the :term:`got_online` event and is sent by
the xep_0045 plugin whenever a member joins the referenced
MUC chat room.
Final Product
-------------
.. compound::
The final step is to create a small runner script for initialising our ``MUCBot`` class and adding some
basic configuration options. By following the basic boilerplate pattern in :ref:`echobot`, we arrive
at the code below. To experiment with this example, you can use:
.. code-block:: sh
python muc.py -d -j jid@example.com -r room@muc.example.net -n lurkbot
which will prompt for the password, log in, and join the group chat. To test, open
your regular IM client and join the same group chat that you sent the bot to. You
will see ``lurkbot`` as one of the members in the group chat, and that it greeted
you upon entry. Send a message with the string "lurkbot" inside the body text, and you
will also see that it responds with our pre-programmed customized message.
.. include:: ../../examples/muc.py
:literal:

View File

@@ -13,7 +13,7 @@ SleekXMPP
``develop`` branch.
**Latest Stable Release**
- `1.0 RC3 <http://github.com/fritzy/SleekXMPP/zipball/1.0-RC3>`_
- `1.0 <http://github.com/fritzy/SleekXMPP/zipball/1.0>`_
**Develop Releases**
- `Latest Develop Version <http://github.com/fritzy/SleekXMPP/zipball/develop>`_
@@ -59,6 +59,72 @@ SleekXMPP's design goals and philosphy are:
sensible defaults and appropriate abstractions. XML can be ugly to work
with, but it doesn't have to be that way.
Here's your first SleekXMPP Bot:
--------------------------------
.. code-block:: python
import logging
from sleekxmpp import ClientXMPP
from sleekxmpp.exceptions import IqError, IqTimeout
class EchoBot(ClientXMPP):
def __init__(self, jid, password):
ClientXMPP.__init__(self, jid, password)
self.add_event_handler("session_start", self.session_start)
self.add_event_handler("message", self.message)
# If you wanted more functionality, here's how to register plugins:
# self.register_plugin('xep_0030') # Service Discovery
# self.register_plugin('xep_0199') # XMPP Ping
# Here's how to access plugins once you've registered them:
# self['xep_0030'].add_feature('echo_demo')
# If you are working with an OpenFire server, you will
# need to use a different SSL version:
# import ssl
# self.ssl_version = ssl.PROTOCOL_SSLv3
def session_start(self, event):
self.send_presence()
self.get_roster()
# Most get_*/set_* methods from plugins use Iq stanzas, which
# can generate IqError and IqTimeout exceptions
#
# try:
# self.get_roster()
# except IqError as err:
# logging.error('There was an error getting the roster')
# logging.error(err.iq['error']['condition'])
# self.disconnect()
# except IqTimeout:
# logging.error('Server is taking too long to respond')
# self.disconnect()
def message(self, msg):
if msg['type'] in ('chat', 'normal'):
msg.reply("Thanks for sending\n%(body)s" % msg).send()
if __name__ == '__main__':
# Ideally use optparse or argparse to get JID,
# password, and log level.
logging.basicConfig(level=logging.DEBUG,
format='%(levelname)-8s %(message)s')
xmpp = EchoBot('somejid@example.com', 'use_getpass')
xmpp.connect()
xmpp.process(block=True)
Getting Started (with Examples)
-------------------------------
.. toctree::
@@ -156,17 +222,24 @@ Additional Info
Credits
-------
**Main Author:** Nathan Fritz
`fritzy@netflint.net <xmpp:fritzy@netflint.net?message>`_,
`@fritzy <http://twitter.com/fritzy>`_
Nathan is also the author of XMPPHP and `Seesmic-AS3-XMPP
<http://code.google.com/p/seesmic-as3-xmpp/>`_, and a member of the XMPP
Council.
**Main Author:** `Nathan Fritz <http://andyet.net/team/fritzy>`_
`fritzy@netflint.net <xmpp:fritzy@netflint.net?message>`_,
`@fritzy <http://twitter.com/fritzy>`_
**Co-Author:** Lance Stout
`lancestout@gmail.com <xmpp:lancestout@gmail.com?message>`_,
`@lancestout <http://twitter.com/lancestout>`_
Nathan is also the author of XMPPHP and `Seesmic-AS3-XMPP
<http://code.google.com/p/seesmic-as3-xmpp/>`_, and a former member of the XMPP
Council.
**Co-Author:** `Lance Stout <http://andyet.net/team/lance>`_
`lancestout@gmail.com <xmpp:lancestout@gmail.com?message>`_,
`@lancestout <http://twitter.com/lancestout>`_
Both Fritzy and Lance work for `&yet <http://andyet.net>`_, which specializes in
realtime web and XMPP applications.
- `contact@andyet.net <mailto:contact@andyet.net>`_
- `XMPP Consulting <http://xmppconsulting.com>`_
**Contributors:**
- Brian Beggs (`macdiesel <http://github.com/macdiesel>`_)

178
examples/admin_commands.py Executable file
View File

@@ -0,0 +1,178 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
SleekXMPP: The Sleek XMPP Library
Copyright (C) 2010 Nathanael C. Fritz
This file is part of SleekXMPP.
See the file LICENSE for copying permission.
"""
import sys
import logging
import getpass
from optparse import OptionParser
import sleekxmpp
# Python versions before 3.0 do not use UTF-8 encoding
# by default. To ensure that Unicode is handled properly
# throughout SleekXMPP, we will set the default encoding
# ourselves to UTF-8.
if sys.version_info < (3, 0):
reload(sys)
sys.setdefaultencoding('utf8')
else:
raw_input = input
class AdminCommands(sleekxmpp.ClientXMPP):
"""
A simple SleekXMPP bot that uses admin commands to
add a new user to a server.
"""
def __init__(self, jid, password, command):
sleekxmpp.ClientXMPP.__init__(self, jid, password)
self.command = command
self.add_event_handler("session_start", self.start)
def start(self, event):
"""
Process the session_start event.
Typical actions for the session_start event are
requesting the roster and broadcasting an initial
presence stanza.
Arguments:
event -- An empty dictionary. The session_start
event does not provide any additional
data.
"""
self.send_presence()
self.get_roster()
def command_success(iq, session):
print('Command completed')
if iq['command']['form']:
for var, field in iq['command']['form']['fields'].items():
print('%s: %s' % (var, field['value']))
if iq['command']['notes']:
print('Command Notes:')
for note in iq['command']['notes']:
print('%s: %s' % note)
self.disconnect()
def command_error(iq, session):
print('Error completing command')
print('%s: %s' % (iq['error']['condition'],
iq['error']['text']))
self['xep_0050'].terminate_command(session)
self.disconnect()
def process_form(iq, session):
form = iq['command']['form']
answers = {}
for var, field in form['fields'].items():
if var != 'FORM_TYPE':
if field['type'] == 'boolean':
answers[var] = raw_input('%s (y/n): ' % field['label'])
if answers[var].lower() in ('1', 'true', 'y', 'yes'):
answers[var] = '1'
else:
answers[var] = '0'
else:
answers[var] = raw_input('%s: ' % field['label'])
else:
answers['FORM_TYPE'] = field['value']
form['type'] = 'submit'
form['values'] = answers
session['next'] = command_success
session['payload'] = form
self['xep_0050'].complete_command(session)
session = {'next': process_form,
'error': command_error}
command = self.command.replace('-', '_')
handler = getattr(self['xep_0133'], command, None)
if handler:
handler(session={
'next': process_form,
'error': command_error
})
else:
print('Invalid command name: %s' % self.command)
self.disconnect()
if __name__ == '__main__':
# Setup the command line arguments.
optp = OptionParser()
# Output verbosity options.
optp.add_option('-q', '--quiet', help='set logging to ERROR',
action='store_const', dest='loglevel',
const=logging.ERROR, default=logging.INFO)
optp.add_option('-d', '--debug', help='set logging to DEBUG',
action='store_const', dest='loglevel',
const=logging.DEBUG, default=logging.INFO)
optp.add_option('-v', '--verbose', help='set logging to COMM',
action='store_const', dest='loglevel',
const=5, default=logging.INFO)
# JID and password options.
optp.add_option("-j", "--jid", dest="jid",
help="JID to use")
optp.add_option("-p", "--password", dest="password",
help="password to use")
optp.add_option("-c", "--command", dest="command",
help="admin command to use")
opts, args = optp.parse_args()
# Setup logging.
logging.basicConfig(level=opts.loglevel,
format='%(levelname)-8s %(message)s')
if opts.jid is None:
opts.jid = raw_input("Username: ")
if opts.password is None:
opts.password = getpass.getpass("Password: ")
if opts.command is None:
opts.command = raw_input("Admin command: ")
# Setup the CommandBot and register plugins. Note that while plugins may
# have interdependencies, the order in which you register them does
# not matter.
xmpp = AdminCommands(opts.jid, opts.password, opts.command)
xmpp.register_plugin('xep_0133') # Service Administration
# If you are working with an OpenFire server, you may need
# to adjust the SSL version used:
# xmpp.ssl_version = ssl.PROTOCOL_SSLv3
# If you want to verify the SSL certificates offered by a server:
# xmpp.ca_certs = "path/to/ca/cert"
# Connect to the XMPP server and start processing XMPP stanzas.
if xmpp.connect():
# If you do not have the dnspython library installed, you will need
# to manually specify the name of the server if it does not match
# the one in the JID. For example, to use Google Talk you would
# need to use:
#
# if xmpp.connect(('talk.google.com', 5222)):
# ...
xmpp.process(block=True)
print("Done")
else:
print("Unable to connect.")

View File

@@ -0,0 +1,173 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
SleekXMPP: The Sleek XMPP Library
Copyright (C) 2010 Nathanael C. Fritz
This file is part of SleekXMPP.
See the file LICENSE for copying permission.
"""
import sys
import logging
import getpass
from optparse import OptionParser
import sleekxmpp
from sleekxmpp import ClientXMPP, Iq
from sleekxmpp.exceptions import IqError, IqTimeout, XMPPError
from sleekxmpp.xmlstream import register_stanza_plugin
from sleekxmpp.xmlstream.handler import Callback
from sleekxmpp.xmlstream.matcher import StanzaPath
from stanza import Action
# Python versions before 3.0 do not use UTF-8 encoding
# by default. To ensure that Unicode is handled properly
# throughout SleekXMPP, we will set the default encoding
# ourselves to UTF-8.
if sys.version_info < (3, 0):
reload(sys)
sys.setdefaultencoding('utf8')
else:
raw_input = input
class ActionBot(sleekxmpp.ClientXMPP):
"""
A simple SleekXMPP bot that receives a custom stanza
from another client.
"""
def __init__(self, jid, password):
sleekxmpp.ClientXMPP.__init__(self, jid, password)
# The session_start event will be triggered when
# the bot establishes its connection with the server
# and the XML streams are ready for use. We want to
# listen for this event so that we we can initialize
# our roster.
self.add_event_handler("session_start", self.start)
self.registerHandler(
Callback('Some custom iq',
StanzaPath('iq@type=set/action'),
self._handle_action))
self.add_event_handler('custom_action',
self._handle_action_event,
threaded=True)
register_stanza_plugin(Iq, Action)
def start(self, event):
"""
Process the session_start event.
Typical actions for the session_start event are
requesting the roster and broadcasting an initial
presence stanza.
Arguments:
event -- An empty dictionary. The session_start
event does not provide any additional
data.
"""
self.send_presence()
self.get_roster()
def _handle_action(self, iq):
"""
Raise an event for the stanza so that it can be processed in its
own thread without blocking the main stanza processing loop.
"""
self.event('custom_action', iq)
def _handle_action_event(self, iq):
"""
Respond to the custom action event.
Since one of the actions is to disconnect, this
event handler needs to be run in threaded mode, by
using `threaded=True` in the `add_event_handler` call.
"""
method = iq['action']['method']
param = iq['action']['param']
if method == 'is_prime' and param == '2':
print("got message: %s" % iq)
iq.reply()
iq['action']['status'] = 'done'
iq.send()
elif method == 'bye':
print("got message: %s" % iq)
self.disconnect()
else:
print("got message: %s" % iq)
iq.reply()
iq['action']['status'] = 'error'
iq.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')
if opts.jid is None:
opts.jid = raw_input("Username: ")
if opts.password is None:
opts.password = getpass.getpass("Password: ")
# Setup the CommandBot and register plugins. Note that while plugins may
# have interdependencies, the order in which you register them does
# not matter.
xmpp = ActionBot(opts.jid, opts.password)
xmpp.register_plugin('xep_0030') # Service Discovery
xmpp.register_plugin('xep_0004') # Data Forms
xmpp.register_plugin('xep_0050') # Adhoc Commands
xmpp.register_plugin('xep_0199', {'keepalive': True, 'frequency':15})
# If you are working with an OpenFire server, you may need
# to adjust the SSL version used:
# xmpp.ssl_version = ssl.PROTOCOL_SSLv3
# If you want to verify the SSL certificates offered by a server:
# xmpp.ca_certs = "path/to/ca/cert"
# Connect to the XMPP server and start processing XMPP stanzas.
if xmpp.connect():
# If you do not have the dnspython library installed, you will need
# to manually specify the name of the server if it does not match
# the one in the JID. For example, to use Google Talk you would
# need to use:
#
# if xmpp.connect(('talk.google.com', 5222)):
# ...
xmpp.process(block=True)
print("Done")
else:
print("Unable to connect.")

View File

@@ -0,0 +1,175 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
SleekXMPP: The Sleek XMPP Library
Copyright (C) 2010 Nathanael C. Fritz
This file is part of SleekXMPP.
See the file LICENSE for copying permission.
"""
import sys
import logging
import getpass
from optparse import OptionParser
import sleekxmpp
from sleekxmpp import Iq
from sleekxmpp.exceptions import XMPPError
from sleekxmpp.xmlstream import register_stanza_plugin
from stanza import Action
# Python versions before 3.0 do not use UTF-8 encoding
# by default. To ensure that Unicode is handled properly
# throughout SleekXMPP, we will set the default encoding
# ourselves to UTF-8.
if sys.version_info < (3, 0):
reload(sys)
sys.setdefaultencoding('utf8')
else:
raw_input = input
class ActionUserBot(sleekxmpp.ClientXMPP):
"""
A simple SleekXMPP bot that sends a custom action stanza
to another client.
"""
def __init__(self, jid, password, other):
sleekxmpp.ClientXMPP.__init__(self, jid, password)
self.action_provider = other
# The session_start event will be triggered when
# the bot establishes its connection with the server
# and the XML streams are ready for use. We want to
# listen for this event so that we we can initialize
# our roster.
self.add_event_handler("session_start", self.start, threaded=True)
self.add_event_handler("message", self.message)
register_stanza_plugin(Iq, Action)
def start(self, event):
"""
Process the session_start event.
Typical actions for the session_start event are
requesting the roster and broadcasting an initial
presence stanza.
Arguments:
event -- An empty dictionary. The session_start
event does not provide any additional
data.
"""
self.send_presence()
self.get_roster()
self.send_custom_iq()
def send_custom_iq(self):
"""Create and send two custom actions.
If the first action was successful, then send
a shutdown command and then disconnect.
"""
iq = self.Iq()
iq['to'] = self.action_provider
iq['type'] = 'set'
iq['action']['method'] = 'is_prime'
iq['action']['param'] = '2'
try:
resp = iq.send()
if resp['action']['status'] == 'done':
#sending bye
iq2 = self.Iq()
iq2['to'] = self.action_provider
iq2['type'] = 'set'
iq2['action']['method'] = 'bye'
iq2.send(block=False)
# The wait=True delays the disconnect until the queue
# of stanzas to be sent becomes empty.
self.disconnect(wait=True)
except XMPPError:
print('There was an error sending the custom action.')
def message(self, msg):
"""
Process incoming message stanzas.
Arguments:
msg -- The received message stanza.
"""
logging.info(msg['body'])
if __name__ == '__main__':
# Setup the command line arguments.
optp = OptionParser()
# Output verbosity options.
optp.add_option('-q', '--quiet', help='set logging to ERROR',
action='store_const', dest='loglevel',
const=logging.ERROR, default=logging.INFO)
optp.add_option('-d', '--debug', help='set logging to DEBUG',
action='store_const', dest='loglevel',
const=logging.DEBUG, default=logging.INFO)
optp.add_option('-v', '--verbose', help='set logging to COMM',
action='store_const', dest='loglevel',
const=5, default=logging.INFO)
# JID and password options.
optp.add_option("-j", "--jid", dest="jid",
help="JID to use")
optp.add_option("-p", "--password", dest="password",
help="password to use")
optp.add_option("-o", "--other", dest="other",
help="JID providing custom stanza")
opts, args = optp.parse_args()
# Setup logging.
logging.basicConfig(level=opts.loglevel,
format='%(levelname)-8s %(message)s')
if opts.jid is None:
opts.jid = raw_input("Username: ")
if opts.password is None:
opts.password = getpass.getpass("Password: ")
if opts.other is None:
opts.other = raw_input("JID Providing custom stanza: ")
# Setup the CommandBot and register plugins. Note that while plugins may
# have interdependencies, the order in which you register them does
# not matter.
xmpp = ActionUserBot(opts.jid, opts.password, opts.other)
xmpp.register_plugin('xep_0030') # Service Discovery
xmpp.register_plugin('xep_0004') # Data Forms
xmpp.register_plugin('xep_0050') # Adhoc Commands
# If you are working with an OpenFire server, you may need
# to adjust the SSL version used:
# xmpp.ssl_version = ssl.PROTOCOL_SSLv3
# If you want to verify the SSL certificates offered by a server:
# xmpp.ca_certs = "path/to/ca/cert"
# Connect to the XMPP server and start processing XMPP stanzas.
if xmpp.connect():
# If you do not have the dnspython library installed, you will need
# to manually specify the name of the server if it does not match
# the one in the JID. For example, to use Google Talk you would
# need to use:
#
# if xmpp.connect(('talk.google.com', 5222)):
# ...
xmpp.process(block=True)
print("Done")
else:
print("Unable to connect.")

View File

@@ -0,0 +1,56 @@
from sleekxmpp.xmlstream import ElementBase
class Action(ElementBase):
"""
A stanza class for XML content of the form:
<action xmlns="sleekxmpp:custom:actions">
<method>X</method>
<param>X</param>
<status>X</status>
</action>
"""
#: The `name` field refers to the basic XML tag name of the
#: stanza. Here, the tag name will be 'action'.
name = 'action'
#: The namespace of the main XML tag.
namespace = 'sleekxmpp:custom:actions'
#: The `plugin_attrib` value is the name that can be used
#: with a parent stanza to access this stanza. For example
#: from an Iq stanza object, accessing:
#:
#: iq['action']
#:
#: would reference an Action object, and will even create
#: an Action object and append it to the Iq stanza if
#: one doesn't already exist.
plugin_attrib = 'action'
#: Stanza objects expose dictionary-like interfaces for
#: accessing and manipulating substanzas and other values.
#: The set of interfaces defined here are the names of
#: these dictionary-like interfaces provided by this stanza
#: type. For example, an Action stanza object can use:
#:
#: action['method'] = 'foo'
#: print(action['param'])
#: del action['status']
#:
#: to set, get, or remove its values.
interfaces = set(('method', 'param', 'status'))
#: By default, values in the `interfaces` set are mapped to
#: attribute values. This can be changed such that an interface
#: maps to a subelement's text value by adding interfaces to
#: the sub_interfaces set. For example, here all interfaces
#: are marked as sub_interfaces, and so the XML produced will
#: look like:
#:
#: <action xmlns="sleekxmpp:custom:actions">
#: <method>foo</method>
#: </action>
sub_interfaces = interfaces

View File

@@ -62,7 +62,7 @@ class Disco(sleekxmpp.ClientXMPP):
# and the XML streams are ready for use. We want to
# listen for this event so that we we can initialize
# our roster.
self.add_event_handler("session_start", self.start)
self.add_event_handler("session_start", self.start, threaded=True)
def start(self, event):
"""

View File

@@ -0,0 +1,184 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
SleekXMPP: The Sleek XMPP Library
Copyright (C) 2012 Nathanael C. Fritz
This file is part of SleekXMPP.
See the file LICENSE for copying permission.
"""
import sys
import logging
import getpass
import threading
from optparse import OptionParser
import sleekxmpp
from sleekxmpp.exceptions import XMPPError
# Python versions before 3.0 do not use UTF-8 encoding
# by default. To ensure that Unicode is handled properly
# throughout SleekXMPP, we will set the default encoding
# ourselves to UTF-8.
if sys.version_info < (3, 0):
reload(sys)
sys.setdefaultencoding('utf8')
else:
raw_input = input
FILE_TYPES = {
'image/png': 'png',
'image/gif': 'gif',
'image/jpeg': 'jpg'
}
class AvatarDownloader(sleekxmpp.ClientXMPP):
"""
A basic script for downloading the avatars for a user's contacts.
"""
def __init__(self, jid, password):
sleekxmpp.ClientXMPP.__init__(self, jid, password)
self.add_event_handler("session_start", self.start, threaded=True)
self.add_event_handler("changed_status", self.wait_for_presences)
self.add_event_handler('vcard_avatar_update', self.on_vcard_avatar)
self.add_event_handler('avatar_metadata_publish', self.on_avatar)
self.received = set()
self.presences_received = threading.Event()
def start(self, event):
"""
Process the session_start event.
Typical actions for the session_start event are
requesting the roster and broadcasting an initial
presence stanza.
Arguments:
event -- An empty dictionary. The session_start
event does not provide any additional
data.
"""
self.send_presence()
self.get_roster()
print('Waiting for presence updates...\n')
self.presences_received.wait(15)
self.disconnect(wait=True)
def on_vcard_avatar(self, pres):
print("Received vCard avatar update from %s" % pres['from'].bare)
try:
result = self['xep_0054'].get_vcard(pres['from'], cached=True)
except XMPPError:
print("Error retrieving avatar for %s" % pres['from'])
return
avatar = result['vcard_temp']['PHOTO']
filetype = FILE_TYPES.get(avatar['TYPE'], 'png')
filename = 'vcard_avatar_%s_%s.%s' % (
pres['from'].bare,
pres['vcard_temp_update']['photo'],
filetype)
with open(filename, 'w+') as img:
img.write(avatar['BINVAL'])
def on_avatar(self, msg):
print("Received avatar update from %s" % msg['from'])
metadata = msg['pubsub_event']['items']['item']['avatar_metadata']
for info in metadata['items']:
if not info['url']:
try:
result = self['xep_0084'].retrieve_avatar(msg['from'], info['id'])
except XMPPError:
print("Error retrieving avatar for %s" % msg['from'])
return
avatar = result['pubsub']['items']['item']['avatar_data']
filetype = FILE_TYPES.get(metadata['type'], 'png')
filename = 'avatar_%s_%s.%s' % (msg['from'].bare, info['id'], filetype)
with open(filename, 'w+') as img:
img.write(avatar['value'])
else:
# We could retrieve the avatar via HTTP, etc here instead.
pass
def wait_for_presences(self, pres):
"""
Wait to receive updates from all roster contacts.
"""
self.received.add(pres['from'].bare)
if len(self.received) >= len(self.client_roster.keys()):
self.presences_received.set()
else:
self.presences_received.clear()
if __name__ == '__main__':
# Setup the command line arguments.
optp = OptionParser()
optp.add_option('-q','--quiet', help='set logging to ERROR',
action='store_const',
dest='loglevel',
const=logging.ERROR,
default=logging.ERROR)
optp.add_option('-d','--debug', help='set logging to DEBUG',
action='store_const',
dest='loglevel',
const=logging.DEBUG,
default=logging.ERROR)
optp.add_option('-v','--verbose', help='set logging to COMM',
action='store_const',
dest='loglevel',
const=5,
default=logging.ERROR)
# JID and password options.
optp.add_option("-j", "--jid", dest="jid",
help="JID to use")
optp.add_option("-p", "--password", dest="password",
help="password to use")
opts,args = optp.parse_args()
# Setup logging.
logging.basicConfig(level=opts.loglevel,
format='%(levelname)-8s %(message)s')
if opts.jid is None:
opts.jid = raw_input("Username: ")
if opts.password is None:
opts.password = getpass.getpass("Password: ")
xmpp = AvatarDownloader(opts.jid, opts.password)
xmpp.register_plugin('xep_0054')
xmpp.register_plugin('xep_0153')
xmpp.register_plugin('xep_0084')
# If you are working with an OpenFire server, you may need
# to adjust the SSL version used:
# xmpp.ssl_version = ssl.PROTOCOL_SSLv3
# If you want to verify the SSL certificates offered by a server:
# xmpp.ca_certs = "path/to/ca/cert"
# Connect to the XMPP server and start processing XMPP stanzas.
if xmpp.connect():
# If you do not have the dnspython library installed, you will need
# to manually specify the name of the server if it does not match
# the one in the JID. For example, to use Google Talk you would
# need to use:
#
# if xmpp.connect(('talk.google.com', 5222)):
# ...
xmpp.process(block=True)
else:
print("Unable to connect.")

View File

@@ -122,6 +122,19 @@ if __name__ == '__main__':
xmpp.register_plugin('xep_0060') # PubSub
xmpp.register_plugin('xep_0199') # XMPP Ping
# If you are connecting to Facebook and wish to use the
# X-FACEBOOK-PLATFORM authentication mechanism, you will need
# your API key and an access token. Then you'll set:
# xmpp.credentials['api_key'] = 'THE_API_KEY'
# xmpp.credentials['access_token'] = 'THE_ACCESS_TOKEN'
# If you are connecting to MSN, then you will need an
# access token, and it does not matter what JID you
# specify other than that the domain is 'messenger.live.com',
# so '_@messenger.live.com' will work. You can specify
# the access token as so:
# xmpp.credentials['access_token'] = 'THE_ACCESS_TOKEN'
# If you are working with an OpenFire server, you may need
# to adjust the SSL version used:
# xmpp.ssl_version = ssl.PROTOCOL_SSLv3

165
examples/gtalk_custom_domain.py Executable file
View File

@@ -0,0 +1,165 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
SleekXMPP: The Sleek XMPP Library
Copyright (C) 2010 Nathanael C. Fritz
This file is part of SleekXMPP.
See the file LICENSE for copying permission.
"""
import sys
import logging
import getpass
from optparse import OptionParser
import sleekxmpp
import ssl
from sleekxmpp.xmlstream import cert
# Python versions before 3.0 do not use UTF-8 encoding
# by default. To ensure that Unicode is handled properly
# throughout SleekXMPP, we will set the default encoding
# ourselves to UTF-8.
if sys.version_info < (3, 0):
reload(sys)
sys.setdefaultencoding('utf8')
else:
raw_input = input
class GTalkBot(sleekxmpp.ClientXMPP):
"""
A demonstration of using SleekXMPP with accounts from a Google Apps
account with a custom domain, because it requires custom certificate
validation.
"""
def __init__(self, jid, password):
sleekxmpp.ClientXMPP.__init__(self, jid, password)
# The session_start event will be triggered when
# the bot establishes its connection with the server
# and the XML streams are ready for use. We want to
# listen for this event so that we we can initialize
# our roster.
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)
# Using a Google Apps custom domain, the certificate
# does not contain the custom domain, just the GTalk
# server name. So we will need to process invalid
# certifcates ourselves and check that it really
# is from Google.
self.add_event_handler("ssl_invalid_cert", self.invalid_cert)
def invalid_cert(self, pem_cert):
der_cert = ssl.PEM_cert_to_DER_cert(pem_cert)
try:
cert.verify('talk.google.com', der_cert)
logging.debug("CERT: Found GTalk certificate")
except cert.CertificateError as err:
log.error(err.message)
self.disconnect(send_close=False)
def start(self, event):
"""
Process the session_start event.
Typical actions for the session_start event are
requesting the roster and broadcasting an initial
presence stanza.
Arguments:
event -- An empty dictionary. The session_start
event does not provide any additional
data.
"""
self.send_presence()
self.get_roster()
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.
"""
if msg['type'] in ('chat', 'normal'):
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')
if opts.jid is None:
opts.jid = raw_input("Username: ")
if opts.password is None:
opts.password = getpass.getpass("Password: ")
# Setup the GTalkBot and register plugins. Note that while plugins may
# have interdependencies, the order in which you register them does
# not matter.
xmpp = GTalkBot(opts.jid, opts.password)
xmpp.register_plugin('xep_0030') # Service Discovery
xmpp.register_plugin('xep_0004') # Data Forms
xmpp.register_plugin('xep_0060') # PubSub
xmpp.register_plugin('xep_0199') # XMPP Ping
# If you are working with an OpenFire server, you may need
# to adjust the SSL version used:
# xmpp.ssl_version = ssl.PROTOCOL_SSLv3
# If you want to verify the SSL certificates offered by a server:
# xmpp.ca_certs = "path/to/ca/cert"
# Connect to the XMPP server and start processing XMPP stanzas.
if xmpp.connect():
# If you do not have the dnspython library installed, you will need
# to manually specify the name of the server if it does not match
# the one in the JID. For example, to use Google Talk you would
# need to use:
#
# if xmpp.connect(('talk.google.com', 5222)):
# ...
xmpp.process(block=True)
print("Done")
else:
print("Unable to connect.")

View File

@@ -0,0 +1,149 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
SleekXMPP: The Sleek XMPP Library
Copyright (C) 2010 Nathanael C. Fritz
This file is part of SleekXMPP.
See the file LICENSE for copying permission.
"""
import sys
import logging
import getpass
from optparse import OptionParser
import sleekxmpp
# Python versions before 3.0 do not use UTF-8 encoding
# by default. To ensure that Unicode is handled properly
# throughout SleekXMPP, we will set the default encoding
# ourselves to UTF-8.
if sys.version_info < (3, 0):
reload(sys)
sys.setdefaultencoding('utf8')
else:
raw_input = input
class IBBReceiver(sleekxmpp.ClientXMPP):
"""
A basic example of creating and using an in-band bytestream.
"""
def __init__(self, jid, password):
sleekxmpp.ClientXMPP.__init__(self, jid, password)
self.register_plugin('xep_0030') # Service Discovery
self.register_plugin('xep_0047', {
'accept_stream': self.accept_stream
}) # In-band Bytestreams
# The session_start event will be triggered when
# the bot establishes its connection with the server
# and the XML streams are ready for use. We want to
# listen for this event so that we we can initialize
# our roster.
self.add_event_handler("session_start", self.start)
self.add_event_handler("ibb_stream_start", self.stream_opened)
self.add_event_handler("ibb_stream_data", self.stream_data)
def start(self, event):
"""
Process the session_start event.
Typical actions for the session_start event are
requesting the roster and broadcasting an initial
presence stanza.
Arguments:
event -- An empty dictionary. The session_start
event does not provide any additional
data.
"""
self.send_presence()
self.get_roster()
def accept_stream(self, iq):
"""
Check that it is ok to accept a stream request.
Controlling stream acceptance can be done via either:
- setting 'auto_accept' to False in the plugin
configuration. The default is True.
- setting 'accept_stream' to a function which accepts
an Iq stanza as its argument, like this one.
The accept_stream function will be used if it exists, and the
auto_accept value will be used otherwise.
"""
return True
def stream_opened(self, stream):
# NOTE: IBB streams are bi-directional, so the original sender is
# now the opened stream's receiver.
print('Stream opened: %s from ' % (stream.sid, stream.receiver))
# You could run a loop reading from the stream using stream.recv(),
# or use the ibb_stream_data event.
def stream_data(self, event):
print(event['data'])
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')
if opts.jid is None:
opts.jid = raw_input("Username: ")
if opts.password is None:
opts.password = getpass.getpass("Password: ")
xmpp = IBBReceiver(opts.jid, opts.password)
# If you are working with an OpenFire server, you may need
# to adjust the SSL version used:
# xmpp.ssl_version = ssl.PROTOCOL_SSLv3
# If you want to verify the SSL certificates offered by a server:
# xmpp.ca_certs = "path/to/ca/cert"
# Connect to the XMPP server and start processing XMPP stanzas.
if xmpp.connect():
# If you do not have the dnspython library installed, you will need
# to manually specify the name of the server if it does not match
# the one in the JID. For example, to use Google Talk you would
# need to use:
#
# if xmpp.connect(('talk.google.com', 5222)):
# ...
xmpp.process(block=True)
print("Done")
else:
print("Unable to connect.")

View File

@@ -0,0 +1,145 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
SleekXMPP: The Sleek XMPP Library
Copyright (C) 2010 Nathanael C. Fritz
This file is part of SleekXMPP.
See the file LICENSE for copying permission.
"""
import sys
import logging
import getpass
from optparse import OptionParser
import sleekxmpp
# Python versions before 3.0 do not use UTF-8 encoding
# by default. To ensure that Unicode is handled properly
# throughout SleekXMPP, we will set the default encoding
# ourselves to UTF-8.
if sys.version_info < (3, 0):
reload(sys)
sys.setdefaultencoding('utf8')
else:
raw_input = input
class IBBSender(sleekxmpp.ClientXMPP):
"""
A basic example of creating and using an in-band bytestream.
"""
def __init__(self, jid, password, receiver, filename):
sleekxmpp.ClientXMPP.__init__(self, jid, password)
self.receiver = receiver
self.filename = filename
# The session_start event will be triggered when
# the bot establishes its connection with the server
# and the XML streams are ready for use. We want to
# listen for this event so that we we can initialize
# our roster.
self.add_event_handler("session_start", self.start)
def start(self, event):
"""
Process the session_start event.
Typical actions for the session_start event are
requesting the roster and broadcasting an initial
presence stanza.
Arguments:
event -- An empty dictionary. The session_start
event does not provide any additional
data.
"""
self.send_presence()
self.get_roster()
# For the purpose of demonstration, we'll set a very small block
# size. The default block size is 4096. We'll also use a window
# allowing sending multiple blocks at a time; in this case, three
# block transfers may be in progress at any time.
stream = self['xep_0047'].open_stream(self.receiver)
with open(self.filename) as f:
data = f.read()
stream.sendall(data)
if __name__ == '__main__':
# Setup the command line arguments.
optp = OptionParser()
# Output verbosity options.
optp.add_option('-q', '--quiet', help='set logging to ERROR',
action='store_const', dest='loglevel',
const=logging.ERROR, default=logging.INFO)
optp.add_option('-d', '--debug', help='set logging to DEBUG',
action='store_const', dest='loglevel',
const=logging.DEBUG, default=logging.INFO)
optp.add_option('-v', '--verbose', help='set logging to COMM',
action='store_const', dest='loglevel',
const=5, default=logging.INFO)
# JID and password options.
optp.add_option("-j", "--jid", dest="jid",
help="JID to use")
optp.add_option("-p", "--password", dest="password",
help="password to use")
optp.add_option("-r", "--receiver", dest="receiver",
help="JID to use")
optp.add_option("-f", "--file", dest="filename",
help="JID to use")
opts, args = optp.parse_args()
# Setup logging.
logging.basicConfig(level=opts.loglevel,
format='%(levelname)-8s %(message)s')
if opts.jid is None:
opts.jid = raw_input("Username: ")
if opts.password is None:
opts.password = getpass.getpass("Password: ")
if opts.receiver is None:
opts.receiver = raw_input("Receiver: ")
if opts.filename is None:
opts.filename = raw_input("File path: ")
# Setup the EchoBot and register plugins. Note that while plugins may
# have interdependencies, the order in which you register them does
# not matter.
xmpp = IBBSender(opts.jid, opts.password, opts.receiver, opts.filename)
xmpp.register_plugin('xep_0030') # Service Discovery
xmpp.register_plugin('xep_0004') # Data Forms
xmpp.register_plugin('xep_0047') # In-band Bytestreams
xmpp.register_plugin('xep_0060') # PubSub
xmpp.register_plugin('xep_0199') # XMPP Ping
# If you are working with an OpenFire server, you may need
# to adjust the SSL version used:
# xmpp.ssl_version = ssl.PROTOCOL_SSLv3
# If you want to verify the SSL certificates offered by a server:
# xmpp.ca_certs = "path/to/ca/cert"
# Connect to the XMPP server and start processing XMPP stanzas.
if xmpp.connect():
# If you do not have the dnspython library installed, you will need
# to manually specify the name of the server if it does not match
# the one in the JID. For example, to use Google Talk you would
# need to use:
#
# if xmpp.connect(('talk.google.com', 5222)):
# ...
xmpp.process(block=True)
print("Done")
else:
print("Unable to connect.")

View File

@@ -76,8 +76,8 @@ class MUCBot(sleekxmpp.ClientXMPP):
event does not provide any additional
data.
"""
self.getRoster()
self.sendPresence()
self.get_roster()
self.send_presence()
self.plugin['xep_0045'].joinMUC(self.room,
self.nick,
# If a room password is needed, use:

View File

@@ -45,7 +45,7 @@ class PingTest(sleekxmpp.ClientXMPP):
# and the XML streams are ready for use. We want to
# listen for this event so that we we can initialize
# our roster.
self.add_event_handler("session_start", self.start)
self.add_event_handler("session_start", self.start, threaded=True)
def start(self, event):
"""

198
examples/pubsub_client.py Normal file
View File

@@ -0,0 +1,198 @@
import sys
import logging
import getpass
from optparse import OptionParser
import sleekxmpp
from sleekxmpp.xmlstream import ET, tostring
# Python versions before 3.0 do not use UTF-8 encoding
# by default. To ensure that Unicode is handled properly
# throughout SleekXMPP, we will set the default encoding
# ourselves to UTF-8.
if sys.version_info < (3, 0):
reload(sys)
sys.setdefaultencoding('utf8')
else:
raw_input = input
class PubsubClient(sleekxmpp.ClientXMPP):
def __init__(self, jid, password, server,
node=None, action='list', data=''):
super(PubsubClient, self).__init__(jid, password)
self.register_plugin('xep_0030')
self.register_plugin('xep_0059')
self.register_plugin('xep_0060')
self.actions = ['nodes', 'create', 'delete',
'publish', 'get', 'retract',
'purge', 'subscribe', 'unsubscribe']
self.action = action
self.node = node
self.data = data
self.pubsub_server = server
self.add_event_handler('session_start', self.start, threaded=True)
def start(self, event):
self.get_roster()
self.send_presence()
try:
getattr(self, self.action)()
except:
logging.error('Could not execute: %s' % self.action)
self.disconnect()
def nodes(self):
try:
result = self['xep_0060'].get_nodes(self.pubsub_server, self.node)
for item in result['disco_items']['items']:
print(' - %s' % str(item))
except:
logging.error('Could not retrieve node list.')
def create(self):
try:
self['xep_0060'].create_node(self.pubsub_server, self.node)
except:
logging.error('Could not create node: %s' % self.node)
def delete(self):
try:
self['xep_0060'].delete_node(self.pubsub_server, self.node)
print('Deleted node: %s' % self.node)
except:
logging.error('Could not delete node: %s' % self.node)
def publish(self):
payload = ET.fromstring("<test xmlns='test'>%s</test>" % self.data)
try:
result = self['xep_0060'].publish(self.pubsub_server, self.node, payload=payload)
id = result['pubsub']['publish']['item']['id']
print('Published at item id: %s' % id)
except:
logging.error('Could not publish to: %s' % self.node)
def get(self):
try:
result = self['xep_0060'].get_item(self.pubsub_server, self.node, self.data)
for item in result['pubsub']['items']['substanzas']:
print('Retrieved item %s: %s' % (item['id'], tostring(item['payload'])))
except:
logging.error('Could not retrieve item %s from node %s' % (self.data, self.node))
def retract(self):
try:
result = self['xep_0060'].retract(self.pubsub_server, self.node, self.data)
print('Retracted item %s from node %s' % (self.data, self.node))
except:
logging.error('Could not retract item %s from node %s' % (self.data, self.node))
def purge(self):
try:
result = self['xep_0060'].purge(self.pubsub_server, self.node)
print('Purged all items from node %s' % self.node)
except:
logging.error('Could not purge items from node %s' % self.node)
def subscribe(self):
try:
result = self['xep_0060'].subscribe(self.pubsub_server, self.node)
print('Subscribed %s to node %s' % (self.boundjid.bare, self.node))
except:
logging.error('Could not subscribe %s to node %s' % (self.boundjid.bare, self.node))
def unsubscribe(self):
try:
result = self['xep_0060'].unsubscribe(self.pubsub_server, self.node)
print('Unsubscribed %s from node %s' % (self.boundjid.bare, self.node))
except:
logging.error('Could not unsubscribe %s from node %s' % (self.boundjid.bare, self.node))
if __name__ == '__main__':
# Setup the command line arguments.
optp = OptionParser()
optp.version = '%%prog 0.1'
optp.usage = "Usage: %%prog [options] <jid> " + \
'nodes|create|delete|purge|subscribe|unsubscribe|publish|retract|get' + \
' [<node> <data>]'
optp.add_option('-q','--quiet', help='set logging to ERROR',
action='store_const',
dest='loglevel',
const=logging.ERROR,
default=logging.ERROR)
optp.add_option('-d','--debug', help='set logging to DEBUG',
action='store_const',
dest='loglevel',
const=logging.DEBUG,
default=logging.ERROR)
optp.add_option('-v','--verbose', help='set logging to COMM',
action='store_const',
dest='loglevel',
const=5,
default=logging.ERROR)
# JID and password options.
optp.add_option("-j", "--jid", dest="jid",
help="JID to use")
optp.add_option("-p", "--password", dest="password",
help="password to use")
opts,args = optp.parse_args()
# Setup logging.
logging.basicConfig(level=opts.loglevel,
format='%(levelname)-8s %(message)s')
if len(args) < 2:
optp.print_help()
exit()
if opts.jid is None:
opts.jid = raw_input("Username: ")
if opts.password is None:
opts.password = getpass.getpass("Password: ")
if len(args) == 2:
args = (args[0], args[1], '', '', '')
elif len(args) == 3:
args = (args[0], args[1], args[2], '', '')
elif len(args) == 4:
args = (args[0], args[1], args[2], args[3], '')
# Setup the Pubsub client
xmpp = PubsubClient(opts.jid, opts.password,
server=args[0],
node=args[2],
action=args[1],
data=args[3])
# If you are working with an OpenFire server, you may need
# to adjust the SSL version used:
# xmpp.ssl_version = ssl.PROTOCOL_SSLv3
# If you want to verify the SSL certificates offered by a server:
# xmpp.ca_certs = "path/to/ca/cert"
# Connect to the XMPP server and start processing XMPP stanzas.
if xmpp.connect():
# If you do not have the dnspython library installed, you will need
# to manually specify the name of the server if it does not match
# the one in the JID. For example, to use Google Talk you would
# need to use:
#
# if xmpp.connect(('talk.google.com', 5222)):
# ...
xmpp.process(block=True)
else:
print("Unable to connect.")

151
examples/pubsub_events.py Normal file
View File

@@ -0,0 +1,151 @@
import sys
import logging
import getpass
from optparse import OptionParser
import sleekxmpp
from sleekxmpp.xmlstream import ET, tostring
from sleekxmpp.xmlstream.matcher import StanzaPath
from sleekxmpp.xmlstream.handler import Callback
# Python versions before 3.0 do not use UTF-8 encoding
# by default. To ensure that Unicode is handled properly
# throughout SleekXMPP, we will set the default encoding
# ourselves to UTF-8.
if sys.version_info < (3, 0):
reload(sys)
sys.setdefaultencoding('utf8')
else:
raw_input = input
class PubsubEvents(sleekxmpp.ClientXMPP):
def __init__(self, jid, password):
super(PubsubEvents, self).__init__(jid, password)
self.register_plugin('xep_0030')
self.register_plugin('xep_0059')
self.register_plugin('xep_0060')
self.add_event_handler('session_start', self.start)
# Some services may require configuration to allow
# sending delete, configuration, or subscription events.
self.add_event_handler('pubsub_publish', self._publish)
self.add_event_handler('pubsub_retract', self._retract)
self.add_event_handler('pubsub_purge', self._purge)
self.add_event_handler('pubsub_delete', self._delete)
self.add_event_handler('pubsub_config', self._config)
self.add_event_handler('pubsub_subscription', self._subscription)
# Want to use nicer, more specific pubsub event names?
# self['xep_0060'].map_node_event('node_name', 'event_prefix')
# self.add_event_handler('event_prefix_publish', handler)
# self.add_event_handler('event_prefix_retract', handler)
# self.add_event_handler('event_prefix_purge', handler)
# self.add_event_handler('event_prefix_delete', handler)
def start(self, event):
self.get_roster()
self.send_presence()
def _publish(self, msg):
"""Handle receiving a publish item event."""
print('Published item %s to %s:' % (
msg['pubsub_event']['items']['item']['id'],
msg['pubsub_event']['items']['node']))
data = msg['pubsub_event']['items']['item']['payload']
if data is not None:
print(tostring(data))
else:
print('No item content')
def _retract(self, msg):
"""Handle receiving a retract item event."""
print('Retracted item %s from %s' % (
msg['pubsub_event']['items']['retract']['id'],
msg['pubsub_event']['items']['node']))
def _purge(self, msg):
"""Handle receiving a node purge event."""
print('Purged all items from %s' % (
msg['pubsub_event']['purge']['node']))
def _delete(self, msg):
"""Handle receiving a node deletion event."""
print('Deleted node %s' % (
msg['pubsub_event']['delete']['node']))
def _config(self, msg):
"""Handle receiving a node configuration event."""
print('Configured node %s:' % (
msg['pubsub_event']['configuration']['node']))
print(msg['pubsub_event']['configuration']['form'])
def _subscription(self, msg):
"""Handle receiving a node subscription event."""
print('Subscription change for node %s:' % (
msg['pubsub_event']['subscription']['node']))
print(msg['pubsub_event']['subscription'])
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')
if opts.jid is None:
opts.jid = raw_input("Username: ")
if opts.password is None:
opts.password = getpass.getpass("Password: ")
logging.info("Run this in conjunction with the pubsub_client.py " + \
"example to watch events happen as you give commands.")
# Setup the PubsubEvents listener
xmpp = PubsubEvents(opts.jid, opts.password)
# If you are working with an OpenFire server, you may need
# to adjust the SSL version used:
# xmpp.ssl_version = ssl.PROTOCOL_SSLv3
# If you want to verify the SSL certificates offered by a server:
# xmpp.ca_certs = "path/to/ca/cert"
# Connect to the XMPP server and start processing XMPP stanzas.
if xmpp.connect():
# If you do not have the dnspython library installed, you will need
# to manually specify the name of the server if it does not match
# the one in the JID. For example, to use Google Talk you would
# need to use:
#
# if xmpp.connect(('talk.google.com', 5222)):
# ...
xmpp.process(block=True)
print("Done")
else:
print("Unable to connect.")

View File

@@ -0,0 +1,175 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
SleekXMPP: The Sleek XMPP Library
Copyright (C) 2010 Nathanael C. Fritz
This file is part of SleekXMPP.
See the file LICENSE for copying permission.
"""
import sys
import logging
import getpass
from optparse import OptionParser
import sleekxmpp
from sleekxmpp.exceptions import IqError, IqTimeout
# Python versions before 3.0 do not use UTF-8 encoding
# by default. To ensure that Unicode is handled properly
# throughout SleekXMPP, we will set the default encoding
# ourselves to UTF-8.
if sys.version_info < (3, 0):
reload(sys)
sys.setdefaultencoding('utf8')
else:
raw_input = input
class RegisterBot(sleekxmpp.ClientXMPP):
"""
A basic bot that will attempt to register an account
with an XMPP server.
NOTE: This follows the very basic registration workflow
from XEP-0077. More advanced server registration
workflows will need to check for data forms, etc.
"""
def __init__(self, jid, password):
sleekxmpp.ClientXMPP.__init__(self, jid, password)
# The session_start event will be triggered when
# the bot establishes its connection with the server
# and the XML streams are ready for use. We want to
# listen for this event so that we we can initialize
# our roster.
self.add_event_handler("session_start", self.start, threaded=True)
# The register event provides an Iq result stanza with
# a registration form from the server. This may include
# the basic registration fields, a data form, an
# out-of-band URL, or any combination. For more advanced
# cases, you will need to examine the fields provided
# and respond accordingly. SleekXMPP provides plugins
# for data forms and OOB links that will make that easier.
self.add_event_handler("register", self.register, threaded=True)
def start(self, event):
"""
Process the session_start event.
Typical actions for the session_start event are
requesting the roster and broadcasting an initial
presence stanza.
Arguments:
event -- An empty dictionary. The session_start
event does not provide any additional
data.
"""
self.send_presence()
self.get_roster()
# We're only concerned about registering, so nothing more to do here.
self.disconnect()
def register(self, iq):
"""
Fill out and submit a registration form.
The form may be composed of basic registration fields, a data form,
an out-of-band link, or any combination thereof. Data forms and OOB
links can be checked for as so:
if iq.match('iq/register/form'):
# do stuff with data form
# iq['register']['form']['fields']
if iq.match('iq/register/oob'):
# do stuff with OOB URL
# iq['register']['oob']['url']
To get the list of basic registration fields, you can use:
iq['register']['fields']
"""
resp = self.Iq()
resp['type'] = 'set'
resp['register']['username'] = self.boundjid.user
resp['register']['password'] = self.password
try:
resp.send(now=True)
logging.info("Account created for %s!" % self.boundjid)
except IqError as e:
logging.error("Could not register account: %s" %
e.iq['error']['text'])
self.disconnect()
except IqTimeout:
logging.error("No response from server.")
self.disconnect()
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')
if opts.jid is None:
opts.jid = raw_input("Username: ")
if opts.password is None:
opts.password = getpass.getpass("Password: ")
# Setup the RegisterBot and register plugins. Note that while plugins may
# have interdependencies, the order in which you register them does
# not matter.
xmpp = RegisterBot(opts.jid, opts.password)
xmpp.register_plugin('xep_0030') # Service Discovery
xmpp.register_plugin('xep_0004') # Data forms
xmpp.register_plugin('xep_0066') # Out-of-band Data
xmpp.register_plugin('xep_0077') # In-band Registration
# If you are working with an OpenFire server, you may need
# to adjust the SSL version used:
# xmpp.ssl_version = ssl.PROTOCOL_SSLv3
# If you want to verify the SSL certificates offered by a server:
# xmpp.ca_certs = "path/to/ca/cert"
# Connect to the XMPP server and start processing XMPP stanzas.
if xmpp.connect():
# If you do not have the dnspython library installed, you will need
# to manually specify the name of the server if it does not match
# the one in the JID. For example, to use Google Talk you would
# need to use:
#
# if xmpp.connect(('talk.google.com', 5222)):
# ...
xmpp.process(block=True)
print("Done")
else:
print("Unable to connect.")

View File

@@ -16,7 +16,7 @@ class Thermostat(Endpoint):
def FQN(self):
return 'thermostat'
def __init(self, initial_temperature):
def __init__(self, initial_temperature):
self._temperature = initial_temperature
self._event = threading.Event()
@@ -50,4 +50,4 @@ def main():
if __name__ == '__main__':
main()

View File

@@ -15,7 +15,7 @@ class Thermostat(Endpoint):
def FQN(self):
return 'thermostat'
def __init(self, initial_temperature):
def __init__(self, initial_temperature):
self._temperature = initial_temperature
self._event = threading.Event()
@@ -49,4 +49,4 @@ def main():
if __name__ == '__main__':
main()

View File

@@ -47,7 +47,7 @@ class SendMsgBot(sleekxmpp.ClientXMPP):
# and the XML streams are ready for use. We want to
# listen for this event so that we we can initialize
# our roster.
self.add_event_handler("session_start", self.start)
self.add_event_handler("session_start", self.start, threaded=True)
def start(self, event):
"""

174
examples/set_avatar.py Normal file
View File

@@ -0,0 +1,174 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
SleekXMPP: The Sleek XMPP Library
Copyright (C) 2012 Nathanael C. Fritz
This file is part of SleekXMPP.
See the file LICENSE for copying permission.
"""
import os
import sys
import imghdr
import logging
import getpass
import threading
from optparse import OptionParser
import sleekxmpp
from sleekxmpp.exceptions import XMPPError
# Python versions before 3.0 do not use UTF-8 encoding
# by default. To ensure that Unicode is handled properly
# throughout SleekXMPP, we will set the default encoding
# ourselves to UTF-8.
if sys.version_info < (3, 0):
reload(sys)
sys.setdefaultencoding('utf8')
else:
raw_input = input
class AvatarSetter(sleekxmpp.ClientXMPP):
"""
A basic script for downloading the avatars for a user's contacts.
"""
def __init__(self, jid, password, filepath):
sleekxmpp.ClientXMPP.__init__(self, jid, password)
self.add_event_handler("session_start", self.start, threaded=True)
self.filepath = filepath
def start(self, event):
"""
Process the session_start event.
Typical actions for the session_start event are
requesting the roster and broadcasting an initial
presence stanza.
Arguments:
event -- An empty dictionary. The session_start
event does not provide any additional
data.
"""
self.send_presence()
self.get_roster()
avatar_file = None
try:
avatar_file = open(os.path.expanduser(self.filepath))
except IOError:
print('Could not find file: %s' % self.filepath)
return self.disconnect()
avatar = avatar_file.read()
avatar_type = 'image/%s' % imghdr.what('', avatar)
avatar_id = self['xep_0084'].generate_id(avatar)
avatar_bytes = len(avatar)
avatar_file.close()
used_xep84 = False
try:
print('Publish XEP-0084 avatar data')
self['xep_0084'].publish_avatar(avatar)
used_xep84 = True
except XMPPError:
print('Could not publish XEP-0084 avatar')
try:
print('Update vCard with avatar')
self['xep_0153'].set_avatar(avatar=avatar, mtype=avatar_type)
except XMPPError:
print('Could not set vCard avatar')
if used_xep84:
try:
print('Advertise XEP-0084 avatar metadata')
self['xep_0084'].publish_avatar_metadata([
{'id': avatar_id,
'type': avatar_type,
'bytes': avatar_bytes}
# We could advertise multiple avatars to provide
# options in image type, source (HTTP vs pubsub),
# size, etc.
# {'id': ....}
])
except XMPPError:
print('Could not publish XEP-0084 metadata')
print('Wait for presence updates to propagate...')
self.schedule('end', 5, self.disconnect, kwargs={'wait': True})
if __name__ == '__main__':
# Setup the command line arguments.
optp = OptionParser()
optp.add_option('-q','--quiet', help='set logging to ERROR',
action='store_const',
dest='loglevel',
const=logging.ERROR,
default=logging.ERROR)
optp.add_option('-d','--debug', help='set logging to DEBUG',
action='store_const',
dest='loglevel',
const=logging.DEBUG,
default=logging.ERROR)
optp.add_option('-v','--verbose', help='set logging to COMM',
action='store_const',
dest='loglevel',
const=5,
default=logging.ERROR)
# JID and password options.
optp.add_option("-j", "--jid", dest="jid",
help="JID to use")
optp.add_option("-p", "--password", dest="password",
help="password to use")
optp.add_option("-f", "--file", dest="filepath",
help="path to the avatar file")
opts,args = optp.parse_args()
# Setup logging.
logging.basicConfig(level=opts.loglevel,
format='%(levelname)-8s %(message)s')
if opts.jid is None:
opts.jid = raw_input("Username: ")
if opts.password is None:
opts.password = getpass.getpass("Password: ")
if opts.filepath is None:
opts.filepath = raw_input("Avatar file location: ")
xmpp = AvatarSetter(opts.jid, opts.password, opts.filepath)
xmpp.register_plugin('xep_0054')
xmpp.register_plugin('xep_0153')
xmpp.register_plugin('xep_0084')
# If you are working with an OpenFire server, you may need
# to adjust the SSL version used:
# xmpp.ssl_version = ssl.PROTOCOL_SSLv3
# If you want to verify the SSL certificates offered by a server:
# xmpp.ca_certs = "path/to/ca/cert"
# Connect to the XMPP server and start processing XMPP stanzas.
if xmpp.connect():
# If you do not have the dnspython library installed, you will need
# to manually specify the name of the server if it does not match
# the one in the JID. For example, to use Google Talk you would
# need to use:
#
# if xmpp.connect(('talk.google.com', 5222)):
# ...
xmpp.process(block=True)
else:
print("Unable to connect.")

247
examples/thirdparty_auth.py Normal file
View File

@@ -0,0 +1,247 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
SleekXMPP: The Sleek XMPP Library
Copyright (C) 2010 Nathanael C. Fritz
This file is part of SleekXMPP.
See the file LICENSE for copying permission.
"""
import sys
import logging
import getpass
from optparse import OptionParser
try:
from httplib import HTTPSConnection
from urllib import urlencode
except ImportError:
from urllib.parse import urlencode
from http.client import HTTPSConnection
import sleekxmpp
from sleekxmpp.xmlstream import JID
# Python versions before 3.0 do not use UTF-8 encoding
# by default. To ensure that Unicode is handled properly
# throughout SleekXMPP, we will set the default encoding
# ourselves to UTF-8.
if sys.version_info < (3, 0):
reload(sys)
sys.setdefaultencoding('utf8')
else:
raw_input = input
class ThirdPartyAuthBot(sleekxmpp.ClientXMPP):
"""
A simple SleekXMPP bot that will echo messages it
receives, along with a short thank you message.
This version uses a thirdpary service for authentication,
such as Facebook or Google.
"""
def __init__(self, jid, password):
sleekxmpp.ClientXMPP.__init__(self, jid, password)
# The X-GOOGLE-TOKEN mech is ranked lower than PLAIN
# due to Google only allowing a single SASL attempt per
# connection. So PLAIN will be used for TLS connections,
# and X-GOOGLE-TOKEN for non-TLS connections. To use
# X-GOOGLE-TOKEN with a TLS connection, explicitly select
# it using:
#
# sleekxmpp.ClientXMPP.__init__(self, jid, password,
# sasl_mech="X-GOOGLE-TOKEN")
# The session_start event will be triggered when
# the bot establishes its connection with the server
# and the XML streams are ready for use. We want to
# listen for this event so that we we can initialize
# our roster.
self.add_event_handler("session_start", self.start)
# 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 initial
presence stanza.
Arguments:
event -- An empty dictionary. The session_start
event does not provide any additional
data.
"""
self.send_presence()
self.get_roster()
def message(self, msg):
"""
Process incoming message stanzas. Be aware that this also
includes MUC messages and error messages. It is usually
a good idea to check the messages's type before processing
or sending replies.
Arguments:
msg -- The received message stanza. See the documentation
for stanza objects and the Message stanza to see
how it may be used.
"""
if msg['type'] in ('chat', 'normal'):
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')
if opts.jid is None:
opts.jid = raw_input("Username: ")
if opts.password is None:
opts.password = getpass.getpass("Password: ")
access_token = None
# Since documentation on how to work with Google tokens
# can be difficult to find, we'll demo a basic version
# here. Note that responses could refer to a Captcha
# URL that would require a browser.
# Using Facebook or MSN's custom authentication requires
# a browser, but the process is the same once a token
# has been retrieved.
# Request an access token from Google:
try:
conn = HTTPSConnection('www.google.com')
except:
print('Could not connect to Google')
sys.exit()
params = urlencode({
'accountType': 'GOOGLE',
'service': 'mail',
'Email': JID(opts.jid).bare,
'Passwd': opts.password
})
headers = {
'Content-Type': 'application/x-www-form-urlencoded'
}
try:
conn.request('POST', '/accounts/ClientLogin', params, headers)
resp = conn.getresponse().read()
data = {}
for line in resp.split():
k, v = line.split(b'=', 1)
data[k] = v
except Exception as e:
print('Could not retrieve login data')
sys.exit()
if b'SID' not in data:
print('Required data not found')
sys.exit()
params = urlencode({
'SID': data[b'SID'],
'LSID': data[b'LSID'],
'service': 'mail'
})
try:
conn.request('POST', '/accounts/IssueAuthToken', params, headers)
resp = conn.getresponse()
data = resp.read().split()
except:
print('Could not retrieve auth data')
sys.exit()
if not data:
print('Could not retrieve token')
sys.exit()
access_token = data[0]
# Setup the ThirdPartyAuthBot and register plugins. Note that while plugins
# may have interdependencies, the order in which you register them does not
# matter.
# If using MSN, the JID should be "user@messenger.live.com", which will
# be overridden on session bind.
# We're using an access token instead of a password, so we'll use `''` as
# a password argument filler.
xmpp = ThirdPartyAuthBot(opts.jid, '')
xmpp.credentials['access_token'] = access_token
# The credentials dictionary is used to provide additional authentication
# information beyond just a password.
xmpp.register_plugin('xep_0030') # Service Discovery
xmpp.register_plugin('xep_0004') # Data Forms
xmpp.register_plugin('xep_0060') # PubSub
# MSN will kill connections that have been inactive for even
# short periods of time. So use pings to keep the session alive;
# whitespace keepalives do not work.
xmpp.register_plugin('xep_0199', {'keepalive': True, 'frequency': 60})
# If you are working with an OpenFire server, you may need
# to adjust the SSL version used:
# xmpp.ssl_version = ssl.PROTOCOL_SSLv3
# If you want to verify the SSL certificates offered by a server:
# xmpp.ca_certs = "path/to/ca/cert"
# Connect to the XMPP server and start processing XMPP stanzas.
# Google only allows one SASL attempt per connection, so in order to
# enable the X-GOOGLE-TOKEN mechanism, we'll disable TLS.
if xmpp.connect(use_tls=False):
# If you do not have the dnspython library installed, you will need
# to manually specify the name of the server if it does not match
# the one in the JID. For example, to use Google Talk you would
# need to use:
#
# if xmpp.connect(('talk.google.com', 5222)):
# ...
xmpp.process(block=True)
print("Done")
else:
print("Unable to connect.")

125
examples/user_location.py Normal file
View File

@@ -0,0 +1,125 @@
#!/usr/bin/env python
import sys
import logging
import getpass
from optparse import OptionParser
try:
import json
except ImportError:
import simplejson as json
try:
import requests
except ImportError:
print('This demo requires the requests package for using HTTP.')
sys.exit()
from sleekxmpp import ClientXMPP
class LocationBot(ClientXMPP):
def __init__(self, jid, password):
super(LocationBot, self).__init__(jid, password)
self.add_event_handler('session_start', self.start, threaded=True)
self.add_event_handler('user_location_publish',
self.user_location_publish)
self.register_plugin('xep_0004')
self.register_plugin('xep_0030')
self.register_plugin('xep_0060')
self.register_plugin('xep_0115')
self.register_plugin('xep_0128')
self.register_plugin('xep_0163')
self.register_plugin('xep_0080')
self.current_tune = None
def start(self, event):
self.send_presence()
self.get_roster()
self['xep_0115'].update_caps()
print("Using freegeoip.net to get geolocation.")
r = requests.get('http://freegeoip.net/json/')
try:
data = json.loads(r.text)
except:
print("Could not retrieve user location.")
self.disconnect()
return
self['xep_0080'].publish_location(
lat=data['latitude'],
lon=data['longitude'],
locality=data['city'],
region=data['region_name'],
country=data['country_name'],
countrycode=data['country_code'],
postalcode=data['zipcode'])
def user_location_publish(self, msg):
geo = msg['pubsub_event']['items']['item']['geoloc']
print("%s is at:" % msg['from'])
for key, val in geo.values.items():
if val:
print(" %s: %s" % (key, val))
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')
if opts.jid is None:
opts.jid = raw_input("Username: ")
if opts.password is None:
opts.password = getpass.getpass("Password: ")
xmpp = LocationBot(opts.jid, opts.password)
# If you are working with an OpenFire server, you may need
# to adjust the SSL version used:
# xmpp.ssl_version = ssl.PROTOCOL_SSLv3
# If you want to verify the SSL certificates offered by a server:
# xmpp.ca_certs = "path/to/ca/cert"
# Connect to the XMPP server and start processing XMPP stanzas.
if xmpp.connect():
# If you do not have the dnspython library installed, you will need
# to manually specify the name of the server if it does not match
# the one in the JID. For example, to use Google Talk you would
# need to use:
#
# if xmpp.connect(('talk.google.com', 5222)):
# ...
xmpp.process(block=True)
print("Done")
else:
print("Unable to connect.")

137
examples/user_tune.py Normal file
View File

@@ -0,0 +1,137 @@
#!/usr/bin/env python
import sys
import logging
import getpass
from optparse import OptionParser
try:
from appscript import *
except ImportError:
print('This demo requires the appscript package to interact with iTunes.')
sys.exit()
from sleekxmpp import ClientXMPP
class TuneBot(ClientXMPP):
def __init__(self, jid, password):
super(TuneBot, self).__init__(jid, password)
# Check for the current song every 5 seconds.
self.schedule('Check Current Tune', 5, self._update_tune, repeat=True)
self.add_event_handler('session_start', self.start)
self.add_event_handler('user_tune_publish', self.user_tune_publish)
self.register_plugin('xep_0004')
self.register_plugin('xep_0030')
self.register_plugin('xep_0060')
self.register_plugin('xep_0115')
self.register_plugin('xep_0118')
self.register_plugin('xep_0128')
self.register_plugin('xep_0163')
self.current_tune = None
def start(self, event):
self.send_presence()
self.get_roster()
self['xep_0115'].update_caps()
def _update_tune(self):
itunes_count = app('System Events').processes[its.name == 'iTunes'].count()
if itunes_count > 0:
iTunes = app('iTunes')
if iTunes.player_state.get() == k.playing:
track = iTunes.current_track.get()
length = track.time.get()
if ':' in length:
minutes, secs = map(int, length.split(':'))
secs += minutes * 60
else:
secs = int(length)
artist = track.artist.get()
title = track.name.get()
source = track.album.get()
rating = track.rating.get() / 10
tune = (artist, secs, rating, source, title)
if tune != self.current_tune:
self.current_tune = tune
# We have a new song playing, so publish it.
self['xep_0118'].publish_tune(
artist=artist,
length=secs,
title=title,
rating=rating,
source=source)
else:
# No song is playing, clear the user tune.
tune = None
if tune != self.current_tune:
self.current_tune = tune
self['xep_0118'].stop()
def user_tune_publish(self, msg):
tune = msg['pubsub_event']['items']['item']['tune']
print("%s is listening to: %s" % (msg['from'], tune['title']))
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')
if opts.jid is None:
opts.jid = raw_input("Username: ")
if opts.password is None:
opts.password = getpass.getpass("Password: ")
xmpp = TuneBot(opts.jid, opts.password)
# If you are working with an OpenFire server, you may need
# to adjust the SSL version used:
# xmpp.ssl_version = ssl.PROTOCOL_SSLv3
# If you want to verify the SSL certificates offered by a server:
# xmpp.ca_certs = "path/to/ca/cert"
# Connect to the XMPP server and start processing XMPP stanzas.
if xmpp.connect():
# If you do not have the dnspython library installed, you will need
# to manually specify the name of the server if it does not match
# the one in the JID. For example, to use Google Talk you would
# need to use:
#
# if xmpp.connect(('talk.google.com', 5222)):
# ...
xmpp.process(block=True)
print("Done")
else:
print("Unable to connect.")

View File

@@ -8,6 +8,7 @@
# file, which you should have received as part of this distribution.
import sys
import codecs
try:
from setuptools import setup, Command
except ImportError:
@@ -31,7 +32,7 @@ from sleekxmpp.version import __version__
VERSION = __version__
DESCRIPTION = 'SleekXMPP is an elegant Python library for XMPP (aka Jabber, Google Talk, etc).'
with open('README.rst') as readme:
with codecs.open('README.rst', 'r', encoding='UTF-8') as readme:
LONG_DESCRIPTION = ''.join(readme)
CLASSIFIERS = [ 'Intended Audience :: Developers',
@@ -48,6 +49,7 @@ packages = [ 'sleekxmpp',
'sleekxmpp/stanza',
'sleekxmpp/test',
'sleekxmpp/roster',
'sleekxmpp/util',
'sleekxmpp/xmlstream',
'sleekxmpp/xmlstream/matcher',
'sleekxmpp/xmlstream/handler',
@@ -56,29 +58,52 @@ packages = [ 'sleekxmpp',
'sleekxmpp/plugins/xep_0004/stanza',
'sleekxmpp/plugins/xep_0009',
'sleekxmpp/plugins/xep_0009/stanza',
'sleekxmpp/plugins/xep_0012',
'sleekxmpp/plugins/xep_0027',
'sleekxmpp/plugins/xep_0030',
'sleekxmpp/plugins/xep_0030/stanza',
'sleekxmpp/plugins/xep_0033',
'sleekxmpp/plugins/xep_0047',
'sleekxmpp/plugins/xep_0050',
'sleekxmpp/plugins/xep_0054',
'sleekxmpp/plugins/xep_0059',
'sleekxmpp/plugins/xep_0060',
'sleekxmpp/plugins/xep_0060/stanza',
'sleekxmpp/plugins/xep_0066',
'sleekxmpp/plugins/xep_0077',
'sleekxmpp/plugins/xep_0078',
'sleekxmpp/plugins/xep_0080',
'sleekxmpp/plugins/xep_0084',
'sleekxmpp/plugins/xep_0085',
'sleekxmpp/plugins/xep_0086',
'sleekxmpp/plugins/xep_0092',
'sleekxmpp/plugins/xep_0107',
'sleekxmpp/plugins/xep_0108',
'sleekxmpp/plugins/xep_0115',
'sleekxmpp/plugins/xep_0118',
'sleekxmpp/plugins/xep_0128',
'sleekxmpp/plugins/xep_0131',
'sleekxmpp/plugins/xep_0153',
'sleekxmpp/plugins/xep_0172',
'sleekxmpp/plugins/xep_0184',
'sleekxmpp/plugins/xep_0186',
'sleekxmpp/plugins/xep_0191',
'sleekxmpp/plugins/xep_0198',
'sleekxmpp/plugins/xep_0199',
'sleekxmpp/plugins/xep_0202',
'sleekxmpp/plugins/xep_0203',
'sleekxmpp/plugins/xep_0221',
'sleekxmpp/plugins/xep_0224',
'sleekxmpp/plugins/xep_0231',
'sleekxmpp/plugins/xep_0249',
'sleekxmpp/plugins/xep_0258',
'sleekxmpp/features',
'sleekxmpp/features/feature_mechanisms',
'sleekxmpp/features/feature_mechanisms/stanza',
'sleekxmpp/features/feature_starttls',
'sleekxmpp/features/feature_bind',
'sleekxmpp/features/feature_session',
'sleekxmpp/features/feature_rosterver',
'sleekxmpp/thirdparty',
'sleekxmpp/thirdparty/suelta',
'sleekxmpp/thirdparty/suelta/mechanisms',
@@ -95,7 +120,7 @@ setup(
license = 'MIT',
platforms = [ 'any' ],
packages = packages,
requires = [ 'dnspython' ],
requires = [ 'dnspython', 'pyasn1', 'pyasn1_modules' ],
classifiers = CLASSIFIERS,
cmdclass = {'test': TestCommand}
)

View File

@@ -10,6 +10,7 @@ from sleekxmpp.basexmpp import BaseXMPP
from sleekxmpp.clientxmpp import ClientXMPP
from sleekxmpp.componentxmpp import ComponentXMPP
from sleekxmpp.stanza import Message, Presence, Iq
from sleekxmpp.jid import JID, InvalidJID
from sleekxmpp.xmlstream.handler import *
from sleekxmpp.xmlstream import XMLStream, RestartStream
from sleekxmpp.xmlstream.matcher import *

200
sleekxmpp/api.py Normal file
View File

@@ -0,0 +1,200 @@
from sleekxmpp.xmlstream import JID
class APIWrapper(object):
def __init__(self, api, name):
self.api = api
self.name = name
if name not in self.api.settings:
self.api.settings[name] = {}
def __getattr__(self, attr):
"""Curry API management commands with the API name."""
if attr == 'name':
return self.name
elif attr == 'settings':
return self.api.settings[self.name]
elif attr == 'register':
def partial(handler, op, jid=None, node=None, default=False):
register = getattr(self.api, attr)
return register(handler, self.name, op, jid, node, default)
return partial
elif attr == 'register_default':
def partial(handler, op, jid=None, node=None):
return getattr(self.api, attr)(handler, self.name, op)
return partial
elif attr in ('run', 'restore_default', 'unregister'):
def partial(*args, **kwargs):
return getattr(self.api, attr)(self.name, *args, **kwargs)
return partial
return None
def __getitem__(self, attr):
def partial(jid=None, node=None, ifrom=None, args=None):
return self.api.run(self.name, attr, jid, node, ifrom, args)
return partial
class APIRegistry(object):
def __init__(self, xmpp):
self._handlers = {}
self._handler_defaults = {}
self.xmpp = xmpp
self.settings = {}
def _setup(self, ctype, op):
"""Initialize the API callback dictionaries.
:param string ctype: The name of the API to initialize.
:param string op: The API operation to initialize.
"""
if ctype not in self.settings:
self.settings[ctype] = {}
if ctype not in self._handler_defaults:
self._handler_defaults[ctype] = {}
if ctype not in self._handlers:
self._handlers[ctype] = {}
if op not in self._handlers[ctype]:
self._handlers[ctype][op] = {'global': None,
'jid': {},
'node': {}}
def wrap(self, ctype):
"""Return a wrapper object that targets a specific API."""
return APIWrapper(self, ctype)
def purge(self, ctype):
"""Remove all information for a given API."""
del self.settings[ctype]
del self._handler_defaults[ctype]
del self._handlers[ctype]
def run(self, ctype, op, jid=None, node=None, ifrom=None, args=None):
"""Execute an API callback, based on specificity.
The API callback that is executed is chosen based on the combination
of the provided JID and node:
JID | node | Handler
==============================
Given | Given | Node handler
Given | None | JID handler
None | None | Global handler
A node handler is responsible for servicing a single node at a single
JID, while a JID handler may respond for any node at a given JID, and
the global handler will answer to any JID+node combination.
Handlers should check that the JID ``ifrom`` is authorized to perform
the desired action.
:param string ctype: The name of the API to use.
:param string op: The API operation to perform.
:param JID jid: Optionally provide specific JID.
:param string node: Optionally provide specific node.
:param JID ifrom: Optionally provide the requesting JID.
:param tuple args: Optional positional arguments to the handler.
"""
self._setup(ctype, op)
if not jid:
jid = self.xmpp.boundjid
elif jid and not isinstance(jid, JID):
jid = JID(jid)
elif jid == JID(''):
jid = self.xmpp.boundjid
if node is None:
node = ''
if self.xmpp.is_component:
if self.settings[ctype].get('component_bare', False):
jid = jid.bare
else:
jid = jid.full
else:
if self.settings[ctype].get('client_bare', False):
jid = jid.bare
else:
jid = jid.full
jid = JID(jid)
handler = self._handlers[ctype][op]['node'].get((jid, node), None)
if handler is None:
handler = self._handlers[ctype][op]['jid'].get(jid, None)
if handler is None:
handler = self._handlers[ctype][op].get('global', None)
if handler:
try:
return handler(jid, node, ifrom, args)
except TypeError:
# To preserve backward compatibility, drop the ifrom
# parameter for existing handlers that don't understand it.
return handler(jid, node, args)
def register(self, handler, ctype, op, jid=None, node=None, default=False):
"""Register an API callback, with JID+node specificity.
The API callback can later be executed based on the
specificity of the provided JID+node combination.
See :meth:`~ApiRegistry.run` for more details.
:param string ctype: The name of the API to use.
:param string op: The API operation to perform.
:param JID jid: Optionally provide specific JID.
:param string node: Optionally provide specific node.
"""
self._setup(ctype, op)
if jid is None and node is None:
if handler is None:
handler = self._handler_defaults[op]
self._handlers[ctype][op]['global'] = handler
elif jid is not None and node is None:
self._handlers[ctype][op]['jid'][jid] = handler
else:
self._handlers[ctype][op]['node'][(jid, node)] = handler
if default:
self.register_default(handler, ctype, op)
def register_default(self, handler, ctype, op):
"""Register a default, global handler for an operation.
:param func handler: The default, global handler for the operation.
:param string ctype: The name of the API to modify.
:param string op: The API operation to use.
"""
self._setup(ctype, op)
self._handler_defaults[ctype][op] = handler
def unregister(self, ctype, op, jid=None, node=None):
"""Remove an API callback.
The API callback chosen for removal is based on the
specificity of the provided JID+node combination.
See :meth:`~ApiRegistry.run` for more details.
:param string ctype: The name of the API to use.
:param string op: The API operation to perform.
:param JID jid: Optionally provide specific JID.
:param string node: Optionally provide specific node.
"""
self._setup(ctype, op)
self.register(None, ctype, op, jid, node)
def restore_default(self, ctype, op, jid=None, node=None):
"""Reset an API callback to use a default handler.
:param string ctype: The name of the API to use.
:param string op: The API operation to perform.
:param JID jid: Optionally provide specific JID.
:param string node: Optionally provide specific node.
"""
self.unregister(ctype, op, jid, node)
self.register(self._handler_defaults[ctype][op], ctype, op, jid, node)

View File

@@ -15,22 +15,27 @@
from __future__ import with_statement, unicode_literals
import sys
import copy
import logging
import threading
import sleekxmpp
from sleekxmpp import plugins, roster
from sleekxmpp import plugins, features, roster
from sleekxmpp.api import APIRegistry
from sleekxmpp.exceptions import IqError, IqTimeout
from sleekxmpp.stanza import Message, Presence, Iq, Error, StreamError
from sleekxmpp.stanza import Message, Presence, Iq, StreamError
from sleekxmpp.stanza.roster import Roster
from sleekxmpp.stanza.nick import Nick
from sleekxmpp.stanza.htmlim import HTMLIM
from sleekxmpp.xmlstream import XMLStream, JID, tostring
from sleekxmpp.xmlstream import XMLStream, JID
from sleekxmpp.xmlstream import ET, register_stanza_plugin
from sleekxmpp.xmlstream.matcher import *
from sleekxmpp.xmlstream.handler import *
from sleekxmpp.xmlstream.matcher import MatchXPath
from sleekxmpp.xmlstream.handler import Callback
from sleekxmpp.xmlstream.stanzabase import XML_NS
from sleekxmpp.features import *
from sleekxmpp.plugins import PluginManager, register_plugin, load_plugin
log = logging.getLogger(__name__)
@@ -63,11 +68,20 @@ class BaseXMPP(XMLStream):
#: An identifier for the stream as given by the server.
self.stream_id = None
#: The JabberID (JID) used by this connection.
#: The JabberID (JID) used by this connection.
self.boundjid = JID(jid)
self._expected_server_name = self.boundjid.host
self._redirect_attempts = 0
#: The maximum number of consecutive see-other-host
#: redirections that will be followed before quitting.
self.max_redirects = 5
self.session_bind_event = threading.Event()
#: A dictionary mapping plugin names to plugins.
self.plugin = {}
self.plugin = PluginManager(self)
#: Configuration options for whitelisted plugins.
#: If a plugin is registered without any configuration,
@@ -82,19 +96,35 @@ class BaseXMPP(XMLStream):
#: owner JIDs, as in the case for components. For clients
#: which only have a single JID, see :attr:`client_roster`.
self.roster = roster.Roster(self)
self.roster.add(self.boundjid.bare)
self.roster.add(self.boundjid)
#: The single roster for the bound JID. This is the
#: equivalent of::
#:
#: self.roster[self.boundjid.bare]
self.client_roster = self.roster[self.boundjid.bare]
self.client_roster = self.roster[self.boundjid]
#: The distinction between clients and components can be
#: important, primarily for choosing how to handle the
#: ``'to'`` and ``'from'`` JIDs of stanzas.
self.is_component = False
#: The API registry is a way to process callbacks based on
#: JID+node combinations. Each callback in the registry is
#: marked with:
#:
#: - An API name, e.g. xep_0030
#: - The name of an action, e.g. get_info
#: - The JID that will be affected
#: - The node that will be affected
#:
#: API handlers with no JID or node will act as global handlers,
#: while those with a JID and no node will service all nodes
#: for a JID, and handlers with both a JID and node will be
#: used only for that specific combination. The handler that
#: provides the most specificity will be used.
self.api = APIRegistry(self)
#: Flag indicating that the initial presence broadcast has
#: been sent. Until this happens, some servers may not
#: behave as expected when sending stanzas.
@@ -113,11 +143,14 @@ class BaseXMPP(XMLStream):
Callback('Presence',
MatchXPath("{%s}presence" % self.default_ns),
self._handle_presence))
self.register_handler(
Callback('Stream Error',
MatchXPath("{%s}error" % self.stream_ns),
self._handle_stream_error))
self.add_event_handler('session_start',
self._handle_session_start)
self.add_event_handler('disconnected',
self._handle_disconnected)
self.add_event_handler('presence_available',
@@ -160,6 +193,8 @@ class BaseXMPP(XMLStream):
:param xml: The incoming stream's root element.
"""
self.stream_id = xml.get('id', '')
self.stream_version = xml.get('version', '')
self.peer_default_lang = xml.get('{%s}lang' % XML_NS, None)
def process(self, *args, **kwargs):
"""Initialize plugins and begin processing the XML stream.
@@ -179,16 +214,25 @@ class BaseXMPP(XMLStream):
Defaults to ``True``. This does **not** mean that no
threads are used at all if ``threaded=False``.
Regardless of these threading options, these threads will
Regardless of these threading options, these threads will
always exist:
- The event queue processor
- The send queue processor
- The scheduler
"""
if 'xep_0115' in self.plugin:
name = 'xep_0115'
if not hasattr(self.plugin[name], 'post_inited'):
if hasattr(self.plugin[name], 'post_init'):
self.plugin[name].post_init()
self.plugin[name].post_inited = True
for name in self.plugin:
if not self.plugin[name].post_inited:
self.plugin[name].post_init()
if not hasattr(self.plugin[name], 'post_inited'):
if hasattr(self.plugin[name], 'post_init'):
self.plugin[name].post_init()
self.plugin[name].post_inited = True
return XMLStream.process(self, *args, **kwargs)
def register_plugin(self, plugin, pconfig={}, module=None):
@@ -201,42 +245,14 @@ class BaseXMPP(XMLStream):
:param module: Optional refence to the module containing the plugin
class if using custom plugins.
"""
try:
# Import the given module that contains the plugin.
if not module:
try:
module = sleekxmpp.plugins
module = __import__(
str("%s.%s" % (module.__name__, plugin)),
globals(), locals(), [str(plugin)])
except ImportError:
module = sleekxmpp.features
module = __import__(
str("%s.%s" % (module.__name__, plugin)),
globals(), locals(), [str(plugin)])
if isinstance(module, str):
# We probably want to load a module from outside
# the sleekxmpp package, so leave out the globals().
module = __import__(module, fromlist=[plugin])
# Use the global plugin config cache, if applicable
if not pconfig:
pconfig = self.plugin_config.get(plugin, {})
# Use the global plugin config cache, if applicable
if not pconfig:
pconfig = self.plugin_config.get(plugin, {})
# Load the plugin class from the module.
self.plugin[plugin] = getattr(module, plugin)(self, pconfig)
# Let XEP/RFC implementing plugins have some extra logging info.
spec = '(CUSTOM) %s'
if self.plugin[plugin].xep:
spec = "(XEP-%s) " % self.plugin[plugin].xep
elif self.plugin[plugin].rfc:
spec = "(RFC-%s) " % self.plugin[plugin].rfc
desc = (spec, self.plugin[plugin].description)
log.debug("Loaded Plugin %s %s" % desc)
except:
log.exception("Unable to load plugin: %s", plugin)
if not self.plugin.registered(plugin):
load_plugin(plugin, module)
self.plugin.enable(plugin, pconfig)
def register_plugins(self):
"""Register and initialize all built-in plugins.
@@ -253,15 +269,10 @@ class BaseXMPP(XMLStream):
for plugin in plugin_list:
if plugin in plugins.__all__:
self.register_plugin(plugin,
self.plugin_config.get(plugin, {}))
self.register_plugin(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:
@@ -276,7 +287,9 @@ class BaseXMPP(XMLStream):
def Message(self, *args, **kwargs):
"""Create a Message stanza associated with this stream."""
return Message(self, *args, **kwargs)
msg = Message(self, *args, **kwargs)
msg['lang'] = self.default_lang
return msg
def Iq(self, *args, **kwargs):
"""Create an Iq stanza associated with this stream."""
@@ -284,18 +297,20 @@ class BaseXMPP(XMLStream):
def Presence(self, *args, **kwargs):
"""Create a Presence stanza associated with this stream."""
return Presence(self, *args, **kwargs)
pres = Presence(self, *args, **kwargs)
pres['lang'] = self.default_lang
return pres
def make_iq(self, id=0, ifrom=None, ito=None, itype=None, iquery=None):
"""Create a new Iq stanza with a given Id and from JID.
:param id: An ideally unique ID value for this stanza thread.
Defaults to 0.
:param ifrom: The from :class:`~sleekxmpp.xmlstream.jid.JID`
:param ifrom: The from :class:`~sleekxmpp.xmlstream.jid.JID`
to use for this stanza.
:param ito: The destination :class:`~sleekxmpp.xmlstream.jid.JID`
for this stanza.
:param itype: The :class:`~sleekxmpp.stanza.iq.Iq`'s type,
:param itype: The :class:`~sleekxmpp.stanza.iq.Iq`'s type,
one of: ``'get'``, ``'set'``, ``'result'``,
or ``'error'``.
:param iquery: Optional namespace for adding a query element.
@@ -333,7 +348,7 @@ class BaseXMPP(XMLStream):
def make_iq_result(self, id=None, ito=None, ifrom=None, iq=None):
"""
Create an :class:`~sleekxmpp.stanza.iq.Iq` stanza of type
Create an :class:`~sleekxmpp.stanza.iq.Iq` stanza of type
``'result'`` with the given ID value.
:param id: An ideally unique ID value. May use :meth:`new_id()`.
@@ -363,10 +378,10 @@ class BaseXMPP(XMLStream):
Optionally, a substanza may be given to use as the
stanza's payload.
:param sub: Either an
:param sub: Either an
:class:`~sleekxmpp.xmlstream.stanzabase.ElementBase`
stanza object or an
:class:`~xml.etree.ElementTree.Element` XML object
:class:`~xml.etree.ElementTree.Element` XML object
to use as the :class:`~sleekxmpp.stanza.iq.Iq`'s payload.
:param ito: The destination :class:`~sleekxmpp.xmlstream.jid.JID`
for this stanza.
@@ -393,9 +408,9 @@ class BaseXMPP(XMLStream):
Create an :class:`~sleekxmpp.stanza.iq.Iq` stanza of type ``'error'``.
:param id: An ideally unique ID value. May use :meth:`new_id()`.
:param type: The type of the error, such as ``'cancel'`` or
:param type: The type of the error, such as ``'cancel'`` or
``'modify'``. Defaults to ``'cancel'``.
:param condition: The error condition. Defaults to
:param condition: The error condition. Defaults to
``'feature-not-implemented'``.
:param text: A message describing the cause of the error.
:param ito: The destination :class:`~sleekxmpp.xmlstream.jid.JID`
@@ -419,7 +434,7 @@ class BaseXMPP(XMLStream):
def make_iq_query(self, iq=None, xmlns='', ito=None, ifrom=None):
"""
Create or modify an :class:`~sleekxmpp.stanza.iq.Iq` stanza
Create or modify an :class:`~sleekxmpp.stanza.iq.Iq` stanza
to use the given query namespace.
:param iq: Optionally use an existing stanza instead
@@ -452,7 +467,7 @@ class BaseXMPP(XMLStream):
def make_message(self, mto, mbody=None, msubject=None, mtype=None,
mhtml=None, mfrom=None, mnick=None):
"""
Create and initialize a new
Create and initialize a new
:class:`~sleekxmpp.stanza.message.Message` stanza.
:param mto: The recipient of the message.
@@ -478,7 +493,7 @@ class BaseXMPP(XMLStream):
def make_presence(self, pshow=None, pstatus=None, ppriority=None,
pto=None, ptype=None, pfrom=None, pnick=None):
"""
Create and initialize a new
Create and initialize a new
:class:`~sleekxmpp.stanza.presence.Presence` stanza.
:param pshow: The presence's show value.
@@ -502,7 +517,7 @@ class BaseXMPP(XMLStream):
def send_message(self, mto, mbody, msubject=None, mtype=None,
mhtml=None, mfrom=None, mnick=None):
"""
Create, initialize, and send a new
Create, initialize, and send a new
:class:`~sleekxmpp.stanza.message.Message` stanza.
:param mto: The recipient of the message.
@@ -522,7 +537,7 @@ class BaseXMPP(XMLStream):
def send_presence(self, pshow=None, pstatus=None, ppriority=None,
pto=None, pfrom=None, ptype=None, pnick=None):
"""
Create, initialize, and send a new
Create, initialize, and send a new
:class:`~sleekxmpp.stanza.presence.Presence` stanza.
:param pshow: The presence's show value.
@@ -533,23 +548,13 @@ class BaseXMPP(XMLStream):
:param pfrom: The sender of the presence.
:param pnick: Optional nickname of the presence's sender.
"""
# Python2.6 chokes on Unicode strings for dict keys.
args = {str('pto'): pto,
str('ptype'): ptype,
str('pshow'): pshow,
str('pstatus'): pstatus,
str('ppriority'): ppriority,
str('pnick'): pnick}
if self.is_component:
self.roster[pfrom].send_presence(**args)
else:
self.client_roster.send_presence(**args)
self.make_presence(pshow, pstatus, ppriority, pto,
ptype, pfrom, pnick).send()
def send_presence_subscription(self, pto, pfrom=None,
ptype='subscribe', pnick=None):
"""
Create, initialize, and send a new
Create, initialize, and send a new
:class:`~sleekxmpp.stanza.presence.Presence` stanza of
type ``'subscribe'``.
@@ -558,14 +563,10 @@ class BaseXMPP(XMLStream):
:param ptype: The type of presence, such as ``'subscribe'``.
:param pnick: Optional nickname of the presence's sender.
"""
presence = self.makePresence(ptype=ptype,
pfrom=pfrom,
pto=self.getjidbare(pto))
if pnick:
nick = ET.Element('{http://jabber.org/protocol/nick}nick')
nick.text = pnick
presence.append(nick)
presence.send()
self.make_presence(ptype=ptype,
pfrom=pfrom,
pto=JID(pto).bare,
pnick=pnick).send()
@property
def jid(self):
@@ -597,7 +598,7 @@ class BaseXMPP(XMLStream):
@resource.setter
def resource(self, value):
log.warning("fulljid property deprecated. Use boundjid.full")
log.warning("fulljid property deprecated. Use boundjid.resource")
self.boundjid.resource = value
@property
@@ -662,28 +663,61 @@ class BaseXMPP(XMLStream):
def getjidbare(self, fulljid):
return fulljid.split('/', 1)[0]
def _handle_session_start(self, event):
"""Reset redirection attempt count."""
self._redirect_attempts = 0
def _handle_disconnected(self, event):
"""When disconnected, reset the roster"""
self.roster.reset()
self.session_bind_event.clear()
def _handle_stream_error(self, error):
self.event('stream_error', error)
if error['condition'] == 'see-other-host':
other_host = error['see_other_host']
if not other_host:
log.warning("No other host specified.")
return
if self._redirect_attempts > self.max_redirects:
log.error("Exceeded maximum number of redirection attempts.")
return
self._redirect_attempts += 1
host = other_host
port = 5222
if '[' in other_host and ']' in other_host:
host = other_host.split(']')[0][1:]
elif ':' in other_host:
host = other_host.split(':')[0]
port_sec = other_host.split(']')[-1]
if ':' in port_sec:
port = int(port_sec.split(':')[1])
self.address = (host, port)
self.default_domain = host
self.dns_records = None
self.reconnect_delay = None
self.reconnect()
def _handle_message(self, msg):
"""Process incoming message stanzas."""
if not self.is_component and not msg['to'].bare:
msg['to'] = self.boundjid
self.event('message', msg)
def _handle_available(self, presence):
pto = presence['to'].bare
pfrom = presence['from'].bare
self.roster[pto][pfrom].handle_available(presence)
def _handle_available(self, pres):
self.roster[pres['to']][pres['from']].handle_available(pres)
def _handle_unavailable(self, presence):
pto = presence['to'].bare
pfrom = presence['from'].bare
self.roster[pto][pfrom].handle_unavailable(presence)
def _handle_unavailable(self, pres):
self.roster[pres['to']][pres['from']].handle_unavailable(pres)
def _handle_new_subscription(self, stanza):
def _handle_new_subscription(self, pres):
"""Attempt to automatically handle subscription requests.
Subscriptions will be approved if the request is from
@@ -695,8 +729,8 @@ class BaseXMPP(XMLStream):
If a subscription is accepted, a request for a mutual
subscription will be sent if :attr:`auto_subscribe` is ``True``.
"""
roster = self.roster[stanza['to'].bare]
item = self.roster[stanza['to'].bare][stanza['from'].bare]
roster = self.roster[pres['to']]
item = self.roster[pres['to']][pres['from']]
if item['whitelisted']:
item.authorize()
elif roster.auto_authorize:
@@ -706,37 +740,31 @@ class BaseXMPP(XMLStream):
elif roster.auto_authorize == False:
item.unauthorize()
def _handle_removed_subscription(self, presence):
pto = presence['to'].bare
pfrom = presence['from'].bare
self.roster[pto][pfrom].unauthorize()
def _handle_removed_subscription(self, pres):
self.roster[pres['to']][pres['from']].handle_unauthorize(pres)
def _handle_subscribe(self, presence):
pto = presence['to'].bare
pfrom = presence['from'].bare
self.roster[pto][pfrom].handle_subscribe(presence)
def _handle_subscribe(self, pres):
self.roster[pres['to']][pres['from']].handle_subscribe(pres)
def _handle_subscribed(self, presence):
pto = presence['to'].bare
pfrom = presence['from'].bare
self.roster[pto][pfrom].handle_subscribed(presence)
def _handle_subscribed(self, pres):
self.roster[pres['to']][pres['from']].handle_subscribed(pres)
def _handle_unsubscribe(self, presence):
pto = presence['to'].bare
pfrom = presence['from'].bare
self.roster[pto][pfrom].handle_unsubscribe(presence)
def _handle_unsubscribe(self, pres):
self.roster[pres['to']][pres['from']].handle_unsubscribe(pres)
def _handle_unsubscribed(self, presence):
pto = presence['to'].bare
pfrom = presence['from'].bare
self.roster[pto][pfrom].handle_unsubscribed(presence)
def _handle_unsubscribed(self, pres):
self.roster[pres['to']][pres['from']].handle_unsubscribed(pres)
def _handle_presence(self, presence):
"""Process incoming presence stanzas.
Update the roster with presence information.
"""
self.event("presence_%s" % presence['type'], presence)
if not self.is_component and not presence['to'].bare:
presence['to'] = self.boundjid
self.event('presence', presence)
self.event('presence_%s' % presence['type'], presence)
# Check for changes in subscription state.
if presence['type'] in ('subscribe', 'subscribed',
@@ -748,7 +776,7 @@ class BaseXMPP(XMLStream):
return
def exception(self, exception):
"""Process any uncaught exceptions, notably
"""Process any uncaught exceptions, notably
:class:`~sleekxmpp.exceptions.IqError` and
:class:`~sleekxmpp.exceptions.IqTimeout` exceptions.
@@ -763,6 +791,11 @@ class BaseXMPP(XMLStream):
iq = exception.iq
log.error('Request timed out: %s', iq)
log.warning('You should catch IqTimeout exceptions')
elif isinstance(exception, SyntaxError):
# Hide stream parsing errors that occur when the
# stream is disconnected (they've been handled, we
# don't need to make a mess in the logs).
pass
else:
log.exception(exception)

View File

@@ -15,22 +15,13 @@
from __future__ import absolute_import, unicode_literals
import logging
import base64
import sys
import hashlib
import random
import threading
import sleekxmpp
from sleekxmpp import plugins
from sleekxmpp import stanza
from sleekxmpp import features
from sleekxmpp.stanza import StreamFeatures
from sleekxmpp.basexmpp import BaseXMPP
from sleekxmpp.stanza import *
from sleekxmpp.xmlstream import XMLStream, RestartStream
from sleekxmpp.xmlstream import StanzaBase, ET, register_stanza_plugin
from sleekxmpp.xmlstream.matcher import *
from sleekxmpp.xmlstream.handler import *
from sleekxmpp.exceptions import XMPPError
from sleekxmpp.xmlstream import XMLStream
from sleekxmpp.xmlstream.matcher import StanzaPath, MatchXPath
from sleekxmpp.xmlstream.handler import Callback
# Flag indicating if DNS SRV records are available for use.
try:
@@ -63,33 +54,41 @@ class ClientXMPP(BaseXMPP):
:param password: The password for the XMPP user account.
:param ssl: **Deprecated.**
:param plugin_config: A dictionary of plugin configurations.
:param plugin_whitelist: A list of approved plugins that
will be loaded when calling
:param plugin_whitelist: A list of approved plugins that
will be loaded when calling
:meth:`~sleekxmpp.basexmpp.BaseXMPP.register_plugins()`.
:param escape_quotes: **Deprecated.**
"""
def __init__(self, jid, password, ssl=False, plugin_config={},
plugin_whitelist=[], escape_quotes=True, sasl_mech=None):
def __init__(self, jid, password, plugin_config={}, plugin_whitelist=[],
escape_quotes=True, sasl_mech=None, lang='en'):
BaseXMPP.__init__(self, jid, 'jabber:client')
self.set_jid(jid)
self.password = password
self.escape_quotes = escape_quotes
self.plugin_config = plugin_config
self.plugin_whitelist = plugin_whitelist
self.default_port = 5222
self.default_lang = lang
self.stream_header = "<stream:stream to='%s' %s %s version='1.0'>" % (
self.credentials = {}
self.password = password
self.stream_header = "<stream:stream to='%s' %s %s %s %s>" % (
self.boundjid.host,
"xmlns:stream='%s'" % self.stream_ns,
"xmlns='%s'" % self.default_ns)
"xmlns='%s'" % self.default_ns,
"xml:lang='%s'" % self.default_lang,
"version='1.0'")
self.stream_footer = "</stream:stream>"
self.features = set()
self._stream_feature_handlers = {}
self._stream_feature_order = []
self.dns_service = 'xmpp-client'
#TODO: Use stream state here
self.authenticated = False
self.sessionstarted = False
@@ -97,6 +96,7 @@ class ClientXMPP(BaseXMPP):
self.bindfail = False
self.add_event_handler('connected', self._handle_connected)
self.add_event_handler('session_bind', self._handle_session_bind)
self.register_stanza(StreamFeatures)
@@ -106,9 +106,7 @@ class ClientXMPP(BaseXMPP):
self._handle_stream_features))
self.register_handler(
Callback('Roster Update',
MatchXPath('{%s}iq/{%s}query' % (
self.default_ns,
'jabber:iq:roster')),
StanzaPath('iq@type=set/roster'),
self._handle_roster))
# Setup default stream features
@@ -117,6 +115,15 @@ class ClientXMPP(BaseXMPP):
self.register_plugin('feature_session')
self.register_plugin('feature_mechanisms',
pconfig={'use_mech': sasl_mech} if sasl_mech else None)
self.register_plugin('feature_rosterver')
@property
def password(self):
return self.credentials.get('password', '')
@password.setter
def password(self, value):
self.credentials['password'] = value
def connect(self, address=tuple(), reattempt=True,
use_tls=True, use_ssl=False):
@@ -135,41 +142,22 @@ class ClientXMPP(BaseXMPP):
should be used. Defaults to ``False``.
"""
self.session_started_event.clear()
if not address:
# If an address was provided, disable using DNS SRV lookup;
# otherwise, use the domain from the client JID with the standard
# XMPP client port and allow SRV lookup.
if address:
self.dns_service = None
else:
address = (self.boundjid.host, 5222)
self.dns_service = 'xmpp-client'
self._expected_server_name = self.boundjid.host
return XMLStream.connect(self, address[0], address[1],
use_tls=use_tls, use_ssl=use_ssl,
reattempt=reattempt)
def get_dns_records(self, domain, port=None):
"""Get the DNS records for a domain, including SRV records.
:param domain: The domain in question.
:param port: If the results don't include a port, use this one.
"""
if port is None:
port = self.default_port
if DNSPYTHON:
try:
record = "_xmpp-client._tcp.%s" % domain
answers = []
for answer in dns.resolver.query(record, dns.rdatatype.SRV):
address = (answer.target.to_text()[:-1], answer.port)
answers.append((address, answer.priority, answer.weight))
except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer):
log.warning("No SRV records for %s", domain)
answers = super(ClientXMPP, self).get_dns_records(domain, port)
except dns.exception.Timeout:
log.warning("DNS resolution timed out " + \
"for SRV record of %s", domain)
answers = super(ClientXMPP, self).get_dns_records(domain, port)
return answers
else:
log.warning("dnspython is not installed -- " + \
"relying on OS A record resolution")
return [((domain, port), 0, 0)]
def register_feature(self, name, handler, restart=False, order=5000):
"""Register a stream feature handler.
@@ -185,8 +173,13 @@ class ClientXMPP(BaseXMPP):
self._stream_feature_order.append((order, name))
self._stream_feature_order.sort()
def update_roster(self, jid, name=None, subscription=None, groups=[],
block=True, timeout=None, callback=None):
def unregister_feature(self, name, order):
if name in self._stream_feature_handlers:
del self._stream_feature_handlers[name]
self._stream_feature_order.remove((order, name))
self._stream_feature_order.sort()
def update_roster(self, jid, **kwargs):
"""Add or change a roster item.
:param jid: The JID of the entry to modify.
@@ -201,18 +194,28 @@ class ClientXMPP(BaseXMPP):
occurs. Defaults to ``True``.
:param timeout: The length of time (in seconds) to wait
for a response before continuing if blocking
is used. Defaults to
is used. Defaults to
:attr:`~sleekxmpp.xmlstream.xmlstream.XMLStream.response_timeout`.
:param callback: Optional reference to a stream handler function.
Will be executed when the roster is received.
Implies ``block=False``.
"""
current = self.client_roster[jid]
name = kwargs.get('name', current['name'])
subscription = kwargs.get('subscription', current['subscription'])
groups = kwargs.get('groups', current['groups'])
block = kwargs.get('block', True)
timeout = kwargs.get('timeout', None)
callback = kwargs.get('callback', None)
return self.client_roster.update(jid, name, subscription, groups,
block, timeout, callback)
def del_roster_item(self, jid):
"""Remove an item from the roster.
This is done by setting its subscription status to ``'remove'``.
:param jid: The JID of the item to remove.
@@ -227,7 +230,7 @@ class ClientXMPP(BaseXMPP):
Defaults to ``True``.
:param timeout: The length of time (in seconds) to wait for a response
before continuing if blocking is used.
Defaults to
Defaults to
:attr:`~sleekxmpp.xmlstream.xmlstream.XMLStream.response_timeout`.
:param callback: Optional reference to a stream handler function. Will
be executed when the roster is received.
@@ -236,10 +239,18 @@ class ClientXMPP(BaseXMPP):
iq = self.Iq()
iq['type'] = 'get'
iq.enable('roster')
response = iq.send(block, timeout, callback)
if 'rosterver' in self.features:
iq['roster']['ver'] = self.client_roster.version
if callback is None:
return self._handle_roster(response, request=True)
if not block and callback is None:
callback = lambda resp: self._handle_roster(resp)
response = iq.send(block, timeout, callback)
self.event('roster_received', response)
if block:
self._handle_roster(response)
return response
def _handle_connected(self, event=None):
#TODO: Use stream state here
@@ -262,31 +273,44 @@ class ClientXMPP(BaseXMPP):
# restarting the XML stream.
return True
def _handle_roster(self, iq, request=False):
def _handle_roster(self, iq):
"""Update the roster after receiving a roster stanza.
:param iq: The roster stanza.
:param request: Indicates if this stanza is a response
to a request for the roster, and not an
empty acknowledgement from the server.
"""
if iq['type'] == 'set' or (iq['type'] == 'result' and request):
for jid in iq['roster']['items']:
item = iq['roster']['items'][jid]
roster = self.roster[iq['to'].bare]
roster[jid]['name'] = item['name']
roster[jid]['groups'] = item['groups']
roster[jid]['from'] = item['subscription'] in ['from', 'both']
roster[jid]['to'] = item['subscription'] in ['to', 'both']
roster[jid]['pending_out'] = (item['ask'] == 'subscribe')
self.event('roster_received', iq)
if iq['type'] == 'set':
if iq['from'].bare and iq['from'].bare != self.boundjid.bare:
raise XMPPError(condition='service-unavailable')
roster = self.client_roster
if iq['roster']['ver']:
roster.version = iq['roster']['ver']
items = iq['roster']['items']
for jid in items:
item = items[jid]
roster[jid]['name'] = item['name']
roster[jid]['groups'] = item['groups']
roster[jid]['from'] = item['subscription'] in ['from', 'both']
roster[jid]['to'] = item['subscription'] in ['to', 'both']
roster[jid]['pending_out'] = (item['ask'] == 'subscribe')
roster[jid].save(remove=(item['subscription'] == 'remove'))
self.event("roster_update", iq)
if iq['type'] == 'set':
iq.reply()
iq.enable('roster')
iq.send()
return True
resp = self.Iq(stype='result',
sto=iq['from'],
sid=iq['id'])
resp.enable('roster')
resp.send()
def _handle_session_bind(self, jid):
"""Set the client roster to the JID set by the server.
:param :class:`sleekxmpp.xmlstream.jid.JID` jid: The bound JID as
dictated by the server. The same as :attr:`boundjid`.
"""
self.client_roster = self.roster[jid]
# To comply with PEP8, method names now use underscores.

View File

@@ -15,17 +15,14 @@
from __future__ import absolute_import
import logging
import base64
import sys
import hashlib
from sleekxmpp import plugins
from sleekxmpp import stanza
from sleekxmpp.basexmpp import BaseXMPP
from sleekxmpp.xmlstream import XMLStream, RestartStream
from sleekxmpp.xmlstream import StanzaBase, ET
from sleekxmpp.xmlstream.matcher import *
from sleekxmpp.xmlstream.handler import *
from sleekxmpp.xmlstream import XMLStream
from sleekxmpp.xmlstream import ET
from sleekxmpp.xmlstream.matcher import MatchXPath
from sleekxmpp.xmlstream.handler import Callback
log = logging.getLogger(__name__)
@@ -43,8 +40,8 @@ class ComponentXMPP(BaseXMPP):
:param host: The server accepting the component.
:param port: The port used to connect to the server.
:param plugin_config: A dictionary of plugin configurations.
:param plugin_whitelist: A list of approved plugins that
will be loaded when calling
:param plugin_whitelist: A list of approved plugins that
will be loaded when calling
:meth:`~sleekxmpp.basexmpp.BaseXMPP.register_plugins()`.
:param use_jc_ns: Indicates if the ``'jabber:client'`` namespace
should be used instead of the standard
@@ -81,8 +78,8 @@ class ComponentXMPP(BaseXMPP):
self.add_event_handler('presence_probe',
self._handle_probe)
def connect(self, host=None, port=None, use_ssl=False,
use_tls=True, reattempt=True):
def connect(self, host=None, port=None, use_ssl=False,
use_tls=False, reattempt=True):
"""Connect to the server.
Setting ``reattempt`` to ``True`` will cause connection attempts to
@@ -104,10 +101,16 @@ class ComponentXMPP(BaseXMPP):
host = self.server_host
if port is None:
port = self.server_port
self.server_name = self.boundjid.host
if use_tls:
log.info("XEP-0114 components can not use TLS")
log.debug("Connecting to %s:%s", host, port)
return XMLStream.connect(self, host=host, port=port,
use_ssl=use_ssl,
use_tls=use_tls,
use_tls=False,
reattempt=reattempt)
def incoming_filter(self, xml):
@@ -153,10 +156,10 @@ class ComponentXMPP(BaseXMPP):
:param xml: The reply handshake stanza.
"""
self.session_bind_event.set()
self.session_started_event.set()
self.event("session_bind", self.boundjid, direct=True)
self.event("session_start")
def _handle_probe(self, presence):
pto = presence['to'].bare
pfrom = presence['from'].bare
self.roster[pto][pfrom].handle_probe(presence)
def _handle_probe(self, pres):
self.roster[pres['to']][pres['from']].handle_probe(pres)

View File

@@ -69,10 +69,11 @@ class IqTimeout(XMPPError):
condition='remote-server-timeout',
etype='cancel')
#: The :class:`~sleekxmpp.stanza.iq.Iq` stanza whose response
#: The :class:`~sleekxmpp.stanza.iq.Iq` stanza whose response
#: did not arrive before the timeout expired.
self.iq = iq
class IqError(XMPPError):
"""

View File

@@ -6,4 +6,10 @@
See the file LICENSE for copying permission.
"""
__all__ = ['feature_starttls', 'feature_mechanisms', 'feature_bind']
__all__ = [
'feature_starttls',
'feature_mechanisms',
'feature_bind',
'feature_session',
'feature_rosterver'
]

View File

@@ -6,5 +6,14 @@
See the file LICENSE for copying permission.
"""
from sleekxmpp.features.feature_bind.bind import feature_bind
from sleekxmpp.plugins.base import register_plugin
from sleekxmpp.features.feature_bind.bind import FeatureBind
from sleekxmpp.features.feature_bind.stanza import Bind
register_plugin(FeatureBind)
# Retain some backwards compatibility
feature_bind = FeatureBind

View File

@@ -11,22 +11,20 @@ import logging
from sleekxmpp.stanza import Iq, StreamFeatures
from sleekxmpp.features.feature_bind import stanza
from sleekxmpp.xmlstream import register_stanza_plugin
from sleekxmpp.xmlstream.matcher import *
from sleekxmpp.xmlstream.handler import *
from sleekxmpp.plugins.base import base_plugin
from sleekxmpp.plugins import BasePlugin, register_plugin
log = logging.getLogger(__name__)
class feature_bind(base_plugin):
class FeatureBind(BasePlugin):
name = 'feature_bind'
description = 'RFC 6120: Stream Feature: Resource Binding'
dependencies = set()
stanza = stanza
def plugin_init(self):
self.name = 'Bind Resource'
self.rfc = '6120'
self.description = 'Resource Binding Stream Feature'
self.stanza = stanza
self.xmpp.register_feature('bind',
self._handle_bind_resource,
restart=False,
@@ -52,6 +50,8 @@ class feature_bind(base_plugin):
self.xmpp.set_jid(response['bind']['jid'])
self.xmpp.bound = True
self.xmpp.event('session_bind', self.xmpp.boundjid, direct=True)
self.xmpp.session_bind_event.set()
self.xmpp.features.add('bind')

View File

@@ -6,8 +6,7 @@
See the file LICENSE for copying permission.
"""
from sleekxmpp.stanza import Iq, StreamFeatures
from sleekxmpp.xmlstream import ElementBase, ET, register_stanza_plugin
from sleekxmpp.xmlstream import ElementBase
class Bind(ElementBase):

View File

@@ -6,8 +6,17 @@
See the file LICENSE for copying permission.
"""
from sleekxmpp.features.feature_mechanisms.mechanisms import feature_mechanisms
from sleekxmpp.plugins.base import register_plugin
from sleekxmpp.features.feature_mechanisms.mechanisms import FeatureMechanisms
from sleekxmpp.features.feature_mechanisms.stanza import Mechanisms
from sleekxmpp.features.feature_mechanisms.stanza import Auth
from sleekxmpp.features.feature_mechanisms.stanza import Success
from sleekxmpp.features.feature_mechanisms.stanza import Failure
register_plugin(FeatureMechanisms)
# Retain some backwards compatibility
feature_mechanisms = FeatureMechanisms

View File

@@ -9,50 +9,67 @@
import logging
from sleekxmpp.thirdparty import suelta
from sleekxmpp.thirdparty.suelta.exceptions import SASLCancelled, SASLError
from sleekxmpp.thirdparty.suelta.exceptions import SASLPrepFailure
from sleekxmpp.stanza import StreamFeatures
from sleekxmpp.xmlstream import RestartStream, register_stanza_plugin
from sleekxmpp.xmlstream.matcher import *
from sleekxmpp.xmlstream.handler import *
from sleekxmpp.plugins.base import base_plugin
from sleekxmpp.plugins import BasePlugin
from sleekxmpp.xmlstream.matcher import MatchXPath
from sleekxmpp.xmlstream.handler import Callback
from sleekxmpp.features.feature_mechanisms import stanza
log = logging.getLogger(__name__)
class feature_mechanisms(base_plugin):
class FeatureMechanisms(BasePlugin):
name = 'feature_mechanisms'
description = 'RFC 6120: Stream Feature: SASL'
dependencies = set()
stanza = stanza
default_config = {
'use_mech': None,
'sasl_callback': None,
'order': 100
}
def plugin_init(self):
self.name = 'SASL Mechanisms'
self.rfc = '6120'
self.description = "SASL Stream Feature"
self.stanza = stanza
self.use_mech = self.config.get('use_mech', None)
if not self.use_mech and not self.xmpp.boundjid.user:
self.use_mech = 'ANONYMOUS'
def tls_active():
return 'starttls' in self.xmpp.features
def basic_callback(mech, values):
if 'username' in values:
values['username'] = self.xmpp.boundjid.user
if 'password' in values:
values['password'] = self.xmpp.password
creds = self.xmpp.credentials
for value in values:
if value == 'username':
values['username'] = self.xmpp.boundjid.user
elif value == 'password':
values['password'] = creds['password']
elif value == 'email':
jid = self.xmpp.boundjid.bare
values['email'] = creds.get('email', jid)
elif value in creds:
values[value] = creds[value]
mech.fulfill(values)
sasl_callback = self.config.get('sasl_callback', None)
if sasl_callback is None:
sasl_callback = basic_callback
if self.sasl_callback is None:
self.sasl_callback = basic_callback
self.mech = None
self.sasl = suelta.SASL(self.xmpp.boundjid.domain, 'xmpp',
username=self.xmpp.boundjid.user,
sec_query=suelta.sec_query_allow,
request_values=sasl_callback,
request_values=self.sasl_callback,
tls_active=tls_active,
mech=self.use_mech)
self.mech_list = set()
self.attempted_mechs = set()
register_stanza_plugin(StreamFeatures, stanza.Mechanisms)
self.xmpp.register_stanza(stanza.Success)
@@ -60,19 +77,18 @@ class feature_mechanisms(base_plugin):
self.xmpp.register_stanza(stanza.Auth)
self.xmpp.register_stanza(stanza.Challenge)
self.xmpp.register_stanza(stanza.Response)
self.xmpp.register_stanza(stanza.Abort)
self.xmpp.register_handler(
Callback('SASL Success',
MatchXPath(stanza.Success.tag_name()),
self._handle_success,
instream=True,
once=True))
instream=True))
self.xmpp.register_handler(
Callback('SASL Failure',
MatchXPath(stanza.Failure.tag_name()),
self._handle_fail,
instream=True,
once=True))
instream=True))
self.xmpp.register_handler(
Callback('SASL Challenge',
MatchXPath(stanza.Challenge.tag_name()),
@@ -81,7 +97,7 @@ class feature_mechanisms(base_plugin):
self.xmpp.register_feature('mechanisms',
self._handle_sasl_auth,
restart=True,
order=self.config.get('order', 100))
order=self.order)
def _handle_sasl_auth(self, features):
"""
@@ -95,35 +111,63 @@ class feature_mechanisms(base_plugin):
# server has incorrectly offered it again.
return False
mech_list = features['mechanisms']
if not self.use_mech:
self.mech_list = set(features['mechanisms'])
else:
self.mech_list = set([self.use_mech])
return self._send_auth()
def _send_auth(self):
mech_list = self.mech_list - self.attempted_mechs
self.mech = self.sasl.choose_mechanism(mech_list)
if self.mech is not None:
if mech_list and self.mech is not None:
resp = stanza.Auth(self.xmpp)
resp['mechanism'] = self.mech.name
resp['value'] = self.mech.process()
resp.send(now=True)
try:
resp['value'] = self.mech.process()
except SASLCancelled:
self.attempted_mechs.add(self.mech.name)
self._send_auth()
except SASLError:
self.attempted_mechs.add(self.mech.name)
self._send_auth()
except SASLPrepFailure:
log.exception("A credential value did not pass SASLprep.")
self.xmpp.disconnect()
else:
resp.send(now=True)
else:
log.error("No appropriate login method.")
self.xmpp.event("no_auth", direct=True)
self.attempted_mechs = set()
self.xmpp.disconnect()
return True
def _handle_challenge(self, stanza):
"""SASL challenge received. Process and send response."""
resp = self.stanza.Response(self.xmpp)
resp['value'] = self.mech.process(stanza['value'])
resp.send(now=True)
try:
resp['value'] = self.mech.process(stanza['value'])
except SASLCancelled:
self.stanza.Abort(self.xmpp).send()
except SASLError:
self.stanza.Abort(self.xmpp).send()
else:
resp.send(now=True)
def _handle_success(self, stanza):
"""SASL authentication succeeded. Restart the stream."""
self.attempted_mechs = set()
self.xmpp.authenticated = True
self.xmpp.features.add('mechanisms')
self.xmpp.event('auth_success', stanza, direct=True)
raise RestartStream()
def _handle_fail(self, stanza):
"""SASL authentication failed. Disconnect and shutdown."""
self.attempted_mechs.add(self.mech.name)
log.info("Authentication failed: %s", stanza['condition'])
self.xmpp.event("failed_auth", stanza, direct=True)
self.xmpp.disconnect()
self._send_auth()
return True

View File

@@ -13,3 +13,4 @@ from sleekxmpp.features.feature_mechanisms.stanza.success import Success
from sleekxmpp.features.feature_mechanisms.stanza.failure import Failure
from sleekxmpp.features.feature_mechanisms.stanza.challenge import Challenge
from sleekxmpp.features.feature_mechanisms.stanza.response import Response
from sleekxmpp.features.feature_mechanisms.stanza.abort import Abort

View File

@@ -0,0 +1,24 @@
"""
SleekXMPP: The Sleek XMPP Library
Copyright (C) 2011 Nathanael C. Fritz
This file is part of SleekXMPP.
See the file LICENSE for copying permission.
"""
from sleekxmpp.xmlstream import StanzaBase
class Abort(StanzaBase):
"""
"""
name = 'abort'
namespace = 'urn:ietf:params:xml:ns:xmpp-sasl'
interfaces = set()
plugin_attrib = name
def setup(self, xml):
StanzaBase.setup(self, xml)
self.xml.tag = self.tag_name()

View File

@@ -10,9 +10,7 @@ import base64
from sleekxmpp.thirdparty.suelta.util import bytes
from sleekxmpp.stanza import StreamFeatures
from sleekxmpp.xmlstream import ElementBase, StanzaBase, ET
from sleekxmpp.xmlstream import register_stanza_plugin
from sleekxmpp.xmlstream import StanzaBase
class Auth(StanzaBase):
@@ -25,15 +23,28 @@ class Auth(StanzaBase):
interfaces = set(('mechanism', 'value'))
plugin_attrib = name
#: Some SASL mechs require sending values as is,
#: without converting base64.
plain_mechs = set(['X-MESSENGER-OAUTH2'])
def setup(self, xml):
StanzaBase.setup(self, xml)
self.xml.tag = self.tag_name()
def get_value(self):
return base64.b64decode(bytes(self.xml.text))
if not self['mechanism'] in self.plain_mechs:
return base64.b64decode(bytes(self.xml.text))
else:
return self.xml.text
def set_value(self, values):
self.xml.text = bytes(base64.b64encode(values)).decode('utf-8')
if not self['mechanism'] in self.plain_mechs:
if values:
self.xml.text = bytes(base64.b64encode(values)).decode('utf-8')
else:
self.xml.text = '='
else:
self.xml.text = bytes(values).decode('utf-8')
def del_value(self):
self.xml.text = ''

View File

@@ -10,9 +10,7 @@ import base64
from sleekxmpp.thirdparty.suelta.util import bytes
from sleekxmpp.stanza import StreamFeatures
from sleekxmpp.xmlstream import ElementBase, StanzaBase, ET
from sleekxmpp.xmlstream import register_stanza_plugin
from sleekxmpp.xmlstream import StanzaBase
class Challenge(StanzaBase):
@@ -33,7 +31,10 @@ class Challenge(StanzaBase):
return base64.b64decode(bytes(self.xml.text))
def set_value(self, values):
self.xml.text = bytes(base64.b64encode(values)).decode('utf-8')
if values:
self.xml.text = bytes(base64.b64encode(values)).decode('utf-8')
else:
self.xml.text = '='
def del_value(self):
self.xml.text = ''

View File

@@ -6,9 +6,7 @@
See the file LICENSE for copying permission.
"""
from sleekxmpp.stanza import StreamFeatures
from sleekxmpp.xmlstream import ElementBase, StanzaBase, ET
from sleekxmpp.xmlstream import register_stanza_plugin
from sleekxmpp.xmlstream import StanzaBase, ET
class Failure(StanzaBase):
@@ -49,7 +47,7 @@ class Failure(StanzaBase):
def get_condition(self):
"""Return the condition element's name."""
for child in self.xml.getchildren():
for child in self.xml:
if "{%s}" % self.namespace in child.tag:
cond = child.tag.split('}', 1)[-1]
if cond in self.conditions:
@@ -70,7 +68,7 @@ class Failure(StanzaBase):
def del_condition(self):
"""Remove the condition element."""
for child in self.xml.getchildren():
for child in self.xml:
if "{%s}" % self.condition_ns in child.tag:
tag = child.tag.split('}', 1)[-1]
if tag in self.conditions:

View File

@@ -6,9 +6,7 @@
See the file LICENSE for copying permission.
"""
from sleekxmpp.stanza import StreamFeatures
from sleekxmpp.xmlstream import ElementBase, StanzaBase, ET
from sleekxmpp.xmlstream import register_stanza_plugin
from sleekxmpp.xmlstream import ElementBase, ET
class Mechanisms(ElementBase):

View File

@@ -10,9 +10,7 @@ import base64
from sleekxmpp.thirdparty.suelta.util import bytes
from sleekxmpp.stanza import StreamFeatures
from sleekxmpp.xmlstream import ElementBase, StanzaBase, ET
from sleekxmpp.xmlstream import register_stanza_plugin
from sleekxmpp.xmlstream import StanzaBase
class Response(StanzaBase):
@@ -33,7 +31,10 @@ class Response(StanzaBase):
return base64.b64decode(bytes(self.xml.text))
def set_value(self, values):
self.xml.text = bytes(base64.b64encode(values)).decode('utf-8')
if values:
self.xml.text = bytes(base64.b64encode(values)).decode('utf-8')
else:
self.xml.text = '='
def del_value(self):
self.xml.text = ''

View File

@@ -6,9 +6,7 @@
See the file LICENSE for copying permission.
"""
from sleekxmpp.stanza import StreamFeatures
from sleekxmpp.xmlstream import ElementBase, StanzaBase, ET
from sleekxmpp.xmlstream import register_stanza_plugin
from sleekxmpp.xmlstream import StanzaBase
class Success(StanzaBase):

View File

@@ -0,0 +1,19 @@
"""
SleekXMPP: The Sleek XMPP Library
Copyright (C) 2012 Nathanael C. Fritz
This file is part of SleekXMPP.
See the file LICENSE for copying permission.
"""
from sleekxmpp.plugins.base import register_plugin
from sleekxmpp.features.feature_rosterver.rosterver import FeatureRosterVer
from sleekxmpp.features.feature_rosterver.stanza import RosterVer
register_plugin(FeatureRosterVer)
# Retain some backwards compatibility
feature_rosterver = FeatureRosterVer

View File

@@ -0,0 +1,42 @@
"""
SleekXMPP: The Sleek XMPP Library
Copyright (C) 2012 Nathanael C. Fritz
This file is part of SleekXMPP.
See the file LICENSE for copying permission.
"""
import logging
from sleekxmpp.stanza import Iq, StreamFeatures
from sleekxmpp.features.feature_rosterver import stanza
from sleekxmpp.xmlstream import register_stanza_plugin
from sleekxmpp.plugins.base import BasePlugin
log = logging.getLogger(__name__)
class FeatureRosterVer(BasePlugin):
name = 'feature_rosterver'
description = 'RFC 6121: Stream Feature: Roster Versioning'
dependences = set()
stanza = stanza
def plugin_init(self):
self.xmpp.register_feature('rosterver',
self._handle_rosterver,
restart=False,
order=9000)
register_stanza_plugin(StreamFeatures, stanza.RosterVer)
def _handle_rosterver(self, features):
"""Enable using roster versioning.
Arguments:
features -- The stream features stanza.
"""
log.debug("Enabling roster versioning.")
self.xmpp.features.add('rosterver')

View File

@@ -0,0 +1,17 @@
"""
SleekXMPP: The Sleek XMPP Library
Copyright (C) 2012 Nathanael C. Fritz
This file is part of SleekXMPP.
See the file LICENSE for copying permission.
"""
from sleekxmpp.xmlstream import ElementBase
class RosterVer(ElementBase):
name = 'ver'
namespace = 'urn:xmpp:features:rosterver'
interfaces = set()
plugin_attrib = 'rosterver'

View File

@@ -6,5 +6,14 @@
See the file LICENSE for copying permission.
"""
from sleekxmpp.features.feature_session.session import feature_session
from sleekxmpp.plugins.base import register_plugin
from sleekxmpp.features.feature_session.session import FeatureSession
from sleekxmpp.features.feature_session.stanza import Session
register_plugin(FeatureSession)
# Retain some backwards compatibility
feature_session = FeatureSession

View File

@@ -10,9 +10,7 @@ import logging
from sleekxmpp.stanza import Iq, StreamFeatures
from sleekxmpp.xmlstream import register_stanza_plugin
from sleekxmpp.xmlstream.matcher import *
from sleekxmpp.xmlstream.handler import *
from sleekxmpp.plugins.base import base_plugin
from sleekxmpp.plugins import BasePlugin
from sleekxmpp.features.feature_session import stanza
@@ -20,14 +18,14 @@ from sleekxmpp.features.feature_session import stanza
log = logging.getLogger(__name__)
class feature_session(base_plugin):
class FeatureSession(BasePlugin):
name = 'feature_session'
description = 'RFC 3920: Stream Feature: Start Session'
dependencies = set()
stanza = stanza
def plugin_init(self):
self.name = 'Start Session'
self.rfc = '3920'
self.description = 'Start Session Stream Feature'
self.stanza = stanza
self.xmpp.register_feature('session',
self._handle_start_session,
restart=False,
@@ -46,7 +44,7 @@ class feature_session(base_plugin):
iq = self.xmpp.Iq()
iq['type'] = 'set'
iq.enable('session')
response = iq.send(now=True)
iq.send(now=True)
self.xmpp.features.add('session')

View File

@@ -6,8 +6,7 @@
See the file LICENSE for copying permission.
"""
from sleekxmpp.stanza import Iq, StreamFeatures
from sleekxmpp.xmlstream import ElementBase, ET, register_stanza_plugin
from sleekxmpp.xmlstream import ElementBase
class Session(ElementBase):

View File

@@ -6,5 +6,14 @@
See the file LICENSE for copying permission.
"""
from sleekxmpp.features.feature_starttls.starttls import feature_starttls
from sleekxmpp.plugins.base import register_plugin
from sleekxmpp.features.feature_starttls.starttls import FeatureSTARTTLS
from sleekxmpp.features.feature_starttls.stanza import *
register_plugin(FeatureSTARTTLS)
# Retain some backwards compatibility
feature_starttls = FeatureSTARTTLS

View File

@@ -6,9 +6,7 @@
See the file LICENSE for copying permission.
"""
from sleekxmpp.stanza import StreamFeatures
from sleekxmpp.xmlstream import StanzaBase, ElementBase
from sleekxmpp.xmlstream import register_stanza_plugin
class STARTTLS(ElementBase):

View File

@@ -10,23 +10,23 @@ import logging
from sleekxmpp.stanza import StreamFeatures
from sleekxmpp.xmlstream import RestartStream, register_stanza_plugin
from sleekxmpp.xmlstream.matcher import *
from sleekxmpp.xmlstream.handler import *
from sleekxmpp.plugins.base import base_plugin
from sleekxmpp.plugins import BasePlugin
from sleekxmpp.xmlstream.matcher import MatchXPath
from sleekxmpp.xmlstream.handler import Callback
from sleekxmpp.features.feature_starttls import stanza
log = logging.getLogger(__name__)
class feature_starttls(base_plugin):
class FeatureSTARTTLS(BasePlugin):
name = 'feature_starttls'
description = 'RFC 6120: Stream Feature: STARTTLS'
dependencies = set()
stanza = stanza
def plugin_init(self):
self.name = "STARTTLS"
self.rfc = '6120'
self.description = "STARTTLS Stream Feature"
self.stanza = stanza
self.xmpp.register_handler(
Callback('STARTTLS Proceed',
MatchXPath(stanza.Proceed.tag_name()),

543
sleekxmpp/jid.py Normal file
View File

@@ -0,0 +1,543 @@
# -*- coding: utf-8 -*-
"""
sleekxmpp.jid
~~~~~~~~~~~~~~~~~~~~~~~
This module allows for working with Jabber IDs (JIDs).
Part of SleekXMPP: The Sleek XMPP Library
:copyright: (c) 2011 Nathanael C. Fritz
:license: MIT, see LICENSE for more details
"""
from __future__ import unicode_literals
import re
import socket
import stringprep
import encodings.idna
from sleekxmpp.util import stringprep_profiles
#: These characters are not allowed to appear in a JID.
ILLEGAL_CHARS = '\x00\x01\x02\x03\x04\x05\x06\x07\x08\t\n\x0b\x0c\r' + \
'\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19' + \
'\x1a\x1b\x1c\x1d\x1e\x1f' + \
' !"#$%&\'()*+,./:;<=>?@[\\]^_`{|}~\x7f'
#: The basic regex pattern that a JID must match in order to determine
#: the local, domain, and resource parts. This regex does NOT do any
#: validation, which requires application of nodeprep, resourceprep, etc.
JID_PATTERN = re.compile(
"^(?:([^\"&'/:<>@]{1,1023})@)?([^/@]{1,1023})(?:/(.{1,1023}))?$"
)
#: The set of escape sequences for the characters not allowed by nodeprep.
JID_ESCAPE_SEQUENCES = set(['\\20', '\\22', '\\26', '\\27', '\\2f',
'\\3a', '\\3c', '\\3e', '\\40', '\\5c'])
#: A mapping of unallowed characters to their escape sequences. An escape
#: sequence for '\' is also included since it must also be escaped in
#: certain situations.
JID_ESCAPE_TRANSFORMATIONS = {' ': '\\20',
'"': '\\22',
'&': '\\26',
"'": '\\27',
'/': '\\2f',
':': '\\3a',
'<': '\\3c',
'>': '\\3e',
'@': '\\40',
'\\': '\\5c'}
#: The reverse mapping of escape sequences to their original forms.
JID_UNESCAPE_TRANSFORMATIONS = {'\\20': ' ',
'\\22': '"',
'\\26': '&',
'\\27': "'",
'\\2f': '/',
'\\3a': ':',
'\\3c': '<',
'\\3e': '>',
'\\40': '@',
'\\5c': '\\'}
# pylint: disable=c0103
#: The nodeprep profile of stringprep used to validate the local,
#: or username, portion of a JID.
nodeprep = stringprep_profiles.create(
nfkc=True,
bidi=True,
mappings=[
stringprep_profiles.b1_mapping,
stringprep_profiles.c12_mapping],
prohibited=[
stringprep.in_table_c11,
stringprep.in_table_c12,
stringprep.in_table_c21,
stringprep.in_table_c22,
stringprep.in_table_c3,
stringprep.in_table_c4,
stringprep.in_table_c5,
stringprep.in_table_c6,
stringprep.in_table_c7,
stringprep.in_table_c8,
stringprep.in_table_c9,
lambda c: c in ' \'"&/:<>@'],
unassigned=[stringprep.in_table_a1])
# pylint: disable=c0103
#: The resourceprep profile of stringprep, which is used to validate
#: the resource portion of a JID.
resourceprep = stringprep_profiles.create(
nfkc=True,
bidi=True,
mappings=[stringprep_profiles.b1_mapping],
prohibited=[
stringprep.in_table_c12,
stringprep.in_table_c21,
stringprep.in_table_c22,
stringprep.in_table_c3,
stringprep.in_table_c4,
stringprep.in_table_c5,
stringprep.in_table_c6,
stringprep.in_table_c7,
stringprep.in_table_c8,
stringprep.in_table_c9],
unassigned=[stringprep.in_table_a1])
def _parse_jid(data):
"""
Parse string data into the node, domain, and resource
components of a JID, if possible.
:param string data: A string that is potentially a JID.
:raises InvalidJID:
:returns: tuple of the validated local, domain, and resource strings
"""
match = JID_PATTERN.match(data)
if not match:
raise InvalidJID('JID could not be parsed')
(node, domain, resource) = match.groups()
node = _validate_node(node)
domain = _validate_domain(domain)
resource = _validate_resource(resource)
return node, domain, resource
def _validate_node(node):
"""Validate the local, or username, portion of a JID.
:raises InvalidJID:
:returns: The local portion of a JID, as validated by nodeprep.
"""
try:
if node is not None:
node = nodeprep(node)
if not node:
raise InvalidJID('Localpart must not be 0 bytes')
if len(node) > 1023:
raise InvalidJID('Localpart must be less than 1024 bytes')
return node
except stringprep_profiles.StringPrepError:
raise InvalidJID('Invalid local part')
def _validate_domain(domain):
"""Validate the domain portion of a JID.
IP literal addresses are left as-is, if valid. Domain names
are stripped of any trailing label separators (`.`), and are
checked with the nameprep profile of stringprep. If the given
domain is actually a punyencoded version of a domain name, it
is converted back into its original Unicode form. Domains must
also not start or end with a dash (`-`).
:raises InvalidJID:
:returns: The validated domain name
"""
ip_addr = False
# First, check if this is an IPv4 address
try:
socket.inet_aton(domain)
ip_addr = True
except socket.error:
pass
# Check if this is an IPv6 address
if not ip_addr and hasattr(socket, 'inet_pton'):
try:
socket.inet_pton(socket.AF_INET6, domain.strip('[]'))
domain = '[%s]' % domain.strip('[]')
ip_addr = True
except socket.error:
pass
if not ip_addr:
# This is a domain name, which must be checked further
if domain and domain[-1] == '.':
domain = domain[:-1]
domain_parts = []
for label in domain.split('.'):
try:
label = encodings.idna.nameprep(label)
encodings.idna.ToASCII(label)
pass_nameprep = True
except UnicodeError:
pass_nameprep = False
if not pass_nameprep:
raise InvalidJID('Could not encode domain as ASCII')
if label.startswith('xn--'):
label = encodings.idna.ToUnicode(label)
for char in label:
if char in ILLEGAL_CHARS:
raise InvalidJID('Domain contains illegar characters')
if '-' in (label[0], label[-1]):
raise InvalidJID('Domain started or ended with -')
domain_parts.append(label)
domain = '.'.join(domain_parts)
if not domain:
raise InvalidJID('Domain must not be 0 bytes')
if len(domain) > 1023:
raise InvalidJID('Domain must be less than 1024 bytes')
return domain
def _validate_resource(resource):
"""Validate the resource portion of a JID.
:raises InvalidJID:
:returns: The local portion of a JID, as validated by resourceprep.
"""
try:
if resource is not None:
resource = resourceprep(resource)
if not resource:
raise InvalidJID('Resource must not be 0 bytes')
if len(resource) > 1023:
raise InvalidJID('Resource must be less than 1024 bytes')
return resource
except stringprep_profiles.StringPrepError:
raise InvalidJID('Invalid resource')
def _escape_node(node):
"""Escape the local portion of a JID."""
result = []
for i, char in enumerate(node):
if char == '\\':
if ''.join((node[i:i+3])) in JID_ESCAPE_SEQUENCES:
result.append('\\5c')
continue
result.append(char)
for i, char in enumerate(result):
if char != '\\':
result[i] = JID_ESCAPE_TRANSFORMATIONS.get(char, char)
escaped = ''.join(result)
if escaped.startswith('\\20') or escaped.endswith('\\20'):
raise InvalidJID('Escaped local part starts or ends with "\\20"')
_validate_node(escaped)
return escaped
def _unescape_node(node):
"""Unescape a local portion of a JID.
.. note::
The unescaped local portion is meant ONLY for presentation,
and should not be used for other purposes.
"""
unescaped = []
seq = ''
for i, char in enumerate(node):
if char == '\\':
seq = node[i:i+3]
if seq not in JID_ESCAPE_SEQUENCES:
seq = ''
if seq:
if len(seq) == 3:
unescaped.append(JID_UNESCAPE_TRANSFORMATIONS.get(seq, char))
# Pop character off the escape sequence, and ignore it
seq = seq[1:]
else:
unescaped.append(char)
unescaped = ''.join(unescaped)
return unescaped
def _format_jid(local=None, domain=None, resource=None):
"""Format the given JID components into a full or bare JID.
:param string local: Optional. The local portion of the JID.
:param string domain: Required. The domain name portion of the JID.
:param strin resource: Optional. The resource portion of the JID.
:return: A full or bare JID string.
"""
result = []
if local:
result.append(local)
result.append('@')
if domain:
result.append(domain)
if resource:
result.append('/')
result.append(resource)
return ''.join(result)
class InvalidJID(ValueError):
"""
Raised when attempting to create a JID that does not pass validation.
It can also be raised if modifying an existing JID in such a way as
to make it invalid, such trying to remove the domain from an existing
full JID while the local and resource portions still exist.
"""
# pylint: disable=R0903
class UnescapedJID(object):
"""
.. versionadded:: 1.1.10
"""
def __init__(self, local, domain, resource):
self._jid = (local, domain, resource)
# pylint: disable=R0911
def __getattr__(self, name):
"""Retrieve the given JID component.
:param name: one of: user, server, domain, resource,
full, or bare.
"""
if name == 'resource':
return self._jid[2] or ''
elif name in ('user', 'username', 'local', 'node'):
return self._jid[0] or ''
elif name in ('server', 'domain', 'host'):
return self._jid[1] or ''
elif name in ('full', 'jid'):
return _format_jid(*self._jid)
elif name == 'bare':
return _format_jid(self._jid[0], self._jid[1])
elif name == '_jid':
return getattr(super(JID, self), '_jid')
else:
return None
def __str__(self):
"""Use the full JID as the string value."""
return _format_jid(*self._jid)
def __repr__(self):
"""Use the full JID as the representation."""
return self.__str__()
class JID(object):
"""
A representation of a Jabber ID, or JID.
Each JID may have three components: a user, a domain, and an optional
resource. For example: user@domain/resource
When a resource is not used, the JID is called a bare JID.
The JID is a full JID otherwise.
**JID Properties:**
:jid: Alias for ``full``.
:full: The string value of the full JID.
:bare: The string value of the bare JID.
:user: The username portion of the JID.
:username: Alias for ``user``.
:local: Alias for ``user``.
:node: Alias for ``user``.
:domain: The domain name portion of the JID.
:server: Alias for ``domain``.
:host: Alias for ``domain``.
:resource: The resource portion of the JID.
:param string jid:
A string of the form ``'[user@]domain[/resource]'``.
:param string local:
Optional. Specify the local, or username, portion
of the JID. If provided, it will override the local
value provided by the `jid` parameter. The given
local value will also be escaped if necessary.
:param string domain:
Optional. Specify the domain of the JID. If
provided, it will override the domain given by
the `jid` parameter.
:param string resource:
Optional. Specify the resource value of the JID.
If provided, it will override the domain given
by the `jid` parameter.
:raises InvalidJID:
"""
# pylint: disable=W0212
def __init__(self, jid=None, **kwargs):
self._jid = (None, None, None)
if jid is None or jid == '':
jid = (None, None, None)
elif not isinstance(jid, JID):
jid = _parse_jid(jid)
else:
jid = jid._jid
local, domain, resource = jid
local = kwargs.get('local', local)
domain = kwargs.get('domain', domain)
resource = kwargs.get('resource', resource)
if 'local' in kwargs:
local = _escape_node(local)
if 'domain' in kwargs:
domain = _validate_domain(domain)
if 'resource' in kwargs:
resource = _validate_resource(resource)
self._jid = (local, domain, resource)
def unescape(self):
"""Return an unescaped JID object.
Using an unescaped JID is preferred for displaying JIDs
to humans, and they should NOT be used for any other
purposes than for presentation.
:return: :class:`UnescapedJID`
.. versionadded:: 1.1.10
"""
return UnescapedJID(_unescape_node(self._jid[0]),
self._jid[1],
self._jid[2])
def regenerate(self):
"""No-op
.. deprecated:: 1.1.10
"""
pass
def reset(self, data):
"""Start fresh from a new JID string.
:param string data: A string of the form ``'[user@]domain[/resource]'``.
.. deprecated:: 1.1.10
"""
self._jid = JID(data)._jid
# pylint: disable=R0911
def __getattr__(self, name):
"""Retrieve the given JID component.
:param name: one of: user, server, domain, resource,
full, or bare.
"""
if name == 'resource':
return self._jid[2] or ''
elif name in ('user', 'username', 'local', 'node'):
return self._jid[0] or ''
elif name in ('server', 'domain', 'host'):
return self._jid[1] or ''
elif name in ('full', 'jid'):
return _format_jid(*self._jid)
elif name == 'bare':
return _format_jid(self._jid[0], self._jid[1])
elif name == '_jid':
return getattr(super(JID, self), '_jid')
else:
return None
# pylint: disable=W0212
def __setattr__(self, name, value):
"""Update the given JID component.
:param name: one of: ``user``, ``username``, ``local``,
``node``, ``server``, ``domain``, ``host``,
``resource``, ``full``, ``jid``, or ``bare``.
:param value: The new string value of the JID component.
"""
if name == 'resource':
self._jid = JID(self, resource=value)._jid
elif name in ('user', 'username', 'local', 'node'):
self._jid = JID(self, local=value)._jid
elif name in ('server', 'domain', 'host'):
self._jid = JID(self, domain=value)._jid
elif name in ('full', 'jid'):
self._jid = JID(value)._jid
elif name == 'bare':
parsed = JID(value)._jid
self._jid = (parsed[0], parsed[1], self._jid[2])
elif name == '_jid':
super(JID, self).__setattr__('_jid', value)
def __str__(self):
"""Use the full JID as the string value."""
return _format_jid(*self._jid)
def __repr__(self):
"""Use the full JID as the representation."""
return self.__str__()
# pylint: disable=W0212
def __eq__(self, other):
"""Two JIDs are equal if they have the same full JID value."""
if isinstance(other, UnescapedJID):
return False
other = JID(other)
return self._jid == other._jid
# pylint: disable=W0212
def __ne__(self, other):
"""Two JIDs are considered unequal if they are not equal."""
return not self == other
def __hash__(self):
"""Hash a JID based on the string version of its full JID."""
return hash(self.__str__())
def __copy__(self):
"""Generate a duplicate JID."""
return JID(self)

View File

@@ -5,9 +5,63 @@
See the file LICENSE for copying permission.
"""
__all__ = ['xep_0004', 'xep_0009', 'xep_0012', 'xep_0030', 'xep_0033',
'xep_0045', 'xep_0050', 'xep_0060', 'xep_0066', 'xep_0082',
'xep_0085', 'xep_0086', 'xep_0092', 'xep_0128', 'xep_0199',
'xep_0203', 'xep_0224', 'xep_0249', 'gmail_notify']
# Don't automatically load xep_0078
from sleekxmpp.plugins.base import PluginManager, PluginNotFound, BasePlugin
from sleekxmpp.plugins.base import register_plugin, load_plugin
__all__ = [
# Non-standard
'gmail_notify', # Gmail searching and notifications
# XEPS
'xep_0004', # Data Forms
'xep_0009', # Jabber-RPC
'xep_0012', # Last Activity
'xep_0027', # Current Jabber OpenPGP Usage
'xep_0030', # Service Discovery
'xep_0033', # Extended Stanza Addresses
'xep_0045', # Multi-User Chat (Client)
'xep_0047', # In-Band Bytestreams
'xep_0050', # Ad-hoc Commands
'xep_0054', # vcard-temp
'xep_0059', # Result Set Management
'xep_0060', # Pubsub (Client)
'xep_0066', # Out of Band Data
'xep_0077', # In-Band Registration
# 'xep_0078', # Non-SASL auth. Don't automatically load
'xep_0080', # User Location
'xep_0082', # XMPP Date and Time Profiles
'xep_0084', # User Avatar
'xep_0085', # Chat State Notifications
'xep_0086', # Legacy Error Codes
'xep_0092', # Software Version
'xep_0106', # JID Escaping
'xep_0107', # User Mood
'xep_0108', # User Activity
'xep_0115', # Entity Capabilities
'xep_0118', # User Tune
'xep_0128', # Extended Service Discovery
'xep_0131', # Standard Headers and Internet Metadata
'xep_0133', # Service Administration
'xep_0153', # vCard-Based Avatars
'xep_0163', # Personal Eventing Protocol
'xep_0172', # User Nickname
'xep_0184', # Message Receipts
'xep_0186', # Invisible Command
'xep_0191', # Blocking Command
'xep_0198', # Stream Management
'xep_0199', # Ping
'xep_0202', # Entity Time
'xep_0203', # Delayed Delivery
'xep_0221', # Data Forms Media Element
'xep_0222', # Persistent Storage of Public Data via Pubsub
'xep_0223', # Persistent Storage of Private Data via Pubsub
'xep_0224', # Attention
'xep_0231', # Bits of Binary
'xep_0249', # Direct MUC Invitations
'xep_0256', # Last Activity in Presence
'xep_0258', # Security Labels in XMPP
'xep_0270', # XMPP Compliance Suites 2010
'xep_0302', # XMPP Compliance Suites 2012
]

View File

@@ -1,91 +1,360 @@
"""
SleekXMPP: The Sleek XMPP Library
Copyright (C) 2010 Nathanael C. Fritz
This file is part of SleekXMPP.
# -*- encoding: utf-8 -*-
See the file LICENSE for copying permission.
"""
sleekxmpp.plugins.base
~~~~~~~~~~~~~~~~~~~~~~
This module provides XMPP functionality that
is specific to client connections.
Part of SleekXMPP: The Sleek XMPP Library
:copyright: (c) 2012 Nathanael C. Fritz
:license: MIT, see LICENSE for more details
"""
import sys
import copy
import logging
import threading
class base_plugin(object):
if sys.version_info >= (3, 0):
unicode = str
log = logging.getLogger(__name__)
#: Associate short string names of plugins with implementations. The
#: plugin names are based on the spec used by the plugin, such as
#: `'xep_0030'` for a plugin that implements XEP-0030.
PLUGIN_REGISTRY = {}
#: In order to do cascading plugin disabling, reverse dependencies
#: must be tracked.
PLUGIN_DEPENDENTS = {}
#: Only allow one thread to manipulate the plugin registry at a time.
REGISTRY_LOCK = threading.RLock()
class PluginNotFound(Exception):
"""Raised if an unknown plugin is accessed."""
def register_plugin(impl, name=None):
"""Add a new plugin implementation to the registry.
:param class impl: The plugin class.
The implementation class must provide a :attr:`~BasePlugin.name`
value that will be used as a short name for enabling and disabling
the plugin. The name should be based on the specification used by
the plugin. For example, a plugin implementing XEP-0030 would be
named `'xep_0030'`.
"""
The base_plugin class serves as a base for user created plugins
that provide support for existing or experimental XEPS.
if name is None:
name = impl.name
with REGISTRY_LOCK:
PLUGIN_REGISTRY[name] = impl
if name not in PLUGIN_DEPENDENTS:
PLUGIN_DEPENDENTS[name] = set()
for dep in impl.dependencies:
if dep not in PLUGIN_DEPENDENTS:
PLUGIN_DEPENDENTS[dep] = set()
PLUGIN_DEPENDENTS[dep].add(name)
Each plugin has a dictionary for configuration options, as well
as a name and description.
The lifecycle of a plugin is:
1. The plugin is instantiated during registration.
2. Once the XML stream begins processing, the method
plugin_init() is called (if the plugin is configured
as enabled with {'enable': True}).
3. After all plugins have been initialized, the
method post_init() is called.
def load_plugin(name, module=None):
"""Find and import a plugin module so that it can be registered.
Recommended event handlers:
session_start -- Plugins which require the use of the current
bound JID SHOULD wait for the session_start
event to perform any initialization (or
resetting). This is a transitive recommendation,
plugins that use other plugins which use the
bound JID should also wait for session_start
before making such calls.
session_end -- If the plugin keeps any per-session state,
such as joined MUC rooms, such state SHOULD
be cleared when the session_end event is raised.
This function is called to import plugins that have selected for
enabling, but no matching registered plugin has been found.
Attributes:
xep -- The XEP number the plugin implements, if any.
description -- A short description of the plugin, typically
the long name of the implemented XEP.
xmpp -- The main SleekXMPP instance.
config -- A dictionary of custom configuration values.
The value 'enable' is special and controls
whether or not the plugin is initialized
after registration.
post_initted -- Executed after all plugins have been initialized
to handle any cross-plugin interactions, such as
registering service discovery items.
enable -- Indicates that the plugin is enabled for use and
will be initialized after registration.
Methods:
plugin_init -- Initialize the plugin state.
post_init -- Handle any cross-plugin concerns.
:param str name: The name of the plugin. It is expected that
plugins are in packages matching their name,
even though the plugin class name does not
have to match.
:param str module: The name of the base module to search
for the plugin.
"""
try:
if not module:
try:
module = 'sleekxmpp.plugins.%s' % name
__import__(module)
mod = sys.modules[module]
except ImportError:
module = 'sleekxmpp.features.%s' % name
__import__(module)
mod = sys.modules[module]
elif isinstance(module, (str, unicode)):
__import__(module)
mod = sys.modules[module]
else:
mod = module
# Add older style plugins to the registry.
if hasattr(mod, name):
plugin = getattr(mod, name)
if hasattr(plugin, 'xep') or hasattr(plugin, 'rfc'):
plugin.name = name
# Mark the plugin as an older style plugin so
# we can work around dependency issues.
plugin.old_style = True
register_plugin(plugin, name)
except ImportError:
log.exception("Unable to load plugin: %s", name)
class PluginManager(object):
def __init__(self, xmpp, config=None):
"""
Instantiate a new plugin and store the given configuration.
#: We will track all enabled plugins in a set so that we
#: can enable plugins in batches and pull in dependencies
#: without problems.
self._enabled = set()
Arguments:
xmpp -- The main SleekXMPP instance.
config -- A dictionary of configuration values.
#: Maintain references to active plugins.
self._plugins = {}
self._plugin_lock = threading.RLock()
#: Globally set default plugin configuration. This will
#: be used for plugins that are auto-enabled through
#: dependency loading.
self.config = config if config else {}
self.xmpp = xmpp
def register(self, plugin, enable=True):
"""Register a new plugin, and optionally enable it.
:param class plugin: The implementation class of the plugin
to register.
:param bool enable: If ``True``, immediately enable the
plugin after registration.
"""
register_plugin(plugin)
if enable:
self.enable(plugin.name)
def enable(self, name, config=None, enabled=None):
"""Enable a plugin, including any dependencies.
:param string name: The short name of the plugin.
:param dict config: Optional settings dictionary for
configuring plugin behaviour.
"""
top_level = False
if enabled is None:
enabled = set()
with self._plugin_lock:
if name not in self._enabled:
enabled.add(name)
self._enabled.add(name)
if not self.registered(name):
load_plugin(name)
plugin_class = PLUGIN_REGISTRY.get(name, None)
if not plugin_class:
raise PluginNotFound(name)
if config is None:
config = self.config.get(name, None)
plugin = plugin_class(self.xmpp, config)
self._plugins[name] = plugin
for dep in plugin.dependencies:
self.enable(dep, enabled=enabled)
plugin._init()
if top_level:
for name in enabled:
if hasattr(self.plugins[name], 'old_style'):
# Older style plugins require post_init()
# to run just before stream processing begins,
# so we don't call it here.
pass
self.plugins[name].post_init()
def enable_all(self, names=None, config=None):
"""Enable all registered plugins.
:param list names: A list of plugin names to enable. If
none are provided, all registered plugins
will be enabled.
:param dict config: A dictionary mapping plugin names to
configuration dictionaries, as used by
:meth:`~PluginManager.enable`.
"""
names = names if names else PLUGIN_REGISTRY.keys()
if config is None:
config = {}
self.xep = None
self.rfc = None
self.description = 'Base Plugin'
for name in names:
self.enable(name, config.get(name, {}))
def enabled(self, name):
"""Check if a plugin has been enabled.
:param string name: The name of the plugin to check.
:return: boolean
"""
return name in self._enabled
def registered(self, name):
"""Check if a plugin has been registered.
:param string name: The name of the plugin to check.
:return: boolean
"""
return name in PLUGIN_REGISTRY
def disable(self, name, _disabled=None):
"""Disable a plugin, including any dependent upon it.
:param string name: The name of the plugin to disable.
:param set _disabled: Private set used to track the
disabled status of plugins during
the cascading process.
"""
if _disabled is None:
_disabled = set()
with self._plugin_lock:
if name not in _disabled and name in self._enabled:
_disabled.add(name)
plugin = self._plugins.get(name, None)
if plugin is None:
raise PluginNotFound(name)
for dep in PLUGIN_DEPENDENTS[name]:
self.disable(dep, _disabled)
plugin._end()
if name in self._enabled:
self._enabled.remove(name)
del self._plugins[name]
def __keys__(self):
"""Return the set of enabled plugins."""
return self._plugins.keys()
def __getitem__(self, name):
"""
Allow plugins to be accessed through the manager as if
it were a dictionary.
"""
plugin = self._plugins.get(name, None)
if plugin is None:
raise PluginNotFound(name)
return plugin
def __iter__(self):
"""Return an iterator over the set of enabled plugins."""
return self._plugins.__iter__()
def __len__(self):
"""Return the number of enabled plugins."""
return len(self._plugins)
class BasePlugin(object):
#: A short name for the plugin based on the implemented specification.
#: For example, a plugin for XEP-0030 would use `'xep_0030'`.
name = ''
#: A longer name for the plugin, describing its purpose. For example,
#: a plugin for XEP-0030 would use `'Service Discovery'` as its
#: description value.
description = ''
#: Some plugins may depend on others in order to function properly.
#: Any plugin names included in :attr:`~BasePlugin.dependencies` will
#: be initialized as needed if this plugin is enabled.
dependencies = set()
#: The basic, standard configuration for the plugin, which may
#: be overridden when initializing the plugin. The configuration
#: fields included here may be accessed directly as attributes of
#: the plugin. For example, including the configuration field 'foo'
#: would mean accessing `plugin.foo` returns the current value of
#: `plugin.config['foo']`.
default_config = {}
def __init__(self, xmpp, config=None):
self.xmpp = xmpp
self.config = config
self.post_inited = False
self.enable = config.get('enable', True)
if self.enable:
self.plugin_init()
if self.xmpp:
self.api = self.xmpp.api.wrap(self.name)
#: A plugin's behaviour may be configurable, in which case those
#: configuration settings will be provided as a dictionary.
self.config = copy.copy(self.default_config)
if config:
self.config.update(config)
def __getattr__(self, key):
"""Provide direct access to configuration fields.
If the standard configuration includes the option `'foo'`, then
accessing `self.foo` should be the same as `self.config['foo']`.
"""
if key in self.default_config:
return self.config.get(key, None)
else:
return object.__getattribute__(self, key)
def __setattr__(self, key, value):
"""Provide direct assignment to configuration fields.
If the standard configuration includes the option `'foo'`, then
assigning to `self.foo` should be the same as assigning to
`self.config['foo']`.
"""
if key in self.default_config:
self.config[key] = value
else:
super(BasePlugin, self).__setattr__(key, value)
def _init(self):
"""Initialize plugin state, such as registering event handlers.
Also sets up required event handlers.
"""
if self.xmpp is not None:
self.xmpp.add_event_handler('session_bind', self.session_bind)
if self.xmpp.session_bind_event.is_set():
self.session_bind(self.xmpp.boundjid.full)
self.plugin_init()
log.debug('Loaded Plugin: %s', self.description)
def _end(self):
"""Cleanup plugin state, and prepare for plugin removal.
Also removes required event handlers.
"""
if self.xmpp is not None:
self.xmpp.del_event_handler('session_bind', self.session_bind)
self.plugin_end()
log.debug('Disabled Plugin: %s' % self.description)
def plugin_init(self):
"""
Initialize plugin state, such as registering any stream or
event handlers, or new stanza types.
"""
"""Initialize plugin state, such as registering event handlers."""
pass
def plugin_end(self):
"""Cleanup plugin state, and prepare for plugin removal."""
pass
def session_bind(self, jid):
"""Initialize plugin state based on the bound JID."""
pass
def post_init(self):
"""Initialize any cross-plugin state.
Only needed if the plugin has circular dependencies.
"""
Perform any cross-plugin interactions, such as registering
service discovery identities or items.
"""
self.post_inited = True
pass
base_plugin = BasePlugin

View File

@@ -6,6 +6,17 @@
See the file LICENSE for copying permission.
"""
from sleekxmpp.plugins.base import register_plugin
from sleekxmpp.plugins.xep_0004.stanza import Form
from sleekxmpp.plugins.xep_0004.stanza import FormField, FieldOption
from sleekxmpp.plugins.xep_0004.dataforms import xep_0004
from sleekxmpp.plugins.xep_0004.dataforms import XEP_0004
register_plugin(XEP_0004)
# Retain some backwards compatibility
xep_0004 = XEP_0004
xep_0004.makeForm = xep_0004.make_form
xep_0004.buildForm = xep_0004.build_form

View File

@@ -6,30 +6,28 @@
See the file LICENSE for copying permission.
"""
import copy
from sleekxmpp.thirdparty import OrderedDict
from sleekxmpp import Message
from sleekxmpp.xmlstream import register_stanza_plugin, ElementBase, ET
from sleekxmpp.xmlstream import register_stanza_plugin
from sleekxmpp.xmlstream.handler import Callback
from sleekxmpp.xmlstream.matcher import StanzaPath
from sleekxmpp.plugins.base import base_plugin
from sleekxmpp.plugins import BasePlugin
from sleekxmpp.plugins.xep_0004 import stanza
from sleekxmpp.plugins.xep_0004.stanza import Form, FormField, FieldOption
class xep_0004(base_plugin):
class XEP_0004(BasePlugin):
"""
XEP-0004: Data Forms
"""
def plugin_init(self):
self.xep = '0004'
self.description = 'Data Forms'
self.stanza = stanza
name = 'xep_0004'
description = 'XEP-0004: Data Forms'
dependencies = set(['xep_0030'])
stanza = stanza
self.xmpp.registerHandler(
def plugin_init(self):
self.xmpp.register_handler(
Callback('Data Form',
StanzaPath('message/form'),
self.handle_form))
@@ -38,6 +36,13 @@ class xep_0004(base_plugin):
register_stanza_plugin(Form, FormField, iterable=True)
register_stanza_plugin(Message, Form)
def plugin_end(self):
self.xmpp.remove_handler('Data Form')
self.xmpp['xep_0030'].del_feature(feature='jabber:x:data')
def session_bind(self, jid):
self.xmpp['xep_0030'].add_feature('jabber:x:data')
def make_form(self, ftype='form', title='', instructions=''):
f = Form()
f['type'] = ftype
@@ -45,16 +50,8 @@ class xep_0004(base_plugin):
f['instructions'] = instructions
return f
def post_init(self):
base_plugin.post_init(self)
self.xmpp.plugin['xep_0030'].add_feature('jabber:x:data')
def handle_form(self, message):
self.xmpp.event("message_xform", message)
def build_form(self, xml):
return Form(xml=xml)
xep_0004.makeForm = xep_0004.make_form
xep_0004.buildForm = xep_0004.build_form

View File

@@ -79,19 +79,21 @@ class FormField(ElementBase):
reqXML = self.xml.find('{%s}required' % self.namespace)
return reqXML is not None
def get_value(self):
def get_value(self, convert=True):
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
if convert:
return valsXML[0].text in self.true_values
return valsXML[0].text
elif self._type in self.multi_value_types or len(valsXML) > 1:
values = []
for valXML in valsXML:
if valXML.text is None:
valXML.text = ''
values.append(valXML.text)
if self._type == 'text-multi':
if self._type == 'text-multi' and convert:
values = "\n".join(values)
return values
else:
@@ -136,6 +138,8 @@ class FormField(ElementBase):
valXML.text = '0'
self.xml.append(valXML)
elif self._type in self.multi_value_types or self._type in ('', None):
if isinstance(value, bool):
value = [value]
if not isinstance(value, list):
value = value.replace('\r', '')
value = value.split('\n')

View File

@@ -6,6 +6,15 @@
See the file LICENSE for copying permission.
"""
from sleekxmpp.plugins.base import register_plugin
from sleekxmpp.plugins.xep_0009 import stanza
from sleekxmpp.plugins.xep_0009.rpc import xep_0009
from sleekxmpp.plugins.xep_0009.rpc import XEP_0009
from sleekxmpp.plugins.xep_0009.stanza import RPCQuery, MethodCall, MethodResponse
register_plugin(XEP_0009)
# Retain some backwards compatibility
xep_0009 = XEP_0009

View File

@@ -6,10 +6,14 @@
See the file LICENSE for copying permission.
"""
from xml.etree import cElementTree as ET
from sleekxmpp.xmlstream import ET
import base64
import logging
import time
import sys
if sys.version_info > (3, 0):
unicode = str
log = logging.getLogger(__name__)
@@ -54,7 +58,7 @@ def _py2xml(*args):
boolean = ET.Element("{%s}boolean" % _namespace)
boolean.text = str(int(x))
val.append(boolean)
elif type(x) is str:
elif type(x) in (str, unicode):
string = ET.Element("{%s}string" % _namespace)
string.text = x
val.append(string)
@@ -152,7 +156,7 @@ class rpctime(object):
def __init__(self,data=None):
#assume string data is in iso format YYYYMMDDTHH:MM:SS
if type(data) is str:
if type(data) in (str, unicode):
self.timestamp = time.strptime(data,"%Y%m%dT%H:%M:%S")
elif type(data) is time.struct_time:
self.timestamp = data

View File

@@ -6,28 +6,28 @@
See the file LICENSE for copying permission.
"""
from sleekxmpp.plugins import base
from sleekxmpp.plugins.xep_0009.stanza.RPC import RPCQuery, MethodCall, MethodResponse
from sleekxmpp.stanza.iq import Iq
from sleekxmpp.xmlstream.handler.callback import Callback
from sleekxmpp.xmlstream.matcher.xpath import MatchXPath
from sleekxmpp.xmlstream.stanzabase import register_stanza_plugin
from xml.etree import cElementTree as ET
import logging
from sleekxmpp import Iq
from sleekxmpp.xmlstream import ET, register_stanza_plugin
from sleekxmpp.xmlstream.handler import Callback
from sleekxmpp.xmlstream.matcher import MatchXPath
from sleekxmpp.plugins import BasePlugin
from sleekxmpp.plugins.xep_0009 import stanza
from sleekxmpp.plugins.xep_0009.stanza.RPC import RPCQuery, MethodCall, MethodResponse
log = logging.getLogger(__name__)
class XEP_0009(BasePlugin):
class xep_0009(base.base_plugin):
name = 'xep_0009'
description = 'XEP-0009: Jabber-RPC'
dependencies = set(['xep_0030'])
stanza = stanza
def plugin_init(self):
self.xep = '0009'
self.description = 'Jabber-RPC'
#self.stanza = sleekxmpp.plugins.xep_0009.stanza
register_stanza_plugin(Iq, RPCQuery)
register_stanza_plugin(RPCQuery, MethodCall)
register_stanza_plugin(RPCQuery, MethodResponse)
@@ -51,10 +51,8 @@ class xep_0009(base.base_plugin):
self.xmpp.add_event_handler('error', self._handle_error)
#self.activeCalls = []
def post_init(self):
base.base_plugin.post_init(self)
self.xmpp.plugin['xep_0030'].add_feature('jabber:iq:rpc')
self.xmpp.plugin['xep_0030'].add_identity('automation','rpc')
self.xmpp['xep_0030'].add_feature('jabber:iq:rpc')
self.xmpp['xep_0030'].add_identity('automation','rpc')
def make_iq_method_call(self, pto, pmethod, params):
iq = self.xmpp.makeIqSet()
@@ -218,4 +216,3 @@ class xep_0009(base.base_plugin):
def _extract_method(self, stanza):
xml = ET.fromstring("%s" % stanza)
return xml.find("./methodCall/methodName").text

View File

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

View File

@@ -0,0 +1,19 @@
"""
SleekXMPP: The Sleek XMPP Library
Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
This file is part of SleekXMPP.
See the file LICENSE for copying permission.
"""
from sleekxmpp.plugins.base import register_plugin
from sleekxmpp.plugins.xep_0012.stanza import LastActivity
from sleekxmpp.plugins.xep_0012.last_activity import XEP_0012
register_plugin(XEP_0012)
# Retain some backwards compatibility
xep_0004 = XEP_0012

View File

@@ -0,0 +1,157 @@
"""
SleekXMPP: The Sleek XMPP Library
Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
This file is part of SleekXMPP.
See the file LICENSE for copying permission.
"""
import logging
from datetime import datetime, timedelta
from sleekxmpp.plugins import BasePlugin, register_plugin
from sleekxmpp import Iq
from sleekxmpp.exceptions import XMPPError
from sleekxmpp.xmlstream import JID, register_stanza_plugin
from sleekxmpp.xmlstream.handler import Callback
from sleekxmpp.xmlstream.matcher import StanzaPath
from sleekxmpp.plugins.xep_0012 import stanza, LastActivity
log = logging.getLogger(__name__)
class XEP_0012(BasePlugin):
"""
XEP-0012 Last Activity
"""
name = 'xep_0012'
description = 'XEP-0012: Last Activity'
dependencies = set(['xep_0030'])
stanza = stanza
def plugin_init(self):
register_stanza_plugin(Iq, LastActivity)
self._last_activities = {}
self.xmpp.register_handler(
Callback('Last Activity',
StanzaPath('iq@type=get/last_activity'),
self._handle_get_last_activity))
self.api.register(self._default_get_last_activity,
'get_last_activity',
default=True)
self.api.register(self._default_set_last_activity,
'set_last_activity',
default=True)
self.api.register(self._default_del_last_activity,
'del_last_activity',
default=True)
def plugin_end(self):
self.xmpp.remove_handler('Last Activity')
self.xmpp['xep_0030'].del_feature(feature='jabber:iq:last')
def session_bind(self, jid):
self.xmpp['xep_0030'].add_feature('jabber:iq:last')
def begin_idle(self, jid=None, status=None):
self.set_last_activity(jid, 0, status)
def end_idle(self, jid=None):
self.del_last_activity(jid)
def start_uptime(self, status=None):
self.set_last_activity(jid, 0, status)
def set_last_activity(self, jid=None, seconds=None, status=None):
self.api['set_last_activity'](jid, args={
'seconds': seconds,
'status': status})
def del_last_activity(self, jid):
self.api['del_last_activity'](jid)
def get_last_activity(self, jid, local=False, ifrom=None, block=True,
timeout=None, callback=None):
if jid is not None and not isinstance(jid, JID):
jid = JID(jid)
if self.xmpp.is_component:
if jid.domain == self.xmpp.boundjid.domain:
local = True
else:
if str(jid) == str(self.xmpp.boundjid):
local = True
jid = jid.full
if local or jid in (None, ''):
log.debug("Looking up local last activity data for %s", jid)
return self.api['get_last_activity'](jid, None, ifrom, None)
iq = self.xmpp.Iq()
iq['from'] = ifrom
iq['to'] = jid
iq['type'] = 'get'
iq.enable('last_activity')
return iq.send(timeout=timeout,
block=block,
callback=callback)
def _handle_get_last_activity(self, iq):
log.debug("Received last activity query from " + \
"<%s> to <%s>.", iq['from'], iq['to'])
reply = self.api['get_last_activity'](iq['to'], None, iq['from'], iq)
reply.send()
# =================================================================
# Default in-memory implementations for storing last activity data.
# =================================================================
def _default_set_last_activity(self, jid, node, ifrom, data):
seconds = data.get('seconds', None)
if seconds is None:
seconds = 0
status = data.get('status', None)
if status is None:
status = ''
self._last_activities[jid] = {
'seconds': datetime.now() - timedelta(seconds=seconds),
'status': status}
def _default_del_last_activity(self, jid, node, ifrom, data):
if jid in self._last_activities:
del self._last_activities[jid]
def _default_get_last_activity(self, jid, node, ifrom, iq):
if not isinstance(iq, Iq):
reply = self.xmpp.Iq()
else:
iq.reply()
reply = iq
if jid not in self._last_activities:
raise XMPPError('service-unavailable')
bare = JID(jid).bare
if bare != self.xmpp.boundjid.bare:
if bare in self.xmpp.roster[jid]:
sub = self.xmpp.roster[jid][bare]['subscription']
if sub not in ('from', 'both'):
raise XMPPError('forbidden')
td = datetime.now() - self._last_activities[jid]['seconds']
seconds = td.seconds + td.days * 24 * 3600
status = self._last_activities[jid]['status']
reply['last_activity']['seconds'] = seconds
reply['last_activity']['status'] = status
return reply

View File

@@ -0,0 +1,32 @@
"""
SleekXMPP: The Sleek XMPP Library
Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
This file is part of SleekXMPP.
See the file LICENSE for copying permission.
"""
from sleekxmpp.xmlstream import ElementBase
class 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 = ''

View File

@@ -0,0 +1,15 @@
"""
SleekXMPP: The Sleek XMPP Library
Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
This file is part of SleekXMPP.
See the file LICENSE for copying permission.
"""
from sleekxmpp.plugins.base import register_plugin
from sleekxmpp.plugins.xep_0027.stanza import Signed, Encrypted
from sleekxmpp.plugins.xep_0027.gpg import XEP_0027
register_plugin(XEP_0027)

View File

@@ -0,0 +1,170 @@
"""
SleekXMPP: The Sleek XMPP Library
Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
This file is part of SleekXMPP.
See the file LICENSE for copying permission.
"""
from sleekxmpp.thirdparty import GPG
from sleekxmpp.stanza import Presence, Message
from sleekxmpp.plugins.base import BasePlugin, register_plugin
from sleekxmpp.xmlstream import ElementBase, register_stanza_plugin
from sleekxmpp.xmlstream.handler import Callback
from sleekxmpp.xmlstream.matcher import StanzaPath
from sleekxmpp.plugins.xep_0027 import stanza, Signed, Encrypted
def _extract_data(data, kind):
stripped = []
begin_headers = False
begin_data = False
for line in data.split('\n'):
if not begin_headers and 'BEGIN PGP %s' % kind in line:
begin_headers = True
continue
if begin_headers and line.stripped() == '':
begin_data = True
continue
if 'END PGP %s' % kind in line:
return '\n'.join(stripped)
if begin_data:
stripped.append(line)
return ''
class XEP_0027(BasePlugin):
name = 'xep_0027'
description = 'XEP-0027: Current Jabber OpenPGP Usage'
dependencies = set()
stanza = stanza
default_config = {
'gpg_binary': 'gpg',
'gpg_home': '',
'use_agent': True,
'keyring': None,
'key_server': 'pgp.mit.edu'
}
def plugin_init(self):
self.gpg = GPG(gnupghome=self.gpg_home,
gpgbinary=self.gpg_binary,
use_agent=self.use_agent,
keyring=self.keyring)
self.xmpp.add_filter('out', self._sign_presence)
self._keyids = {}
self.api.register(self._set_keyid, 'set_keyid', default=True)
self.api.register(self._get_keyid, 'get_keyid', default=True)
self.api.register(self._del_keyid, 'del_keyid', default=True)
self.api.register(self._get_keyids, 'get_keyids', default=True)
register_stanza_plugin(Presence, Signed)
register_stanza_plugin(Message, Encrypted)
self.xmpp.add_event_handler('unverified_signed_presence',
self._handle_unverified_signed_presence,
threaded=True)
self.xmpp.register_handler(
Callback('Signed Presence',
StanzaPath('presence/signed'),
self._handle_signed_presence))
self.xmpp.register_handler(
Callback('Encrypted Message',
StanzaPath('message/encrypted'),
self._handle_encrypted_message))
def plugin_end(self):
self.xmpp.remove_handler('Encrypted Message')
self.xmpp.remove_handler('Signed Presence')
self.xmpp.del_filter('out', self._sign_presence)
self.xmpp.del_event_handler('unverified_signed_presence',
self._handle_unverified_signed_presence)
def _sign_presence(self, stanza):
if isinstance(stanza, Presence):
if stanza['type'] == 'available' or \
stanza['type'] in Presence.showtypes:
stanza['signed'] = stanza['status']
return stanza
def sign(self, data, jid=None):
keyid = self.get_keyid(jid)
if keyid:
signed = self.gpg.sign(data, keyid=keyid)
return _extract_data(signed.data, 'SIGNATURE')
def encrypt(self, data, jid=None):
keyid = self.get_keyid(jid)
if keyid:
enc = self.gpg.encrypt(data, keyid)
return _extract_data(enc.data, 'MESSAGE')
def decrypt(self, data, jid=None):
template = '-----BEGIN PGP MESSAGE-----\n' + \
'\n' + \
'%s\n' + \
'-----END PGP MESSAGE-----\n'
dec = self.gpg.decrypt(template % data)
return dec.data
def verify(self, data, sig, jid=None):
template = '-----BEGIN PGP SIGNED MESSAGE-----\n' + \
'Hash: SHA1\n' + \
'\n' + \
'%s\n' + \
'-----BEGIN PGP SIGNATURE-----\n' + \
'\n' + \
'%s\n' + \
'-----END PGP SIGNATURE-----\n'
v = self.gpg.verify(template % (data, sig))
return v
def set_keyid(self, jid=None, keyid=None):
self.api['set_keyid'](jid, args=keyid)
def get_keyid(self, jid=None):
return self.api['get_keyid'](jid)
def del_keyid(self, jid=None):
self.api['del_keyid'](jid)
def get_keyids(self):
return self.api['get_keyids']()
def _handle_signed_presence(self, pres):
self.xmpp.event('unverified_signed_presence', pres)
def _handle_unverified_signed_presence(self, pres):
verified = self.verify(pres['status'], pres['signed'])
if verified.key_id:
if not self.get_keyid(pres['from']):
known_keyids = [e['keyid'] for e in self.gpg.list_keys()]
if verified.key_id not in known_keyids:
self.gpg.recv_keys(self.key_server, verified.key_id)
self.set_keyid(jid=pres['from'], keyid=verified.key_id)
self.xmpp.event('signed_presence', pres)
def _handle_encrypted_message(self, msg):
self.xmpp.event('encrypted_message', msg)
# =================================================================
def _set_keyid(self, jid, node, ifrom, keyid):
self._keyids[jid] = keyid
def _get_keyid(self, jid, node, ifrom, keyid):
return self._keyids.get(jid, None)
def _del_keyid(self, jid, node, ifrom, keyid):
if jid in self._keyids:
del self._keyids[jid]
def _get_keyids(self, jid, node, ifrom, data):
return self._keyids

View File

@@ -0,0 +1,53 @@
"""
SleekXMPP: The Sleek XMPP Library
Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
This file is part of SleekXMPP.
See the file LICENSE for copying permission.
"""
from sleekxmpp.xmlstream import ElementBase
class Signed(ElementBase):
name = 'x'
namespace = 'jabber:x:signed'
plugin_attrib = 'signed'
interfaces = set(['signed'])
is_extension = True
def set_signed(self, value):
parent = self.parent()
xmpp = parent.stream
data = xmpp['xep_0027'].sign(value, parent['from'])
if data:
self.xml.text = data
else:
del parent['signed']
def get_signed(self):
return self.xml.text
class Encrypted(ElementBase):
name = 'x'
namespace = 'jabber:x:encrypted'
plugin_attrib = 'encrypted'
interfaces = set(['encrypted'])
is_extension = True
def set_encrypted(self, value):
parent = self.parent()
xmpp = parent.stream
data = xmpp['xep_0027'].encrypt(value, parent['to'].bare)
if data:
self.xml.text = data
else:
del parent['encrypted']
def get_encrypted(self):
parent = self.parent()
xmpp = parent.stream
if self.xml.text:
return xmpp['xep_0027'].decrypt(self.xml.text, parent['to'])
return None

View File

@@ -6,7 +6,18 @@
See the file LICENSE for copying permission.
"""
from sleekxmpp.plugins.base import register_plugin
from sleekxmpp.plugins.xep_0030 import stanza
from sleekxmpp.plugins.xep_0030.stanza import DiscoInfo, DiscoItems
from sleekxmpp.plugins.xep_0030.static import StaticDisco
from sleekxmpp.plugins.xep_0030.disco import xep_0030
from sleekxmpp.plugins.xep_0030.disco import XEP_0030
register_plugin(XEP_0030)
# Retain some backwards compatibility
xep_0030 = XEP_0030
XEP_0030.getInfo = XEP_0030.get_info
XEP_0030.getItems = XEP_0030.get_items
XEP_0030.make_static = XEP_0030.restore_defaults

View File

@@ -8,20 +8,19 @@
import logging
import sleekxmpp
from sleekxmpp import Iq
from sleekxmpp.exceptions import XMPPError
from sleekxmpp.plugins.base import base_plugin
from sleekxmpp.plugins import BasePlugin
from sleekxmpp.xmlstream.handler import Callback
from sleekxmpp.xmlstream.matcher import StanzaPath
from sleekxmpp.xmlstream import register_stanza_plugin, ElementBase, ET, JID
from sleekxmpp.plugins.xep_0030 import DiscoInfo, DiscoItems, StaticDisco
from sleekxmpp.xmlstream import register_stanza_plugin, JID
from sleekxmpp.plugins.xep_0030 import stanza, DiscoInfo, DiscoItems
from sleekxmpp.plugins.xep_0030 import StaticDisco
log = logging.getLogger(__name__)
class xep_0030(base_plugin):
class XEP_0030(BasePlugin):
"""
XEP-0030: Service Discovery
@@ -85,14 +84,19 @@ class xep_0030(base_plugin):
add_item --
"""
name = 'xep_0030'
description = 'XEP-0030: Service Discovery'
dependencies = set()
stanza = stanza
default_config = {
'use_cache': True,
'wrap_results': False
}
def plugin_init(self):
"""
Start the XEP-0030 plugin.
"""
self.xep = '0030'
self.description = 'Service Discovery'
self.stanza = sleekxmpp.plugins.xep_0030.stanza
self.xmpp.register_handler(
Callback('Disco Info',
StanzaPath('iq/disco_info'),
@@ -106,30 +110,21 @@ class xep_0030(base_plugin):
register_stanza_plugin(Iq, DiscoInfo)
register_stanza_plugin(Iq, DiscoItems)
self.static = StaticDisco(self.xmpp)
self.static = StaticDisco(self.xmpp, self)
self._disco_ops = [
'get_info', 'set_info', 'set_identities', 'set_features',
'get_items', 'set_items', 'del_items', 'add_identity',
'del_identity', 'add_feature', 'del_feature', 'add_item',
'del_item', 'del_identities', 'del_features', 'cache_info',
'get_cached_info', 'supports', 'has_identity']
self._disco_ops = ['get_info', 'set_identities', 'set_features',
'get_items', 'set_items', 'del_items',
'add_identity', 'del_identity', 'add_feature',
'del_feature', 'add_item', 'del_item',
'del_identities', 'del_features']
self.default_handlers = {}
self._handlers = {}
for op in self._disco_ops:
self._add_disco_op(op, getattr(self.static, op))
def post_init(self):
"""Handle cross-plugin dependencies."""
base_plugin.post_init(self)
if 'xep_0059' in self.xmpp.plugin:
register_stanza_plugin(DiscoItems,
self.xmpp['xep_0059'].stanza.Set)
self.api.register(getattr(self.static, op), op, default=True)
def _add_disco_op(self, op, default_handler):
self.default_handlers[op] = default_handler
self._handlers[op] = {'global': default_handler,
'jid': {},
'node': {}}
self.api.register(default_handler, op)
self.api.register_default(default_handler, op)
def set_node_handler(self, htype, jid=None, node=None, handler=None):
"""
@@ -175,20 +170,7 @@ class xep_0030(base_plugin):
assumed.
handler -- The handler function to use.
"""
if htype not in self._disco_ops:
return
if jid is None and node is None:
self._handlers[htype]['global'] = handler
elif node is None:
self._handlers[htype]['jid'][jid] = handler
elif jid is None:
if self.xmpp.is_component:
jid = self.xmpp.boundjid.full
else:
jid = self.xmpp.boundjid.bare
self._handlers[htype]['node'][(jid, node)] = handler
else:
self._handlers[htype]['node'][(jid, node)] = handler
self.api.register(handler, htype, jid, node)
def del_node_handler(self, htype, jid, node):
"""
@@ -211,7 +193,7 @@ class xep_0030(base_plugin):
jid -- The JID from which to remove the handler.
node -- The node from which to remove the handler.
"""
self.set_node_handler(htype, jid, node, None)
self.api.unregister(htype, jid, node)
def restore_defaults(self, jid=None, node=None, handlers=None):
"""
@@ -234,10 +216,80 @@ class xep_0030(base_plugin):
if handlers is None:
handlers = self._disco_ops
for op in handlers:
self.del_node_handler(op, jid, node)
self.set_node_handler(op, jid, node, self.default_handlers[op])
self.api.restore_default(op, jid, node)
def get_info(self, jid=None, node=None, local=False, **kwargs):
def supports(self, jid=None, node=None, feature=None, local=False,
cached=True, ifrom=None):
"""
Check if a JID supports a given feature.
Return values:
True -- The feature is supported
False -- The feature is not listed as supported
None -- Nothing could be found due to a timeout
Arguments:
jid -- Request info from this JID.
node -- The particular node to query.
feature -- The name of the feature to check.
local -- If true, then the query is for a JID/node
combination handled by this Sleek instance and
no stanzas need to be sent.
Otherwise, a disco stanza must be sent to the
remove JID to retrieve the info.
cached -- If true, then look for the disco info data from
the local cache system. If no results are found,
send the query as usual. The self.use_cache
setting must be set to true for this option to
be useful. If set to false, then the cache will
be skipped, even if a result has already been
cached. Defaults to false.
ifrom -- Specifiy the sender's JID.
"""
data = {'feature': feature,
'local': local,
'cached': cached}
return self.api['supports'](jid, node, ifrom, data)
def has_identity(self, jid=None, node=None, category=None, itype=None,
lang=None, local=False, cached=True, ifrom=None):
"""
Check if a JID provides a given identity.
Return values:
True -- The identity is provided
False -- The identity is not listed
None -- Nothing could be found due to a timeout
Arguments:
jid -- Request info from this JID.
node -- The particular node to query.
category -- The category of the identity to check.
itype -- The type of the identity to check.
lang -- The language of the identity to check.
local -- If true, then the query is for a JID/node
combination handled by this Sleek instance and
no stanzas need to be sent.
Otherwise, a disco stanza must be sent to the
remove JID to retrieve the info.
cached -- If true, then look for the disco info data from
the local cache system. If no results are found,
send the query as usual. The self.use_cache
setting must be set to true for this option to
be useful. If set to false, then the cache will
be skipped, even if a result has already been
cached. Defaults to false.
ifrom -- Specifiy the sender's JID.
"""
data = {'category': category,
'itype': itype,
'lang': lang,
'local': local,
'cached': cached}
return self.api['has_identity'](jid, node, ifrom, data)
def get_info(self, jid=None, node=None, local=False,
cached=None, **kwargs):
"""
Retrieve the disco#info results from a given JID/node combination.
@@ -257,6 +309,13 @@ class xep_0030(base_plugin):
no stanzas need to be sent.
Otherwise, a disco stanza must be sent to the
remove JID to retrieve the info.
cached -- If true, then look for the disco info data from
the local cache system. If no results are found,
send the query as usual. The self.use_cache
setting must be set to true for this option to
be useful. If set to false, then the cache will
be skipped, even if a result has already been
cached. Defaults to false.
ifrom -- Specifiy the sender's JID.
block -- If true, block and wait for the stanzas' reply.
timeout -- The time in seconds to block while waiting for
@@ -266,11 +325,35 @@ class xep_0030(base_plugin):
received instead of blocking and waiting for
the reply.
"""
if local or jid is None:
if jid is not None and not isinstance(jid, JID):
jid = JID(jid)
if self.xmpp.is_component:
if jid.domain == self.xmpp.boundjid.domain:
local = True
else:
if str(jid) == str(self.xmpp.boundjid):
local = True
jid = jid.full
elif jid in (None, ''):
local = True
if local:
log.debug("Looking up local disco#info data " + \
"for %s, node %s.", jid, node)
info = self._run_node_handler('get_info', jid, node, kwargs)
return self._fix_default_info(info)
info = self.api['get_info'](jid, node,
kwargs.get('ifrom', None),
kwargs)
info = self._fix_default_info(info)
return self._wrap(kwargs.get('ifrom', None), jid, info)
if cached:
log.debug("Looking up cached disco#info data " + \
"for %s, node %s.", jid, node)
info = self.api['get_cached_info'](jid, node,
kwargs.get('ifrom', None),
kwargs)
if info is not None:
return self._wrap(kwargs.get('ifrom', None), jid, info)
iq = self.xmpp.Iq()
# Check dfrom parameter for backwards compatibility
@@ -282,6 +365,15 @@ class xep_0030(base_plugin):
block=kwargs.get('block', True),
callback=kwargs.get('callback', None))
def set_info(self, jid=None, node=None, info=None):
"""
Set the disco#info data for a JID/node based on an existing
disco#info stanza.
"""
if isinstance(info, Iq):
info = info['disco_info']
self.api['set_info'](jid, node, None, info)
def get_items(self, jid=None, node=None, local=False, **kwargs):
"""
Retrieve the disco#items results from a given JID/node combination.
@@ -314,7 +406,10 @@ class xep_0030(base_plugin):
Otherwise the parameter is ignored.
"""
if local or jid is None:
return self._run_node_handler('get_items', jid, node, kwargs)
items = self.api['get_items'](jid, node,
kwargs.get('ifrom', None),
kwargs)
return self._wrap(kwargs.get('ifrom', None), jid, items)
iq = self.xmpp.Iq()
# Check dfrom parameter for backwards compatibility
@@ -341,7 +436,7 @@ class xep_0030(base_plugin):
node -- Optional node to modify.
items -- A series of items in tuple format.
"""
self._run_node_handler('set_items', jid, node, kwargs)
self.api['set_items'](jid, node, None, kwargs)
def del_items(self, jid=None, node=None, **kwargs):
"""
@@ -351,7 +446,7 @@ class xep_0030(base_plugin):
jid -- The JID to modify.
node -- Optional node to modify.
"""
self._run_node_handler('del_items', jid, node, kwargs)
self.api['del_items'](jid, node, None, kwargs)
def add_item(self, jid='', name='', node=None, subnode='', ijid=None):
"""
@@ -372,7 +467,7 @@ class xep_0030(base_plugin):
kwargs = {'ijid': jid,
'name': name,
'inode': subnode}
self._run_node_handler('add_item', ijid, node, kwargs)
self.api['add_item'](ijid, node, None, kwargs)
def del_item(self, jid=None, node=None, **kwargs):
"""
@@ -384,7 +479,7 @@ class xep_0030(base_plugin):
ijid -- The item's JID.
inode -- The item's node.
"""
self._run_node_handler('del_item', jid, node, kwargs)
self.api['del_item'](jid, node, None, kwargs)
def add_identity(self, category='', itype='', name='',
node=None, jid=None, lang=None):
@@ -411,7 +506,7 @@ class xep_0030(base_plugin):
'itype': itype,
'name': name,
'lang': lang}
self._run_node_handler('add_identity', jid, node, kwargs)
self.api['add_identity'](jid, node, None, kwargs)
def add_feature(self, feature, node=None, jid=None):
"""
@@ -423,7 +518,7 @@ class xep_0030(base_plugin):
jid -- The JID to modify.
"""
kwargs = {'feature': feature}
self._run_node_handler('add_feature', jid, node, kwargs)
self.api['add_feature'](jid, node, None, kwargs)
def del_identity(self, jid=None, node=None, **kwargs):
"""
@@ -437,7 +532,7 @@ class xep_0030(base_plugin):
name -- Optional, human readable name for the identity.
lang -- Optional, the identity's xml:lang value.
"""
self._run_node_handler('del_identity', jid, node, kwargs)
self.api['del_identity'](jid, node, None, kwargs)
def del_feature(self, jid=None, node=None, **kwargs):
"""
@@ -448,7 +543,7 @@ class xep_0030(base_plugin):
node -- The node to modify.
feature -- The feature's namespace.
"""
self._run_node_handler('del_feature', jid, node, kwargs)
self.api['del_feature'](jid, node, None, kwargs)
def set_identities(self, jid=None, node=None, **kwargs):
"""
@@ -463,7 +558,7 @@ class xep_0030(base_plugin):
identities -- A set of identities in tuple form.
lang -- Optional, xml:lang value.
"""
self._run_node_handler('set_identities', jid, node, kwargs)
self.api['set_identities'](jid, node, None, kwargs)
def del_identities(self, jid=None, node=None, **kwargs):
"""
@@ -478,7 +573,7 @@ class xep_0030(base_plugin):
lang -- Optional. If given, only remove identities
using this xml:lang value.
"""
self._run_node_handler('del_identities', jid, node, kwargs)
self.api['del_identities'](jid, node, None, kwargs)
def set_features(self, jid=None, node=None, **kwargs):
"""
@@ -490,7 +585,7 @@ class xep_0030(base_plugin):
node -- The node to modify.
features -- The new set of supported features.
"""
self._run_node_handler('set_features', jid, node, kwargs)
self.api['set_features'](jid, node, None, kwargs)
def del_features(self, jid=None, node=None, **kwargs):
"""
@@ -500,9 +595,9 @@ class xep_0030(base_plugin):
jid -- The JID to modify.
node -- The node to modify.
"""
self._run_node_handler('del_features', jid, node, kwargs)
self.api['del_features'](jid, node, None, kwargs)
def _run_node_handler(self, htype, jid, node, data={}):
def _run_node_handler(self, htype, jid, node=None, ifrom=None, data={}):
"""
Execute the most specific node handler for the given
JID/node combination.
@@ -513,22 +608,7 @@ class xep_0030(base_plugin):
node -- The node requested.
data -- Optional, custom data to pass to the handler.
"""
if jid is None:
if self.xmpp.is_component:
jid = self.xmpp.boundjid.full
else:
jid = self.xmpp.boundjid.bare
if node is None:
node = ''
if self._handlers[htype]['node'].get((jid, node), False):
return self._handlers[htype]['node'][(jid, node)](jid, node, data)
elif self._handlers[htype]['jid'].get(jid, False):
return self._handlers[htype]['jid'][jid](jid, node, data)
elif self._handlers[htype]['global']:
return self._handlers[htype]['global'](jid, node, data)
else:
return None
return self.api[htype](jid, node, ifrom, data)
def _handle_disco_info(self, iq):
"""
@@ -543,15 +623,12 @@ class xep_0030(base_plugin):
if iq['type'] == 'get':
log.debug("Received disco info query from " + \
"<%s> to <%s>.", iq['from'], iq['to'])
if self.xmpp.is_component:
jid = iq['to'].full
else:
jid = iq['to'].bare
info = self._run_node_handler('get_info',
jid,
iq['disco_info']['node'],
iq)
info = self.api['get_info'](iq['to'],
iq['disco_info']['node'],
iq['from'],
iq)
if isinstance(info, Iq):
info['id'] = iq['id']
info.send()
else:
iq.reply()
@@ -560,8 +637,19 @@ class xep_0030(base_plugin):
iq.set_payload(info.xml)
iq.send()
elif iq['type'] == 'result':
log.debug("Received disco info result from" + \
"%s to %s.", iq['from'], iq['to'])
log.debug("Received disco info result from " + \
"<%s> to <%s>.", iq['from'], iq['to'])
if self.use_cache:
log.debug("Caching disco info result from " \
"<%s> to <%s>.", iq['from'], iq['to'])
if self.xmpp.is_component:
ito = iq['to'].full
else:
ito = None
self.api['cache_info'](iq['from'],
iq['disco_info']['node'],
ito,
iq)
self.xmpp.event('disco_info', iq)
def _handle_disco_items(self, iq):
@@ -576,13 +664,9 @@ class xep_0030(base_plugin):
if iq['type'] == 'get':
log.debug("Received disco items query from " + \
"<%s> to <%s>.", iq['from'], iq['to'])
if self.xmpp.is_component:
jid = iq['to'].full
else:
jid = iq['to'].bare
items = self._run_node_handler('get_items',
jid,
items = self.api['get_items'](iq['to'],
iq['disco_items']['node'],
iq['from'],
iq)
if isinstance(items, Iq):
items.send()
@@ -592,7 +676,7 @@ class xep_0030(base_plugin):
iq.set_payload(items.xml)
iq.send()
elif iq['type'] == 'result':
log.debug("Received disco items result from" + \
log.debug("Received disco items result from " + \
"%s to %s.", iq['from'], iq['to'])
self.xmpp.event('disco_items', iq)
@@ -607,24 +691,43 @@ class xep_0030(base_plugin):
Arguments:
info -- The disco#info quest (not the full Iq stanza) to modify.
"""
result = info
if isinstance(info, Iq):
info = info['disco_info']
if not info['node']:
if not info['identities']:
if self.xmpp.is_component:
log.debug("No identity found for this entity." + \
log.debug("No identity found for this entity. " + \
"Using default component identity.")
info.add_identity('component', 'generic')
else:
log.debug("No identity found for this entity." + \
log.debug("No identity found for this entity. " + \
"Using default client identity.")
info.add_identity('client', 'bot')
if not info['features']:
log.debug("No features found for this entity." + \
log.debug("No features found for this entity. " + \
"Using default disco#info feature.")
info.add_feature(info.namespace)
return info
return result
def _wrap(self, ito, ifrom, payload, force=False):
"""
Ensure that results are wrapped in an Iq stanza
if self.wrap_results has been set to True.
# Retain some backwards compatibility
xep_0030.getInfo = xep_0030.get_info
xep_0030.getItems = xep_0030.get_items
xep_0030.make_static = xep_0030.restore_defaults
Arguments:
ito -- The JID to use as the 'to' value
ifrom -- The JID to use as the 'from' value
payload -- The disco data to wrap
force -- Force wrapping, regardless of self.wrap_results
"""
if (force or self.wrap_results) and not isinstance(payload, Iq):
iq = self.xmpp.Iq()
# Since we're simulating a result, we have to treat
# the 'from' and 'to' values opposite the normal way.
iq['to'] = self.xmpp.boundjid if ito is None else ito
iq['from'] = self.xmpp.boundjid if ifrom is None else ifrom
iq['type'] = 'result'
iq.append(payload)
return iq
return payload

View File

@@ -146,7 +146,7 @@ class DiscoInfo(ElementBase):
return True
return False
def get_identities(self, lang=None):
def get_identities(self, lang=None, dedupe=True):
"""
Return a set of all identities in tuple form as so:
(category, type, lang, name)
@@ -155,17 +155,25 @@ class DiscoInfo(ElementBase):
that language.
Arguments:
lang -- Optional, standard xml:lang value.
lang -- Optional, standard xml:lang value.
dedupe -- If True, de-duplicate identities, otherwise
return a list of all identities.
"""
identities = set()
if dedupe:
identities = set()
else:
identities = []
for id_xml in self.findall('{%s}identity' % self.namespace):
xml_lang = id_xml.attrib.get('{%s}lang' % self.xml_ns, None)
if lang is None or xml_lang == lang:
identities.add((
id_xml.attrib['category'],
id_xml.attrib['type'],
id_xml.attrib.get('{%s}lang' % self.xml_ns, None),
id_xml.attrib.get('name', None)))
id = (id_xml.attrib['category'],
id_xml.attrib['type'],
id_xml.attrib.get('{%s}lang' % self.xml_ns, None),
id_xml.attrib.get('name', None))
if dedupe:
identities.add(id)
else:
identities.append(id)
return identities
def set_identities(self, identities, lang=None):
@@ -237,11 +245,17 @@ class DiscoInfo(ElementBase):
return True
return False
def get_features(self):
def get_features(self, dedupe=True):
"""Return the set of all supported features."""
features = set()
if dedupe:
features = set()
else:
features = []
for feature_xml in self.findall('{%s}feature' % self.namespace):
features.add(feature_xml.attrib['var'])
if dedupe:
features.add(feature_xml.attrib['var'])
else:
features.append(feature_xml.attrib['var'])
return features
def set_features(self, features):

View File

@@ -6,7 +6,7 @@
See the file LICENSE for copying permission.
"""
from sleekxmpp.xmlstream import ElementBase, ET
from sleekxmpp.xmlstream import ElementBase, register_stanza_plugin
class DiscoItems(ElementBase):
@@ -78,13 +78,11 @@ class DiscoItems(ElementBase):
"""
if (jid, node) not in self._items:
self._items.add((jid, node))
item_xml = ET.Element('{%s}item' % self.namespace)
item_xml.attrib['jid'] = jid
if name:
item_xml.attrib['name'] = name
if node:
item_xml.attrib['node'] = node
self.xml.append(item_xml)
item = DiscoItem(parent=self)
item['jid'] = jid
item['node'] = node
item['name'] = name
self.iterables.append(item)
return True
return False
@@ -108,11 +106,9 @@ class DiscoItems(ElementBase):
def get_items(self):
"""Return all items."""
items = set()
for item_xml in self.findall('{%s}item' % self.namespace):
item = (item_xml.attrib['jid'],
item_xml.attrib.get('node'),
item_xml.attrib.get('name'))
items.add(item)
for item in self['substanzas']:
if isinstance(item, DiscoItem):
items.add((item['jid'], item['node'], item['name']))
return items
def set_items(self, items):
@@ -132,5 +128,24 @@ class DiscoItems(ElementBase):
def del_items(self):
"""Remove all items."""
self._items = set()
for item_xml in self.findall('{%s}item' % self.namespace):
self.xml.remove(item_xml)
for item in self['substanzas']:
if isinstance(item, DiscoItem):
self.xml.remove(item.xml)
class DiscoItem(ElementBase):
name = 'item'
namespace = 'http://jabber.org/protocol/disco#items'
plugin_attrib = name
interfaces = set(('jid', 'node', 'name'))
def get_node(self):
"""Return the item's node name or ``None``."""
return self._get_attr('node', None)
def get_name(self):
"""Return the item's human readable name, or ``None``."""
return self._get_attr('name', None)
register_stanza_plugin(DiscoItems, DiscoItem, iterable=True)

View File

@@ -7,14 +7,11 @@
"""
import logging
import threading
import sleekxmpp
from sleekxmpp import Iq
from sleekxmpp.exceptions import XMPPError
from sleekxmpp.plugins.base import base_plugin
from sleekxmpp.xmlstream.handler import Callback
from sleekxmpp.xmlstream.matcher import StanzaPath
from sleekxmpp.xmlstream import register_stanza_plugin, ElementBase, ET, JID
from sleekxmpp.exceptions import XMPPError, IqError, IqTimeout
from sleekxmpp.xmlstream import JID
from sleekxmpp.plugins.xep_0030 import DiscoInfo, DiscoItems
@@ -38,7 +35,7 @@ class StaticDisco(object):
xmpp -- The main SleekXMPP object.
"""
def __init__(self, xmpp):
def __init__(self, xmpp, disco):
"""
Create a static disco interface. Sets of disco#info and
disco#items are maintained for every given JID and node
@@ -50,8 +47,10 @@ class StaticDisco(object):
"""
self.nodes = {}
self.xmpp = xmpp
self.disco = disco
self.lock = threading.RLock()
def add_node(self, jid=None, node=None):
def add_node(self, jid=None, node=None, ifrom=None):
"""
Create a new set of stanzas for the provided
JID and node combination.
@@ -60,83 +59,213 @@ class StaticDisco(object):
jid -- The JID that will own the new stanzas.
node -- The node that will own the new stanzas.
"""
if jid is None:
jid = self.xmpp.boundjid.full
if node is None:
node = ''
if (jid, node) not in self.nodes:
self.nodes[(jid, node)] = {'info': DiscoInfo(),
'items': DiscoItems()}
self.nodes[(jid, node)]['info']['node'] = node
self.nodes[(jid, node)]['items']['node'] = node
with self.lock:
if jid is None:
jid = self.xmpp.boundjid.full
if node is None:
node = ''
if ifrom is None:
ifrom = ''
if isinstance(ifrom, JID):
ifrom = ifrom.full
if (jid, node, ifrom) not in self.nodes:
self.nodes[(jid, node, ifrom)] = {'info': DiscoInfo(),
'items': DiscoItems()}
self.nodes[(jid, node, ifrom)]['info']['node'] = node
self.nodes[(jid, node, ifrom)]['items']['node'] = node
def get_node(self, jid=None, node=None, ifrom=None):
with self.lock:
if jid is None:
jid = self.xmpp.boundjid.full
if node is None:
node = ''
if ifrom is None:
ifrom = ''
if isinstance(ifrom, JID):
ifrom = ifrom.full
if (jid, node, ifrom) not in self.nodes:
self.add_node(jid, node, ifrom)
return self.nodes[(jid, node, ifrom)]
def node_exists(self, jid=None, node=None, ifrom=None):
with self.lock:
if jid is None:
jid = self.xmpp.boundjid.full
if node is None:
node = ''
if ifrom is None:
ifrom = ''
if isinstance(ifrom, JID):
ifrom = ifrom.full
if (jid, node, ifrom) not in self.nodes:
return False
return True
# =================================================================
# Node Handlers
#
# Each handler accepts three arguments: jid, node, and data.
# The jid and node parameters together determine the set of
# info and items stanzas that will be retrieved or added.
# The data parameter is a dictionary with additional paramters
# that will be passed to other calls.
# Each handler accepts four arguments: jid, node, ifrom, and data.
# The jid and node parameters together determine the set of info
# and items stanzas that will be retrieved or added. Additionally,
# the ifrom value allows for cached results when results vary based
# on the requester's JID. The data parameter is a dictionary with
# additional parameters that will be passed to other calls.
#
# This implementation does not allow different responses based on
# the requester's JID, except for cached results. To do that,
# register a custom node handler.
def get_info(self, jid, node, data):
def supports(self, jid, node, ifrom, data):
"""
Check if a JID supports a given feature.
The data parameter may provide:
feature -- The feature to check for support.
local -- If true, then the query is for a JID/node
combination handled by this Sleek instance and
no stanzas need to be sent.
Otherwise, a disco stanza must be sent to the
remove JID to retrieve the info.
cached -- If true, then look for the disco info data from
the local cache system. If no results are found,
send the query as usual. The self.use_cache
setting must be set to true for this option to
be useful. If set to false, then the cache will
be skipped, even if a result has already been
cached. Defaults to false.
"""
feature = data.get('feature', None)
data = {'local': data.get('local', False),
'cached': data.get('cached', True)}
if not feature:
return False
try:
info = self.disco.get_info(jid=jid, node=node,
ifrom=ifrom, **data)
info = self.disco._wrap(ifrom, jid, info, True)
features = info['disco_info']['features']
return feature in features
except IqError:
return False
except IqTimeout:
return None
def has_identity(self, jid, node, ifrom, data):
"""
Check if a JID has a given identity.
The data parameter may provide:
category -- The category of the identity to check.
itype -- The type of the identity to check.
lang -- The language of the identity to check.
local -- If true, then the query is for a JID/node
combination handled by this Sleek instance and
no stanzas need to be sent.
Otherwise, a disco stanza must be sent to the
remove JID to retrieve the info.
cached -- If true, then look for the disco info data from
the local cache system. If no results are found,
send the query as usual. The self.use_cache
setting must be set to true for this option to
be useful. If set to false, then the cache will
be skipped, even if a result has already been
cached. Defaults to false.
"""
identity = (data.get('category', None),
data.get('itype', None),
data.get('lang', None))
data = {'local': data.get('local', False),
'cached': data.get('cached', True)}
try:
info = self.disco.get_info(jid=jid, node=node,
ifrom=ifrom, **data)
info = self.disco._wrap(ifrom, jid, info, True)
trunc = lambda i: (i[0], i[1], i[2])
return identity in map(trunc, info['disco_info']['identities'])
except IqError:
return False
except IqTimeout:
return None
def get_info(self, jid, node, ifrom, data):
"""
Return the stored info data for the requested JID/node combination.
The data parameter is not used.
"""
if (jid, node) not in self.nodes:
if not node:
return DiscoInfo()
with self.lock:
if not self.node_exists(jid, node):
if not node:
return DiscoInfo()
else:
raise XMPPError(condition='item-not-found')
else:
raise XMPPError(condition='item-not-found')
else:
return self.nodes[(jid, node)]['info']
return self.get_node(jid, node)['info']
def del_info(self, jid, node, data):
def set_info(self, jid, node, ifrom, data):
"""
Set the entire info stanza for a JID/node at once.
The data parameter is a disco#info substanza.
"""
with self.lock:
self.add_node(jid, node)
self.get_node(jid, node)['info'] = data
def del_info(self, jid, node, ifrom, data):
"""
Reset the info stanza for a given JID/node combination.
The data parameter is not used.
"""
if (jid, node) in self.nodes:
self.nodes[(jid, node)]['info'] = DiscoInfo()
with self.lock:
if self.node_exists(jid, node):
self.get_node(jid, node)['info'] = DiscoInfo()
def get_items(self, jid, node, data):
def get_items(self, jid, node, ifrom, data):
"""
Return the stored items data for the requested JID/node combination.
The data parameter is not used.
"""
if (jid, node) not in self.nodes:
if not node:
return DiscoInfo()
with self.lock:
if not self.node_exists(jid, node):
if not node:
return DiscoItems()
else:
raise XMPPError(condition='item-not-found')
else:
raise XMPPError(condition='item-not-found')
else:
return self.nodes[(jid, node)]['items']
return self.get_node(jid, node)['items']
def set_items(self, jid, node, data):
def set_items(self, jid, node, ifrom, data):
"""
Replace the stored items data for a JID/node combination.
The data parameter may provided:
The data parameter may provide:
items -- A set of items in tuple format.
"""
items = data.get('items', set())
self.add_node(jid, node)
self.nodes[(jid, node)]['items']['items'] = items
with self.lock:
items = data.get('items', set())
self.add_node(jid, node)
self.get_node(jid, node)['items']['items'] = items
def del_items(self, jid, node, data):
def del_items(self, jid, node, ifrom, data):
"""
Reset the items stanza for a given JID/node combination.
The data parameter is not used.
"""
if (jid, node) in self.nodes:
self.nodes[(jid, node)]['items'] = DiscoItems()
with self.lock:
if self.node_exists(jid, node):
self.get_node(jid, node)['items'] = DiscoItems()
def add_identity(self, jid, node, data):
def add_identity(self, jid, node, ifrom, data):
"""
Add a new identity to te JID/node combination.
@@ -146,14 +275,15 @@ class StaticDisco(object):
name -- Optional human readable name for this identity.
lang -- Optional standard xml:lang value.
"""
self.add_node(jid, node)
self.nodes[(jid, node)]['info'].add_identity(
data.get('category', ''),
data.get('itype', ''),
data.get('name', None),
data.get('lang', None))
with self.lock:
self.add_node(jid, node)
self.get_node(jid, node)['info'].add_identity(
data.get('category', ''),
data.get('itype', ''),
data.get('name', None),
data.get('lang', None))
def set_identities(self, jid, node, data):
def set_identities(self, jid, node, ifrom, data):
"""
Add or replace all identities for a JID/node combination.
@@ -161,11 +291,12 @@ class StaticDisco(object):
identities -- A list of identities in tuple form:
(category, type, name, lang)
"""
identities = data.get('identities', set())
self.add_node(jid, node)
self.nodes[(jid, node)]['info']['identities'] = identities
with self.lock:
identities = data.get('identities', set())
self.add_node(jid, node)
self.get_node(jid, node)['info']['identities'] = identities
def del_identity(self, jid, node, data):
def del_identity(self, jid, node, ifrom, data):
"""
Remove an identity from a JID/node combination.
@@ -175,67 +306,72 @@ class StaticDisco(object):
name -- Optional human readable name for this identity.
lang -- Optional, standard xml:lang value.
"""
if (jid, node) not in self.nodes:
return
self.nodes[(jid, node)]['info'].del_identity(
data.get('category', ''),
data.get('itype', ''),
data.get('name', None),
data.get('lang', None))
with self.lock:
if self.node_exists(jid, node):
self.get_node(jid, node)['info'].del_identity(
data.get('category', ''),
data.get('itype', ''),
data.get('name', None),
data.get('lang', None))
def del_identities(self, jid, node, data):
def del_identities(self, jid, node, ifrom, data):
"""
Remove all identities from a JID/node combination.
The data parameter is not used.
"""
if (jid, node) not in self.nodes:
return
del self.nodes[(jid, node)]['info']['identities']
with self.lock:
if self.node_exists(jid, node):
del self.get_node(jid, node)['info']['identities']
def add_feature(self, jid, node, data):
def add_feature(self, jid, node, ifrom, data):
"""
Add a feature to a JID/node combination.
The data parameter should include:
feature -- The namespace of the supported feature.
"""
self.add_node(jid, node)
self.nodes[(jid, node)]['info'].add_feature(data.get('feature', ''))
with self.lock:
self.add_node(jid, node)
self.get_node(jid, node)['info'].add_feature(
data.get('feature', ''))
def set_features(self, jid, node, data):
def set_features(self, jid, node, ifrom, data):
"""
Add or replace all features for a JID/node combination.
The data parameter should include:
features -- The new set of supported features.
"""
features = data.get('features', set())
self.add_node(jid, node)
self.nodes[(jid, node)]['info']['features'] = features
with self.lock:
features = data.get('features', set())
self.add_node(jid, node)
self.get_node(jid, node)['info']['features'] = features
def del_feature(self, jid, node, data):
def del_feature(self, jid, node, ifrom, data):
"""
Remove a feature from a JID/node combination.
The data parameter should include:
feature -- The namespace of the removed feature.
"""
if (jid, node) not in self.nodes:
return
self.nodes[(jid, node)]['info'].del_feature(data.get('feature', ''))
with self.lock:
if self.node_exists(jid, node):
self.get_node(jid, node)['info'].del_feature(
data.get('feature', ''))
def del_features(self, jid, node, data):
def del_features(self, jid, node, ifrom, data):
"""
Remove all features from a JID/node combination.
The data parameter is not used.
"""
if (jid, node) not in self.nodes:
return
del self.nodes[(jid, node)]['info']['features']
with self.lock:
if not self.node_exists(jid, node):
return
del self.get_node(jid, node)['info']['features']
def add_item(self, jid, node, data):
def add_item(self, jid, node, ifrom, data):
"""
Add an item to a JID/node combination.
@@ -245,13 +381,14 @@ class StaticDisco(object):
non-addressable items.
name -- Optional human readable name for the item.
"""
self.add_node(jid, node)
self.nodes[(jid, node)]['items'].add_item(
data.get('ijid', ''),
node=data.get('inode', ''),
name=data.get('name', ''))
with self.lock:
self.add_node(jid, node)
self.get_node(jid, node)['items'].add_item(
data.get('ijid', ''),
node=data.get('inode', ''),
name=data.get('name', ''))
def del_item(self, jid, node, data):
def del_item(self, jid, node, ifrom, data):
"""
Remove an item from a JID/node combination.
@@ -259,7 +396,35 @@ class StaticDisco(object):
ijid -- JID of the item to remove.
inode -- Optional extra identifying information.
"""
if (jid, node) in self.nodes:
self.nodes[(jid, node)]['items'].del_item(
data.get('ijid', ''),
node=data.get('inode', None))
with self.lock:
if self.node_exists(jid, node):
self.get_node(jid, node)['items'].del_item(
data.get('ijid', ''),
node=data.get('inode', None))
def cache_info(self, jid, node, ifrom, data):
"""
Cache disco information for an external JID.
The data parameter is the Iq result stanza
containing the disco info to cache, or
the disco#info substanza itself.
"""
with self.lock:
if isinstance(data, Iq):
data = data['disco_info']
self.add_node(jid, node, ifrom)
self.get_node(jid, node, ifrom)['info'] = data
def get_cached_info(self, jid, node, ifrom, data):
"""
Retrieve cached disco info data.
The data parameter is not used.
"""
with self.lock:
if not self.node_exists(jid, node, ifrom):
return None
else:
return self.get_node(jid, node, ifrom)['info']

View File

@@ -1,161 +0,0 @@
"""
SleekXMPP: The Sleek XMPP Library
Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout
This file is part of SleekXMPP.
See the file LICENSE for copying permission.
"""
import logging
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

@@ -0,0 +1,20 @@
"""
SleekXMPP: The Sleek XMPP Library
Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
This file is part of SleekXMPP.
See the file LICENSE for copying permission.
"""
from sleekxmpp.plugins.base import register_plugin
from sleekxmpp.plugins.xep_0033 import stanza
from sleekxmpp.plugins.xep_0033.stanza import Addresses, Address
from sleekxmpp.plugins.xep_0033.addresses import XEP_0033
register_plugin(XEP_0033)
# Retain some backwards compatibility
xep_0033 = XEP_0033
Addresses.addAddress = Addresses.add_address

View File

@@ -0,0 +1,37 @@
"""
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 sleekxmpp import Message, Presence
from sleekxmpp.xmlstream import register_stanza_plugin
from sleekxmpp.plugins import BasePlugin
from sleekxmpp.plugins.xep_0033 import stanza, Addresses
class XEP_0033(BasePlugin):
"""
XEP-0033: Extended Stanza Addressing
"""
name = 'xep_0033'
description = 'XEP-0033: Extended Stanza Addressing'
dependencies = set(['xep_0030'])
stanza = stanza
def plugin_init(self):
register_stanza_plugin(Message, Addresses)
register_stanza_plugin(Presence, Addresses)
def plugin_end(self):
self.xmpp['xep_0030'].del_feature(feature=Addresses.namespace)
def session_bind(self, jid):
self.xmpp['xep_0030'].add_feature(Addresses.namespace)

View File

@@ -0,0 +1,131 @@
"""
SleekXMPP: The Sleek XMPP Library
Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
This file is part of SleekXMPP.
See the file LICENSE for copying permission.
"""
from sleekxmpp.xmlstream import JID, ElementBase, ET, register_stanza_plugin
class Addresses(ElementBase):
name = 'addresses'
namespace = 'http://jabber.org/protocol/address'
plugin_attrib = 'addresses'
interfaces = set()
def add_address(self, atype='to', jid='', node='', uri='',
desc='', delivered=False):
addr = Address(parent=self)
addr['type'] = atype
addr['jid'] = jid
addr['node'] = node
addr['uri'] = uri
addr['desc'] = desc
addr['delivered'] = delivered
return addr
# Additional methods for manipulating sets of addresses
# based on type are generated below.
class Address(ElementBase):
name = 'address'
namespace = 'http://jabber.org/protocol/address'
plugin_attrib = 'address'
interfaces = set(['type', 'jid', 'node', 'uri', 'desc', 'delivered'])
address_types = set(('bcc', 'cc', 'noreply', 'replyroom', 'replyto', 'to'))
def get_jid(self):
return JID(self._get_attr('jid'))
def set_jid(self, value):
self._set_attr('jid', str(value))
def get_delivered(self):
value = self._get_attr('delivered', False)
return value and value.lower() in ('true', '1')
def set_delivered(self, delivered):
if delivered:
self._set_attr('delivered', 'true')
else:
del self['delivered']
def set_uri(self, uri):
if uri:
del self['jid']
del self['node']
self._set_attr('uri', uri)
else:
self._del_attr('uri')
# =====================================================================
# Auto-generate address type filters for the Addresses class.
def _addr_filter(atype):
def _type_filter(addr):
if isinstance(addr, Address):
if atype == 'all' or addr['type'] == atype:
return True
return False
return _type_filter
def _build_methods(atype):
def get_multi(self):
return list(filter(_addr_filter(atype), self))
def set_multi(self, value):
del self[atype]
for addr in value:
# Support assigning dictionary versions of addresses
# instead of full Address objects.
if not isinstance(addr, Address):
if atype != 'all':
addr['type'] = atype
elif 'atype' in addr and 'type' not in addr:
addr['type'] = addr['atype']
addrObj = Address()
addrObj.values = addr
addr = addrObj
self.append(addr)
def del_multi(self):
res = list(filter(_addr_filter(atype), self))
for addr in res:
self.iterables.remove(addr)
self.xml.remove(addr.xml)
return get_multi, set_multi, del_multi
for atype in ('all', 'bcc', 'cc', 'noreply', 'replyroom', 'replyto', 'to'):
get_multi, set_multi, del_multi = _build_methods(atype)
Addresses.interfaces.add(atype)
setattr(Addresses, "get_%s" % atype, get_multi)
setattr(Addresses, "set_%s" % atype, set_multi)
setattr(Addresses, "del_%s" % atype, del_multi)
# To retain backwards compatibility:
setattr(Addresses, "get%s" % atype.title(), get_multi)
setattr(Addresses, "set%s" % atype.title(), set_multi)
setattr(Addresses, "del%s" % atype.title(), del_multi)
if atype == 'all':
Addresses.interfaces.add('addresses')
setattr(Addresses, "getAddresses", get_multi)
setattr(Addresses, "setAddresses", set_multi)
setattr(Addresses, "delAddresses", del_multi)
register_stanza_plugin(Addresses, Address, iterable=True)

View File

@@ -6,14 +6,15 @@
See the file LICENSE for copying permission.
"""
from __future__ import with_statement
from . import base
import logging
from xml.etree import cElementTree as ET
from .. xmlstream.stanzabase import registerStanzaPlugin, ElementBase, JID
from .. stanza.presence import Presence
from .. xmlstream.handler.callback import Callback
from .. xmlstream.matcher.xpath import MatchXPath
from .. xmlstream.matcher.xmlmask import MatchXMLMask
from sleekxmpp import Presence
from sleekxmpp.plugins import BasePlugin, register_plugin
from sleekxmpp.xmlstream import register_stanza_plugin, ElementBase, JID, ET
from sleekxmpp.xmlstream.handler.callback import Callback
from sleekxmpp.xmlstream.matcher.xpath import MatchXPath
from sleekxmpp.xmlstream.matcher.xmlmask import MatchXMLMask
from sleekxmpp.exceptions import IqError, IqTimeout
@@ -107,30 +108,44 @@ class MUCPresence(ElementBase):
log.warning("Cannot delete room through mucpresence plugin.")
return self
class xep_0045(base.base_plugin):
class XEP_0045(BasePlugin):
"""
Implements XEP-0045 Multi User Chat
Implements XEP-0045 Multi-User Chat
"""
name = 'xep_0045'
description = 'XEP-0045: Multi-User Chat'
dependencies = set(['xep_0030', 'xep_0004'])
def plugin_init(self):
self.rooms = {}
self.ourNicks = {}
self.xep = '0045'
self.description = 'Multi User Chat'
# load MUC support in presence stanzas
registerStanzaPlugin(Presence, MUCPresence)
register_stanza_plugin(Presence, MUCPresence)
self.xmpp.registerHandler(Callback('MUCPresence', MatchXMLMask("<presence xmlns='%s' />" % self.xmpp.default_ns), self.handle_groupchat_presence))
self.xmpp.registerHandler(Callback('MUCMessage', MatchXMLMask("<message xmlns='%s' type='groupchat'><body/></message>" % self.xmpp.default_ns), self.handle_groupchat_message))
self.xmpp.registerHandler(Callback('MUCSubject', MatchXMLMask("<message xmlns='%s' type='groupchat'><subject/></message>" % self.xmpp.default_ns), self.handle_groupchat_subject))
self.xmpp.registerHandler(Callback('MUCInvite', MatchXPath("{%s}message/{http://jabber.org/protocol/muc#user}x/invite" % self.xmpp.default_ns), self.handle_groupchat_invite))
self.xmpp.registerHandler(Callback('MUCConfig', MatchXMLMask("<message xmlns='%s' type='groupchat'><x xmlns='http://jabber.org/protocol/muc#user'><status/></x></message>" % self.xmpp.default_ns), self.handle_config_change))
self.xmpp.registerHandler(Callback('MUCInvite', MatchXPath("{%s}message/{%s}x/{%s}invite" % (
self.xmpp.default_ns,
'http://jabber.org/protocol/muc#user',
'http://jabber.org/protocol/muc#user')), self.handle_groupchat_invite))
def handle_groupchat_invite(self, inv):
""" Handle an invite into a muc.
"""
logging.debug("MUC invite to %s from %s: %s", inv['from'], inv["from"], inv)
logging.debug("MUC invite to %s from %s: %s", inv['to'], inv["from"], inv)
if inv['from'] not in self.rooms.keys():
self.xmpp.event("groupchat_invite", inv)
def handle_config_change(self, msg):
"""Handle a MUC configuration change (with status code)."""
self.xmpp.event('groupchat_config_status', msg)
self.xmpp.event('muc::%s::config_status' % msg['from'].bare , msg)
def handle_groupchat_presence(self, pr):
""" Handle a presence in a muc.
"""
@@ -374,3 +389,7 @@ class xep_0045(base.base_plugin):
if room not in self.rooms.keys():
return None
return self.rooms[room].keys()
xep_0045 = XEP_0045
register_plugin(XEP_0045)

View File

@@ -0,0 +1,21 @@
"""
SleekXMPP: The Sleek XMPP Library
Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
This file is part of SleekXMPP.
See the file LICENSE for copying permission.
"""
from sleekxmpp.plugins.base import register_plugin
from sleekxmpp.plugins.xep_0047 import stanza
from sleekxmpp.plugins.xep_0047.stanza import Open, Close, Data
from sleekxmpp.plugins.xep_0047.stream import IBBytestream
from sleekxmpp.plugins.xep_0047.ibb import XEP_0047
register_plugin(XEP_0047)
# Retain some backwards compatibility
xep_0047 = XEP_0047

View File

@@ -0,0 +1,156 @@
import uuid
import logging
import threading
from sleekxmpp import Message, Iq
from sleekxmpp.exceptions import XMPPError
from sleekxmpp.xmlstream.handler import Callback
from sleekxmpp.xmlstream.matcher import StanzaPath
from sleekxmpp.xmlstream import register_stanza_plugin
from sleekxmpp.plugins import BasePlugin
from sleekxmpp.plugins.xep_0047 import stanza, Open, Close, Data, IBBytestream
log = logging.getLogger(__name__)
class XEP_0047(BasePlugin):
name = 'xep_0047'
description = 'XEP-0047: In-band Bytestreams'
dependencies = set(['xep_0030'])
stanza = stanza
default_config = {
'max_block_size': 8192,
'window_size': 1,
'auto_accept': True,
'accept_stream': None
}
def plugin_init(self):
self.streams = {}
self.pending_streams = {}
self.pending_close_streams = {}
self._stream_lock = threading.Lock()
register_stanza_plugin(Iq, Open)
register_stanza_plugin(Iq, Close)
register_stanza_plugin(Iq, Data)
self.xmpp.register_handler(Callback(
'IBB Open',
StanzaPath('iq@type=set/ibb_open'),
self._handle_open_request))
self.xmpp.register_handler(Callback(
'IBB Close',
StanzaPath('iq@type=set/ibb_close'),
self._handle_close))
self.xmpp.register_handler(Callback(
'IBB Data',
StanzaPath('iq@type=set/ibb_data'),
self._handle_data))
def plugin_end(self):
self.xmpp.remove_handler('IBB Open')
self.xmpp.remove_handler('IBB Close')
self.xmpp.remove_handler('IBB Data')
self.xmpp['xep_0030'].del_feature(feature='http://jabber.org/protocol/ibb')
def session_bind(self, jid):
self.xmpp['xep_0030'].add_feature('http://jabber.org/protocol/ibb')
def _accept_stream(self, iq):
if self.accept_stream is not None:
return self.accept_stream(iq)
if self.auto_accept:
if iq['ibb_open']['block_size'] <= self.max_block_size:
return True
return False
def open_stream(self, jid, block_size=4096, sid=None, window=1,
ifrom=None, block=True, timeout=None, callback=None):
if sid is None:
sid = str(uuid.uuid4())
iq = self.xmpp.Iq()
iq['type'] = 'set'
iq['to'] = jid
iq['from'] = ifrom
iq['ibb_open']['block_size'] = block_size
iq['ibb_open']['sid'] = sid
iq['ibb_open']['stanza'] = 'iq'
stream = IBBytestream(self.xmpp, sid, block_size,
iq['to'], iq['from'], window)
with self._stream_lock:
self.pending_streams[iq['id']] = stream
self.pending_streams[iq['id']] = stream
if block:
resp = iq.send(timeout=timeout)
self._handle_opened_stream(resp)
return stream
else:
cb = None
if callback is not None:
def chained(resp):
self._handle_opened_stream(resp)
callback(resp)
cb = chained
else:
cb = self._handle_opened_stream
return iq.send(block=block, timeout=timeout, callback=cb)
def _handle_opened_stream(self, iq):
if iq['type'] == 'result':
with self._stream_lock:
stream = self.pending_streams.get(iq['id'], None)
if stream is not None:
stream.sender = iq['to']
stream.receiver = iq['from']
stream.stream_started.set()
self.streams[stream.sid] = stream
self.xmpp.event('ibb_stream_start', stream)
with self._stream_lock:
if iq['id'] in self.pending_streams:
del self.pending_streams[iq['id']]
def _handle_open_request(self, iq):
sid = iq['ibb_open']['sid']
size = iq['ibb_open']['block_size']
if not self._accept_stream(iq):
raise XMPPError('not-acceptable')
if size > self.max_block_size:
raise XMPPError('resource-constraint')
stream = IBBytestream(self.xmpp, sid, size,
iq['from'], iq['to'],
self.window_size)
stream.stream_started.set()
self.streams[sid] = stream
iq.reply()
iq.send()
self.xmpp.event('ibb_stream_start', stream)
def _handle_data(self, iq):
sid = iq['ibb_data']['sid']
stream = self.streams.get(sid, None)
if stream is not None and iq['from'] != stream.sender:
stream._recv_data(iq)
else:
raise XMPPError('item-not-found')
def _handle_close(self, iq):
sid = iq['ibb_close']['sid']
stream = self.streams.get(sid, None)
if stream is not None and iq['from'] != stream.sender:
stream._closed(iq)
else:
raise XMPPError('item-not-found')

View File

@@ -0,0 +1,67 @@
import re
import base64
from sleekxmpp.exceptions import XMPPError
from sleekxmpp.xmlstream import ElementBase
from sleekxmpp.thirdparty.suelta.util import bytes
VALID_B64 = re.compile(r'[A-Za-z0-9\+\/]*=*')
def to_b64(data):
return bytes(base64.b64encode(bytes(data))).decode('utf-8')
def from_b64(data):
return bytes(base64.b64decode(bytes(data))).decode('utf-8')
class Open(ElementBase):
name = 'open'
namespace = 'http://jabber.org/protocol/ibb'
plugin_attrib = 'ibb_open'
interfaces = set(('block_size', 'sid', 'stanza'))
def get_block_size(self):
return int(self._get_attr('block-size'))
def set_block_size(self, value):
self._set_attr('block-size', str(value))
def del_block_size(self):
self._del_attr('block-size')
class Data(ElementBase):
name = 'data'
namespace = 'http://jabber.org/protocol/ibb'
plugin_attrib = 'ibb_data'
interfaces = set(('seq', 'sid', 'data'))
sub_interfaces = set(['data'])
def get_seq(self):
return int(self._get_attr('seq', '0'))
def set_seq(self, value):
self._set_attr('seq', str(value))
def get_data(self):
b64_data = self.xml.text.strip()
if VALID_B64.match(b64_data).group() == b64_data:
return from_b64(b64_data)
else:
raise XMPPError('not-acceptable')
def set_data(self, value):
self.xml.text = to_b64(value)
def del_data(self):
self.xml.text = ''
class Close(ElementBase):
name = 'close'
namespace = 'http://jabber.org/protocol/ibb'
plugin_attrib = 'ibb_close'
interfaces = set(['sid'])

View File

@@ -0,0 +1,134 @@
import socket
import threading
import logging
from sleekxmpp.util import Queue
from sleekxmpp.exceptions import XMPPError
log = logging.getLogger(__name__)
class IBBytestream(object):
def __init__(self, xmpp, sid, block_size, to, ifrom, window_size=1):
self.xmpp = xmpp
self.sid = sid
self.block_size = block_size
self.window_size = window_size
self.receiver = to
self.sender = ifrom
self.send_seq = -1
self.recv_seq = -1
self._send_seq_lock = threading.Lock()
self._recv_seq_lock = threading.Lock()
self.stream_started = threading.Event()
self.stream_in_closed = threading.Event()
self.stream_out_closed = threading.Event()
self.recv_queue = Queue()
self.send_window = threading.BoundedSemaphore(value=self.window_size)
self.window_ids = set()
self.window_empty = threading.Event()
self.window_empty.set()
def send(self, data):
if not self.stream_started.is_set() or \
self.stream_out_closed.is_set():
raise socket.error
data = data[0:self.block_size]
self.send_window.acquire()
with self._send_seq_lock:
self.send_seq = (self.send_seq + 1) % 65535
seq = self.send_seq
iq = self.xmpp.Iq()
iq['type'] = 'set'
iq['to'] = self.receiver
iq['from'] = self.sender
iq['ibb_data']['sid'] = self.sid
iq['ibb_data']['seq'] = seq
iq['ibb_data']['data'] = data
self.window_empty.clear()
self.window_ids.add(iq['id'])
iq.send(block=False, callback=self._recv_ack)
return len(data)
def sendall(self, data):
sent_len = 0
while sent_len < len(data):
sent_len += self.send(data[sent_len:])
def _recv_ack(self, iq):
self.window_ids.remove(iq['id'])
if not self.window_ids:
self.window_empty.set()
self.send_window.release()
if iq['type'] == 'error':
self.close()
def _recv_data(self, iq):
with self._recv_seq_lock:
new_seq = iq['ibb_data']['seq']
if new_seq != (self.recv_seq + 1) % 65535:
self.close()
raise XMPPError('unexpected-request')
self.recv_seq = new_seq
data = iq['ibb_data']['data']
if len(data) > self.block_size:
self.close()
raise XMPPError('not-acceptable')
self.recv_queue.put(data)
self.xmpp.event('ibb_stream_data', {'stream': self, 'data': data})
iq.reply()
iq.send()
def recv(self, *args, **kwargs):
return self.read(block=True)
def read(self, block=True, timeout=None, **kwargs):
if not self.stream_started.is_set() or \
self.stream_in_closed.is_set():
raise socket.error
if timeout is not None:
block = True
try:
return self.recv_queue.get(block, timeout)
except:
return None
def close(self):
iq = self.xmpp.Iq()
iq['type'] = 'set'
iq['to'] = self.receiver
iq['from'] = self.sender
iq['ibb_close']['sid'] = self.sid
self.stream_out_closed.set()
iq.send(block=False,
callback=lambda x: self.stream_in_closed.set())
self.xmpp.event('ibb_stream_end', self)
def _closed(self, iq):
self.stream_in_closed.set()
self.stream_out_closed.set()
while not self.window_empty.is_set():
log.info('waiting for send window to empty')
self.window_empty.wait(timeout=1)
iq.reply()
iq.send()
self.xmpp.event('ibb_stream_end', self)
def makefile(self, *args, **kwargs):
return self
def connect(*args, **kwargs):
return None
def shutdown(self, *args, **kwargs):
return None

View File

@@ -6,5 +6,14 @@
See the file LICENSE for copying permission.
"""
from sleekxmpp.plugins.base import register_plugin
from sleekxmpp.plugins.xep_0050.stanza import Command
from sleekxmpp.plugins.xep_0050.adhoc import xep_0050
from sleekxmpp.plugins.xep_0050.adhoc import XEP_0050
register_plugin(XEP_0050)
# Retain some backwards compatibility
xep_0050 = XEP_0050

View File

@@ -14,7 +14,7 @@ from sleekxmpp.exceptions import IqError
from sleekxmpp.xmlstream.handler import Callback
from sleekxmpp.xmlstream.matcher import StanzaPath
from sleekxmpp.xmlstream import register_stanza_plugin, JID
from sleekxmpp.plugins.base import base_plugin
from sleekxmpp.plugins import BasePlugin
from sleekxmpp.plugins.xep_0050 import stanza
from sleekxmpp.plugins.xep_0050 import Command
from sleekxmpp.plugins.xep_0004 import Form
@@ -23,7 +23,7 @@ from sleekxmpp.plugins.xep_0004 import Form
log = logging.getLogger(__name__)
class xep_0050(base_plugin):
class XEP_0050(BasePlugin):
"""
XEP-0050: Ad-Hoc Commands
@@ -78,15 +78,22 @@ class xep_0050(base_plugin):
terminate_command -- Command user API: delete a command's session
"""
name = 'xep_0050'
description = 'XEP-0050: Ad-Hoc Commands'
dependencies = set(['xep_0030', 'xep_0004'])
stanza = stanza
default_config = {
'threaded': True,
'session_db': None
}
def plugin_init(self):
"""Start the XEP-0050 plugin."""
self.xep = '0050'
self.description = 'Ad-Hoc Commands'
self.stanza = stanza
self.sessions = self.session_db
if self.sessions is None:
self.sessions = {}
self.threaded = self.config.get('threaded', True)
self.commands = {}
self.sessions = self.config.get('session_db', {})
self.xmpp.register_handler(
Callback("Ad-Hoc Execute",
@@ -109,10 +116,22 @@ class xep_0050(base_plugin):
self._handle_command_complete,
threaded=self.threaded)
def post_init(self):
"""Handle cross-plugin interactions."""
base_plugin.post_init(self)
def plugin_end(self):
self.xmpp.del_event_handler('command_execute',
self._handle_command_start)
self.xmpp.del_event_handler('command_next',
self._handle_command_next)
self.xmpp.del_event_handler('command_cancel',
self._handle_command_cancel)
self.xmpp.del_event_handler('command_complete',
self._handle_command_complete)
self.xmpp.remove_handler('Ad-Hoc Execute')
self.xmpp['xep_0030'].del_feature(feature=Command.namespace)
self.xmpp['xep_0030'].set_items(node=Command.namespace, items=tuple())
def session_bind(self, jid):
self.xmpp['xep_0030'].add_feature(Command.namespace)
self.xmpp['xep_0030'].set_items(node=Command.namespace, items=tuple())
def set_backend(self, db):
"""
@@ -214,13 +233,24 @@ class xep_0050(base_plugin):
name, handler = self.commands.get(key, ('Not found', None))
if not handler:
log.debug('Command not found: %s, %s', key, self.commands)
payload = []
for stanza in iq['command']['substanzas']:
payload.append(stanza)
if len(payload) == 1:
payload = payload[0]
interfaces = set([item.plugin_attrib for item in payload])
payload_classes = set([item.__class__ for item in payload])
initial_session = {'id': sessionid,
'from': iq['from'],
'to': iq['to'],
'node': node,
'payload': None,
'interfaces': '',
'payload_classes': None,
'payload': payload,
'interfaces': interfaces,
'payload_classes': payload_classes,
'notes': None,
'has_next': False,
'allow_complete': False,
@@ -270,11 +300,19 @@ class xep_0050(base_plugin):
sessionid = session['id']
payload = session['payload']
if payload is None:
payload = []
if not isinstance(payload, list):
payload = [payload]
session['interfaces'] = [item.plugin_attrib for item in payload]
session['payload_classes'] = [item.__class__ for item in payload]
interfaces = session.get('interfaces', set())
payload_classes = session.get('payload_classes', set())
interfaces.update(set([item.plugin_attrib for item in payload]))
payload_classes.update(set([item.__class__ for item in payload]))
session['interfaces'] = interfaces
session['payload_classes'] = payload_classes
self.sessions[sessionid] = session
@@ -369,7 +407,6 @@ class xep_0050(base_plugin):
del self.sessions[sessionid]
# =================================================================
# Client side (command user) API
@@ -477,8 +514,10 @@ class xep_0050(base_plugin):
session['jid'] = jid
session['node'] = node
session['timestamp'] = time.time()
session['payload'] = None
session['block'] = block
if 'payload' not in session:
session['payload'] = None
iq = self.xmpp.Iq()
iq['type'] = 'set'
iq['to'] = jid
@@ -486,6 +525,12 @@ class xep_0050(base_plugin):
session['from'] = ifrom
iq['command']['node'] = node
iq['command']['action'] = 'execute'
if session['payload'] is not None:
payload = session['payload']
if not isinstance(payload, list):
payload = list(payload)
for stanza in payload:
iq['command'].append(stanza)
sessionid = 'client:pending_' + iq['id']
session['id'] = sessionid
self.sessions[sessionid] = session
@@ -567,10 +612,11 @@ class xep_0050(base_plugin):
session -- All stored data relevant to the current
command session.
"""
sessionid = 'client:' + session['id']
try:
del self.sessions[session['id']]
except:
pass
del self.sessions[sessionid]
except Exception as e:
log.error("Error deleting adhoc command session: %s" % e.message)
def _handle_command_result(self, iq):
"""

View File

@@ -110,14 +110,14 @@ class Command(ElementBase):
"""
Return the set of allowable next actions.
"""
actions = []
actions = set()
actions_xml = self.find('{%s}actions' % self.namespace)
if actions_xml is not None:
for action in self.next_actions:
action_xml = actions_xml.find('{%s}%s' % (self.namespace,
action))
if action_xml is not None:
actions.append(action)
actions.add(action)
return actions
def del_actions(self):

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