Compare commits

...

398 Commits

Author SHA1 Message Date
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
79f1aa0e1b Update version and README for 1.0 release. 2012-01-03 17:04:15 -05:00
Lance Stout
fb5a6a7d71 Merge pull request #132 from rhcarvalho/master
Fix a typo in several files.
2012-01-02 13:10:46 -08:00
Rodolfo Carvalho
7d1c5f4a2b Fix a typo in several files.
This fixes several instances of "intial" for "initial".
2012-01-02 18:59:39 -02: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
1b0fd76b45 Merge pull request #131 from rhcarvalho/master
Small changes to the examples
2011-12-31 17:27:57 -08: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
Rodolfo Carvalho
46e93bea09 Remove unused import.
I forgot about this one before!
2011-12-31 20:14:24 -02:00
Rodolfo Carvalho
cbc6a0296b Ask interactively for missing command line arguments.
Instead of complaining that the arguments were not given, ask interactively for input.
This example was the only one to behave differently from the others.
2011-12-31 19:54:14 -02:00
Rodolfo Carvalho
cc63bef179 Remove unused imports in the examples. 2011-12-31 19:50:53 -02:00
Rodolfo Carvalho
cbcfa156c4 Add missing import. 2011-12-31 19:48:03 -02:00
Lance Stout
fa912aeb84 Merge branch 'develop' into develop-1.1 2011-12-31 12:34:17 -05:00
Lance Stout
4a12e1059a Add proxy docs. 2011-12-31 12:33:32 -05:00
Lance Stout
9a5e2ae768 Merge branch 'develop' into develop-1.1 2011-12-31 02:16:08 -05:00
Lance Stout
f9cd051209 Add echo component briefing. 2011-12-31 02:00:54 -05:00
Lance Stout
e0545bf0bc Merge branch 'develop' into develop-1.1 2011-12-31 01:29:12 -05:00
Lance Stout
03bc38f7e3 Add docs on using Iq stanzas. 2011-12-31 01:28:41 -05:00
Lance Stout
4e23a4e08e Merge pull request #130 from rhcarvalho/master
Some small fixes
2011-12-30 20:14:54 -08: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
Rodolfo Carvalho
8cafa8578f Update examples to use the block'' argument instead of the deprecated threaded''. 2011-12-30 17:25:03 -02:00
Rodolfo Carvalho
b74ea47650 Fix docstring of a method of Message stanzas. 2011-12-30 17:08:32 -02:00
Rodolfo Carvalho
2dc230a68b Replace pydns with dnspython in the comments of the examples. 2011-12-30 00:08:05 -02: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
522f0dac16 Merge branch 'develop' of github.com:fritzy/SleekXMPP into develop 2011-12-27 18:03:08 -05:00
Lance Stout
cd5ae944ec Merge pull request #128 from correl/rpc_value_conversion_fix
XEP-0009 tests updated for Python3
2011-12-27 15:03:01 -08:00
Lance Stout
42a86fe0d4 Disconnect when a SyntaxError is found.
This should resolve issue #102
2011-12-27 18:01:26 -05:00
Correl Roush
e928b9c434 XEP-0009: Updated tests to work in python 3 2011-12-20 21:19:51 -05:00
Lance Stout
fb55d9e9d1 Add comma to fix pubsub error conditions.
Fixes issue #127
2011-12-20 12:30:35 -05:00
Lance Stout
74e7e5a291 Merge pull request #125 from correl/rpc_value_conversion_fix
XEP-0009 XML-RPC value conversion fixes and unit tests
2011-12-20 09:29:03 -08:00
Correl Roush
6c58b8cc4b XEP-0009: Updated RPC value conversion code
Updated the XML-RPC value conversion to correctly apply namespaces, and
fixed an error uncovered by the tests in the XML -> Python conversion of
dateTime values.
2011-12-20 02:03:06 -05:00
Correl Roush
2b3d11a7a5 XEP-0009: Added value conversion unit tests
Added tests for bidirectional conversion of all XML-RPC data types
2011-12-20 02:01:34 -05:00
Correl Roush
9950208d06 Fixes Issue #123: Corrected boolean xml to python conversion 2011-12-16 17:41:16 +00:00
Lance Stout
a67e16d1b7 Merge pull request #122 from correl/acl_check_fix
XEP-0009: ACL.check fix
2011-12-15 16:14:29 -08:00
Correl Roush
c98a22e065 Fixed Issue 93: ACL.check jid parameter should be a string value 2011-12-15 21:58:33 +00: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
d496417deb Allow XEP-0082 to return datetime objects without having to format and reparse. 2011-12-15 12:02:08 -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
116bb6e1b9 Use OrderedDicts instead of regular dictionaries when returning values from forms. 2011-12-13 09:00:45 -08:00
Lance Stout
9c6dde5d22 Ensure that item fields have the proper type.
The item fields were not setting their type based on the reported
field's type attribute, so values were not being encoded properly.

Fixes issue #121
2011-12-13 08:59:39 -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
Lance Stout
fc8a13df5a Allow disco info/items handlers to return full Iq stanzas.
Only allowing handlers to return a DiscoInfo/DiscoItem stanza works
for the majority of cases, but does not allow for the addition of
an RSM stanza, or other extensions.

An Iq stanza returned by a handler must already be configured as
a reply.
2011-12-12 19:38:32 -08:00
Lance Stout
85e9042db6 Pass the Iq stanza to disco item handlers. 2011-12-12 16:34:24 -08:00
Lance Stout
62e6d6fb4c Fix iterable substanzas when added as normal plugin.
If an iterable plugin was an enabled, it wasn't added to
the iterables list.
2011-12-11 17:04:58 -08:00
Lance Stout
16c72e8efd Use UTC for xep_0082.date. 2011-12-09 23:59:33 -08:00
Lance Stout
efe1b9f5a9 Allow sending stanzas on session_end.
May set self.disconnect_wait=True so that all disconnect
calls wait for the send queue to empty, unless explicitly
overridden with wait=False.

The session_end now fires before closing the socket so
that final stanzas may be sent, such as unavailable presences
for components.
2011-12-09 23:56:39 -08:00
Lance Stout
65dbddb6b6 Fix logging when loading plugins. 2011-12-09 20:57:08 -08:00
Lance Stout
2a67a31120 Prevent hang when terminating during delayed connection. 2011-12-07 22:16:58 -08:00
Lance Stout
a720c3348b Updated last bit of core files to use new API format. 2011-12-05 20:37:47 -08:00
Lance Stout
79ac60b6e8 Fix example boilerplate code syntax. 2011-12-05 08:57:57 -08:00
Lance Stout
e01c2d222a More doc updates 2011-12-05 08:55:05 -08:00
Lance Stout
8922e2050a Update the API docs for XMLStream 2011-12-04 20:35:17 -08:00
Lance Stout
a85891c611 Add API docs for the scheduler 2011-12-04 16:43:05 -08:00
Lance Stout
2586fdffda Update api docs for handlers and matchers 2011-12-04 16:26:14 -08:00
Lance Stout
c9dc9ec11e Update supported XEP list 2011-12-04 15:39:49 -08:00
Lance Stout
b9332142c9 Update api docs for JID 2011-12-04 13:42:46 -08:00
Lance Stout
b7b53362e1 Ensure that adhoc command clients have form plugin registered.
The form plugin was being registered on first use for providers,
but not for clients receiving the form.

NOTE: Use of non-form payloads will have this issue - adhoc command
      clients will need to have an expectation beforehand of what
      the command payload will be to properly load stanza plugins.
2011-12-04 01:24:35 -08:00
Lance Stout
68cf66a5fe Ensure that saving a roster item includes the correct subscription value.
Fixes issue #118
2011-11-28 15:00:35 -08:00
Florent Le Coz
4eb7eeb40f Send the encoded data (bytes) and not the str, on the socket. 2011-11-25 01:45:43 +08:00
Lance Stout
a1d64fa215 Experimental support for handling SSL write errors. 2011-11-23 23:59:05 -08:00
Lance Stout
5f44c0e678 Add docs for filesocket 2011-11-22 16:33:38 -08:00
Lance Stout
b87c4d786d Update tostring docs, plus more doc cleanup 2011-11-22 16:25:33 -08:00
Lance Stout
329b0df3f6 Some more docs house cleaning 2011-11-22 15:25:24 -08:00
Lance Stout
6906c15e8e Update docs for tostring 2011-11-22 15:25:02 -08:00
Lance Stout
ff5421cefc Moar docs! 2011-11-21 23:28:19 -08:00
Lance Stout
4498e992a2 Add more stanzabase docs 2011-11-21 23:17:39 -08:00
Lance Stout
2d610dfdc8 Fix stream handler test for multiple handlers to exist properly. 2011-11-21 22:03:43 -08:00
Lance Stout
2b0a05ee32 Update stanzabase docs 2011-11-21 21:51:19 -08:00
Lance Stout
bc2d0ee9a8 Update docs index 2011-11-20 17:29:54 -08:00
Lance Stout
862a2a1440 Ensure that reconnection happens properly after connection loss.
Calling reconnect() simultaneously from multiple threads (like when
using XEP-0199 keepalive) could break because the connection state
can transition and break the state expectations in one of the
reconnect() calls.
2011-11-20 12:18:37 -08:00
Lance Stout
fba60ffff1 Convert daemon threads back into normal threads.
This may need to be reverted if CTRL-C handling breaks, but everything
works fine so far in testing.

Resolves issue #95.
2011-11-20 12:17:35 -08:00
Lance Stout
d1a945a305 Tidy up logging some more 2011-11-19 19:18:43 -08:00
Lance Stout
685b9ab102 Fix logging exceptions from formatting issues. 2011-11-19 19:08:27 -08:00
Lance Stout
24f27c0fe3 Pass generic connection errors to XMLStream.exception() 2011-11-19 19:01:07 -08:00
Lance Stout
3019c82d8a Use a list comprehension instead of filter() to work with Python3. 2011-11-19 18:49:18 -08:00
Lance Stout
f9d0b55ca3 Add unit test for copying stanzas when passed to events. 2011-11-19 18:43:38 -08:00
Lance Stout
b54cc97e4c Merge remote-tracking branch 'vijayp/master' into HEAD
Conflicts:
	examples/ping.py
	sleekxmpp/basexmpp.py
	sleekxmpp/clientxmpp.py
	sleekxmpp/features/feature_bind/bind.py
	sleekxmpp/features/feature_mechanisms/mechanisms.py
	sleekxmpp/plugins/gmail_notify.py
	sleekxmpp/plugins/jobs.py
	sleekxmpp/plugins/xep_0009/remote.py
	sleekxmpp/plugins/xep_0009/rpc.py
	sleekxmpp/plugins/xep_0012.py
	sleekxmpp/plugins/xep_0045.py
	sleekxmpp/plugins/xep_0050/adhoc.py
	sleekxmpp/plugins/xep_0078/legacyauth.py
	sleekxmpp/plugins/xep_0085/chat_states.py
	sleekxmpp/plugins/xep_0199/ping.py
	sleekxmpp/plugins/xep_0224/attention.py
	sleekxmpp/xmlstream/handler/waiter.py
	sleekxmpp/xmlstream/matcher/xmlmask.py
	sleekxmpp/xmlstream/xmlstream.py

Conflicts:
	examples/ping.py
	sleekxmpp/basexmpp.py
	sleekxmpp/clientxmpp.py
	sleekxmpp/features/feature_bind/bind.py
	sleekxmpp/features/feature_mechanisms/mechanisms.py
	sleekxmpp/plugins/gmail_notify.py
	sleekxmpp/plugins/jobs.py
	sleekxmpp/plugins/xep_0009/remote.py
	sleekxmpp/plugins/xep_0009/rpc.py
	sleekxmpp/plugins/xep_0012.py
	sleekxmpp/plugins/xep_0045.py
	sleekxmpp/plugins/xep_0050/adhoc.py
	sleekxmpp/plugins/xep_0078/legacyauth.py
	sleekxmpp/plugins/xep_0085/chat_states.py
	sleekxmpp/plugins/xep_0199/ping.py
	sleekxmpp/plugins/xep_0224/attention.py
	sleekxmpp/xmlstream/handler/waiter.py
	sleekxmpp/xmlstream/matcher/xmlmask.py
	sleekxmpp/xmlstream/xmlstream.py
2011-11-19 18:23:26 -08:00
Vijay Pandurangan
e3b9d5abbf double copy 2011-11-19 16:03:17 -08:00
Vijay Pandurangan
2332970cf2 elide unnecessary copy 2011-11-19 16:02:41 -08:00
Vijay Pandurangan
48af3d3322 remove unnecessary copies when only one handler matches. This was taking up ~ 15% of CPU on moderate load. 2011-11-19 15:59:38 -08:00
Lance Stout
429c94d6a9 Tidy up logging calls. 2011-11-19 12:07:57 -08:00
Vijay Pandurangan
deb52ad350 This change stops sleekxmpp from spending huge amounts of time unnecessarily computing logging data that may never be used. This is a HUGE performance improvement; in some of my test runs, unnecessary string creation was accounting for > 60% of all CPU time.
Note that using % in a string will _always_ perform the sting substitutions, because the strings are constructed before the function is called. So log.debug('%s' % expensiveoperation()) will take about the same CPU time whether or not the logging level is DEBUG or INFO. if you use , no substitutions are performed unless the string is actually logged
2011-11-20 03:39:05 +08:00
Vijay Pandurangan
6f3cc77bb5 This change stops sleekxmpp from spending huge amounts of time unnecessarily computing logging data that may never be used. This is a HUGE performance improvement; in some of my test runs, unnecessary string creation was accounting for > 60% of all CPU time.
Note that using % in a string will _always_ perform the sting substitutions, because the strings are constructed before the function is called. So log.debug('%s' % expensiveoperation()) will take about the same CPU time whether or not the logging level is DEBUG or INFO. if you use , no substitutions are performed unless the string is actually logged
2011-11-19 11:30:44 -08:00
Lance Stout
1baf139ca4 Bump next release version to 1.0-RC4 2011-11-18 16:40:17 -08:00
Lance Stout
7945b3e738 Remove the config_component example in favor of echo_component.
The roster portion of the example is too outdated.
2011-11-18 16:26:02 -08:00
Lance Stout
d4c1ff5309 Also fire changed_status when the status text changes for a resource. 2011-11-18 13:57:41 -08:00
Lance Stout
22868c3924 Fix changed_status event
Once again only fires when a resource's presence show value changes.
2011-11-18 13:39:02 -08:00
Lance Stout
2de1be188c Add echo component example. 2011-11-17 12:25:56 -08:00
Lance Stout
9faecec2db Simplify boilerplate example. 2011-11-14 12:00:21 -08:00
Lance Stout
5d7111fe3b Update list of stable releases. 2011-11-14 11:46:07 -08:00
Lance Stout
0c86f8288d No need to continue processing loop if an error ocurred and auto_reconnect=False. 2011-11-14 11:21:05 -08:00
Lance Stout
5a6a65fd9f Fix typo 2011-11-14 11:20:53 -08:00
Lance Stout
43c4d23896 Explicitly test for inequality in JIDs.
Fixes issue #113
2011-11-14 09:15:43 -08:00
Lance Stout
9f9e8db814 Add use_ssl parameter to ClientXMPP.connect 2011-11-11 01:52:18 -08:00
Lance Stout
b8efcc7cf0 Don't just call self.disconnect in self.reconnect.
It messes up the auto_reconnect value and causes the XML processing
loop to spin wildly with errors on a stream disconnect.
2011-11-08 19:23:53 -08:00
Lance Stout
2f29d18e53 Use setuptools if available. 2011-11-08 07:01:16 -08:00
Lance Stout
888e286a09 Continue trying to reconnect, even if the attempt fails.
The transition from disconnected to connected states must be done in a
loop in case the transition fails, not just once and hope it worked.
2011-11-07 01:13:34 -08:00
Lance Stout
1a93a187f0 Fix a crash when removing a contact.
Original author: louiz
2011-11-06 08:33:03 -08:00
Lance Stout
a8d5da5091 Restore original behaviour for auto_authorize and auto_subscribe.
The change to using the new roster broke the original auto_* values
and used per-roster versions. The original auto_* values will now set
the behaviour globally. Use the per-roster values to override for a
specific JID.
2011-11-06 08:25:29 -08:00
Lance Stout
e2720fac9e FIX SCRAM-SHA-1-PLUS
The mechanism name was being correctly de-plussed, but then we used the
original, -PLUS, name to extract the hash, finding SHA-1-PLUS and therefore
finding no match.

Test-Information:

Tested with Sleek against an Isode M-Link with SCRAM-SHA-1-PLUS available.

Author: dwd
2011-10-27 15:16:54 -04:00
Lance Stout
4374729f20 Update the docs for XEP-0060 publish method. 2011-10-11 20:37:50 -04:00
Lance Stout
87999333cb Fix MUC methods to optionally specify the sending JID.
Should fix issue #107
2011-10-10 11:31:03 -04:00
Lance Stout
335dc2927b Break reference cycle to fix potential memory leaks for callback handlers. 2011-10-08 17:31:30 -04:00
Lance Stout
ccbef6b696 Fix typos in the roster update method. 2011-10-07 18:13:50 -04:00
Lance Stout
3e384d3cfe XEP-0009 will likely be updated to use <base64 /> instead of <Base64 />
Both are supported when reading, but <base64 /> will be used for output.
2011-10-05 12:09:50 -04:00
Lance Stout
e33949c397 Merge branch 'develop' of github.com:fritzy/SleekXMPP into develop 2011-10-04 10:37:42 -04:00
Lance Stout
eccac859ad Fix missing import statement.
Fixes issue #105
2011-10-04 10:36:52 -04:00
Lance Stout
7dd586f2fd Merge pull request #104 from correl/develop
Make RPC events threaded
2011-10-03 13:19:16 -07:00
Correl Roush
3607c5b792 Make RPC events threaded
Allows, for example, an RPC service to make remote RPC calls with its
own connection without blocking its own thread waiting for the result.
2011-10-03 14:32:48 -04:00
Lance Stout
e37adace62 Allow SASL mechanism to be set when creating a ClientXMPP instance.
Instead of using:

    ClientXMPP(jid, password, plugin_config={
        'feature_mechanisms': {'use_mech': 'SOME-MECH'}})

You can use:

    ClientXMPP(jid, password, sasl_mech='SOME-MECH')

If you need to change the mechanism after instantiation, use:

    xmpp['feature_mechanisms'].sasl.mech = 'SCRAM-MD5'
2011-09-28 22:48:30 -04:00
Lance Stout
d10f591bf4 Expand live stream testing capabilities. 2011-09-28 17:26:29 -04:00
Lance Stout
262da78ca7 Fix del_event_handler for Python3 (different semantics for filter()).
Fixes issue #103
2011-09-23 12:03:49 -04:00
Lance Stout
0b83edf439 Fix regression for handling the case where the server terminates the stream.
The processing loop was continuing to call __read_xml after </stream>
was received, which caused SyntaxErrors (can't find starting element).

This should fix issue #102
2011-09-22 01:32:44 -04:00
Nathan Fritz
cf7fcf496e SyntaxError requires a restart 2011-09-19 11:53:09 -07:00
Lance Stout
1765271f84 Make get_node_config block by default. 2011-09-02 11:52:56 -07:00
Lance Stout
0ec79f8dc3 Tweak setup.py, and bump dev version to RC3. 2011-09-01 16:47:30 -07:00
253 changed files with 15101 additions and 4578 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

@@ -44,17 +44,15 @@ The latest source code for SleekXMPP may be found on `Github
``master`` branch, while the latest development version is in the
``develop`` branch.
**Stable Releases**
- `1.0 Beta6.1 <http://github.com/fritzy/SleekXMPP/zipball/1.0-Beta6.1>`_
- `1.0 Beta5 <http://github.com/fritzy/SleekXMPP/zipball/1.0-Beta5>`_
- `1.0 Beta4 <http://github.com/fritzy/SleekXMPP/zipball/1.0-Beta4>`_
- `1.0 Beta3 <http://github.com/fritzy/SleekXMPP/zipball/1.0-Beta3>`_
- `1.0 Beta2 <http://github.com/fritzy/SleekXMPP/zipball/1.0-Beta2>`_
- `1.0 Beta1 <http://github.com/fritzy/SleekXMPP/zipball/1.0-Beta1>`_
**Latest Release**
- `1.1.2 <http://github.com/fritzy/SleekXMPP/zipball/1.1.2>`_
**Develop Releases**
- `Latest Develop Version <http://github.com/fritzy/SleekXMPP/zipball/develop>`_
**Older Stable Releases**
- `1.0 <http://github.com/fritzy/SleekXMPP/zipball/1.0>`_
Installing DNSPython
---------------------
If you are using Python3 and wish to use dnspython, you will have to checkout and
@@ -109,23 +107,37 @@ SleekXMPP projects::
def __init__(self, jid, password):
ClientXMPP.__init__(self, jid, password)
self.add_event_handler("session_start", self.start)
self.add_event_handler("session_start", self.session_start)
self.add_event_handler("message", self.message)
def start(self, event):
self.send_presence()
# 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
# Most get_* methods from plugins use Iq stanzas, which
# 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()
#
# 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'):
@@ -140,19 +152,8 @@ SleekXMPP projects::
format='%(levelname)-8s %(message)s')
xmpp = EchoBot('somejid@example.com', 'use_getpass')
xmpp.register_plugin('xep_0030') # Service Discovery
xmpp.register_plugin('xep_0199') # XMPP Ping
# If you are working with an OpenFire server, you will need
# to use a different SSL version:
#
# import ssl
# xmpp.ssl_version = ssl.PROTOCOL_SSLv3
if xmpp.connect():
xmpp.process(block=True)
else:
print("Unable to connect.")
xmpp.connect()
xmpp.process(block=True)
Credits
@@ -162,8 +163,8 @@ Credits
`@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.
<http://code.google.com/p/seesmic-as3-xmpp/>`_, and a former member of
the XMPP Council.
**Co-Author:** Lance Stout
`lancestout@gmail.com <xmpp:lancestout@gmail.com?message>`_,

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

View File

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

View File

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

View File

@@ -4,7 +4,7 @@
Sphinx layout template for the haiku theme.
:copyright: Copyright 2007-2011 by the Sphinx team, see AUTHORS.
:copyright: Copyright 2007-2010 by the Sphinx team, see AUTHORS.
:license: BSD, see LICENSE for details.
#}
{% extends "basic/layout.html" %}
@@ -44,9 +44,8 @@
{%- if logo -%}
<img class="rightlogo" src="{{ pathto('_static/' + logo, 1) }}" alt="Logo"/>
{%- endif -%}
<h1 class="heading">
<a href="{{ pathto('index') }}"><span>{{ project|e }}</span></a>
</h1>
<h1 class="heading"><a href="{{ pathto('index') }}">
<span>{{ title|striptags }}</span></a></h1>
<h2 class="heading"><span>{{ shorttitle|e }}</span></h2>
{%- endif %}
{%- endblock %}
@@ -66,4 +65,6 @@
<div class="bottomnav">
{{ nav() }}
</div>
<a id="from_andyet" href="http://andyet.net"><h2>From &amp;yet</h2></a>
{% endblock %}

View File

@@ -1,5 +1,5 @@
========
basexmpp
BaseXMPP
========
.. module:: sleekxmpp.basexmpp

View File

@@ -1,17 +1,8 @@
==========
clientxmpp
ClientXMPP
==========
.. module:: sleekxmpp.clientxmpp
.. autoclass:: ClientXMPP
.. automethod:: connect
.. automethod:: register_feature
.. automethod:: get_roster
.. automethod:: update_roster
.. automethod:: del_roster_item
:members:

View File

@@ -0,0 +1,8 @@
=============
ComponentXMPP
=============
.. module:: sleekxmpp.componentxmpp
.. autoclass:: ComponentXMPP
:members:

14
docs/api/exceptions.rst Normal file
View File

@@ -0,0 +1,14 @@
Exceptions
==========
.. module:: sleekxmpp.exceptions
.. autoexception:: XMPPError
:members:
.. autoexception:: IqError
:members:
.. autoexception:: IqTimeout
:members:

View File

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

View File

@@ -0,0 +1,12 @@
.. module:: sleekxmpp.xmlstream.filesocket
.. _filesocket:
Python 2.6 File Socket Shims
============================
.. autoclass:: FileSocket
:members:
.. autoclass:: Socket26
:members:

View File

@@ -0,0 +1,24 @@
Stanza Handlers
===============
The Basic Handler
-----------------
.. module:: sleekxmpp.xmlstream.handler.base
.. autoclass:: BaseHandler
:members:
Callback
--------
.. module:: sleekxmpp.xmlstream.handler.callback
.. autoclass:: Callback
:members:
Waiter
------
.. module:: sleekxmpp.xmlstream.handler.waiter
.. autoclass:: Waiter
:members:

View File

@@ -0,0 +1,7 @@
Jabber IDs (JID)
=================
.. module:: sleekxmpp.xmlstream.jid
.. autoclass:: JID
:members:

View File

@@ -0,0 +1,41 @@
Stanza Matchers
===============
The Basic Matcher
-----------------
.. module:: sleekxmpp.xmlstream.matcher.base
.. autoclass:: MatcherBase
:members:
ID Matching
-----------
.. module:: sleekxmpp.xmlstream.matcher.id
.. autoclass:: MatcherId
:members:
Stanza Path Matching
--------------------
.. module:: sleekxmpp.xmlstream.matcher.stanzapath
.. autoclass:: StanzaPath
:members:
XPath
-----
.. module:: sleekxmpp.xmlstream.matcher.xpath
.. autoclass:: MatchXPath
:members:
XMLMask
-------
.. module:: sleekxmpp.xmlstream.matcher.xmlmask
.. autoclass:: MatchXMLMask
:members:

View File

@@ -0,0 +1,11 @@
=========
Scheduler
=========
.. module:: sleekxmpp.xmlstream.scheduler
.. autoclass:: Task
:members:
.. autoclass:: Scheduler
:members:

View File

@@ -0,0 +1,123 @@
.. _stanzabase:
==============
Stanza Objects
==============
.. module:: sleekxmpp.xmlstream.stanzabase
The :mod:`~sleekmxpp.xmlstream.stanzabase` module provides a wrapper for the
standard :mod:`~xml.etree.ElementTree` module that makes working with XML
less painful. Instead of having to manually move up and down an element
tree and insert subelements and attributes, you can interact with an object
that behaves like a normal dictionary or JSON object, which silently maps
keys to XML attributes and elements behind the scenes.
Overview
--------
The usefulness of this layer grows as the XML you have to work with
becomes nested. The base unit here, :class:`ElementBase`, can map to a
single XML element, or several depending on how advanced of a mapping
is desired from interface keys to XML structures. For example, a single
:class:`ElementBase` derived class could easily describe:
.. code-block:: xml
<message to="user@example.com" from="friend@example.com">
<body>Hi!</body>
<x:extra>
<x:item>Custom item 1</x:item>
<x:item>Custom item 2</x:item>
<x:item>Custom item 3</x:item>
</x:extra>
</message>
If that chunk of XML were put in the :class:`ElementBase` instance
``msg``, we could extract the data from the XML using::
>>> msg['extra']
['Custom item 1', 'Custom item 2', 'Custom item 3']
Provided we set up the handler for the ``'extra'`` interface to load the
``<x:item>`` element content into a list.
The key concept is that given an XML structure that will be repeatedly
used, we can define a set of :term:`interfaces` which when we read from,
write to, or delete, will automatically manipulate the underlying XML
as needed. In addition, some of these interfaces may in turn reference
child objects which expose interfaces for particularly complex child
elements of the original XML chunk.
.. seealso::
:ref:`create-stanza-interfaces`.
Because the :mod:`~sleekxmpp.xmlstream.stanzabase` module was developed
as part of an `XMPP <http://xmpp.org>`_ library, these chunks of XML are
referred to as :term:`stanzas <stanza>`, and in SleekXMPP we refer to a
subclass of :class:`ElementBase` which defines the interfaces needed for
interacting with a given :term:`stanza` a :term:`stanza object`.
To make dealing with more complicated and nested :term:`stanzas <stanza>`
or XML chunks easier, :term:`stanza objects <stanza object>` can be
composed in two ways: as iterable child objects or as plugins. Iterable
child stanzas, or :term:`substanzas`, are accessible through a special
``'substanzas'`` interface. This option is useful for stanzas which
may contain more than one of the same kind of element. When there is
only one child element, the plugin method is more useful. For plugins,
a parent stanza object delegates one of its XML child elements to the
plugin stanza object. Here is an example:
.. code-block:: xml
<iq type="result">
<query xmlns="http://jabber.org/protocol/disco#info">
<identity category="client" type="bot" name="SleekXMPP Bot" />
</query>
</iq>
We can can arrange this stanza into two objects: an outer, wrapper object for
dealing with the ``<iq />`` element and its attributes, and a plugin object to
control the ``<query />`` payload element. If we give the plugin object the
name ``'disco_info'`` (using its :attr:`ElementBase.plugin_attrib` value), then
we can access the plugin as so::
>>> iq['disco_info']
'<query xmlns="http://jabber.org/protocol/disco#info">
<identity category="client" type="bot" name="SleekXMPP Bot" />
</query>'
We can then drill down through the plugin object's interfaces as desired::
>>> iq['disco_info']['identities']
[('client', 'bot', 'SleekXMPP Bot')]
Plugins may also add new interfaces to the parent stanza object as if they
had been defined by the parent directly, and can also override the behaviour
of an interface defined by the parent.
.. seealso::
- :ref:`create-stanza-plugins`
- :ref:`create-extension-plugins`
- :ref:`override-parent-interfaces`
Registering Stanza Plugins
--------------------------
.. autofunction:: register_stanza_plugin
ElementBase
-----------
.. autoclass:: ElementBase
:members:
:private-members:
:special-members:
StanzaBase
----------
.. autoclass:: StanzaBase
:members:

View File

@@ -0,0 +1,46 @@
.. module:: sleekxmpp.xmlstream.tostring
.. _tostring:
XML Serialization
=================
Since the XML layer of SleekXMPP is based on :mod:`~xml.etree.ElementTree`,
why not just use the built-in :func:`~xml.etree.ElementTree.tostring`
method? The answer is that using that method produces ugly results when
using namespaces. The :func:`tostring()` method used here intelligently
hides namespaces when able and does not introduce excessive namespace
prefixes::
>>> from sleekxmpp.xmlstream.tostring import tostring
>>> from xml.etree import cElementTree as ET
>>> xml = ET.fromstring('<foo xmlns="bar"><baz /></foo>')
>>> ET.tostring(xml)
'<ns0:foo xmlns:ns0="bar"><ns0:baz /></foo>'
>>> tostring(xml)
'<foo xmlns="bar"><baz /></foo>'
As a side effect of this namespace hiding, using :func:`tostring()` may
produce unexpected results depending on how the :func:`tostring()` method
is invoked. For example, when sending XML on the wire, the main XMPP
stanzas with their namespace of ``jabber:client`` will not include the
namespace because that is already declared by the stream header. But, if
you create a :class:`~sleekxmpp.stanza.message.Message` instance and dump
it to the terminal, the ``jabber:client`` namespace will appear.
.. autofunction:: tostring
Escaping Special Characters
---------------------------
In order to prevent errors when sending arbitrary text as the textual
content of an XML element, certain characters must be escaped. These
are: ``&``, ``<``, ``>``, ``"``, and ``'``. The default escaping
mechanism is to replace those characters with their equivalent escape
entities: ``&amp;``, ``&lt;``, ``&gt;``, ``&apos;``, and ``&quot;``.
In the future, the use of CDATA sections may be allowed to reduce the
size of escaped text or for when other XMPP processing agents do not
undertand these entities.
.. autofunction:: xml_escape

View File

@@ -0,0 +1,10 @@
==========
XML Stream
==========
.. module:: sleekxmpp.xmlstream.xmlstream
.. autoexception:: RestartStream
.. autoclass:: XMLStream
:members:

View File

@@ -17,21 +17,21 @@ of the tedium of creating/manipulating XML.
The Foundation: XMLStream
-------------------------
``XMLStream`` is a mostly XMPP-agnostic class whose purpose is to read
and write from a bi-directional XML stream. It also allows for callback
functions to execute when XML matching given patterns is received; these
callbacks are also referred to as :term:`stream handlers <stream handler>`.
The class also provides a basic eventing system which can be triggered
either manually or on a timed schedule.
:class:`~sleekxmpp.xmlstream.xmlstream.XMLStream` is a mostly XMPP-agnostic
class whose purpose is to read and write from a bi-directional XML stream.
It also allows for callback functions to execute when XML matching given
patterns is received; these callbacks are also referred to as :term:`stream
handlers <stream handler>`. The class also provides a basic eventing system
which can be triggered either manually or on a timed schedule.
The Main Threads
~~~~~~~~~~~~~~~~
``XMLStream`` instances run using at least three background threads: the
send thread, the read thread, and the scheduler thread. The send thread is
in charge of monitoring the send queue and writing text to the outgoing
XML stream. The read thread pulls text off of the incoming XML stream and
stores the results in an event queue. The scheduler thread is used to emit
events after a given period of time.
:class:`~sleekxmpp.xmlstream.xmlstream.XMLStream` instances run using at
least three background threads: the send thread, the read thread, and the
scheduler thread. The send thread is in charge of monitoring the send queue
and writing text to the outgoing XML stream. The read thread pulls text off
of the incoming XML stream and stores the results in an event queue. The
scheduler thread is used to emit events after a given period of time.
Additionally, the main event processing loop may be executed in its
own thread if SleekXMPP is being used in the background for another
@@ -61,9 +61,10 @@ when this bit of XML is received (with an assumed namespace of
new object is determined using a map of namespaced element names to
classes.
Our incoming XML is thus turned into a ``Message`` :term:`stanza object`
because the namespaced element name ``{jabber:client}message`` is
associated with the class ``sleekxmpp.stanza.Message``.
Our incoming XML is thus turned into a :class:`~sleekxmpp.stanza.Message`
:term:`stanza object` because the namespaced element name
``{jabber:client}message`` is associated with the class
:class:`~sleekxmpp.stanza.Message`.
2. **Match stanza objects to callbacks.**
@@ -72,8 +73,8 @@ when this bit of XML is received (with an assumed namespace of
:term:`stanza object` is paired with a reference to the handler and
placed into the event queue.
Our ``Message`` object is thus paired with the message stanza handler
``BaseXMPP._handle_message`` to create the tuple::
Our :class:`~sleekxmpp.stanza.Message` object is thus paired with the message stanza handler
:meth:`BaseXMPP._handle_message` to create the tuple::
('stanza', stanza_obj, handler)
@@ -88,7 +89,7 @@ when this bit of XML is received (with an assumed namespace of
parameter.
.. warning::
The callback, aka :term:`stream handler`, is executed in the main
The callback, aka :term:`stream handler`, is executed in the main event
processing thread. If the handler blocks, event processing will also
block.
@@ -96,20 +97,22 @@ when this bit of XML is received (with an assumed namespace of
Since a :term:`stream handler` shouldn't block, if extensive processing
for a stanza is required (such as needing to send and receive an
``Iq`` stanza), then custom events must be used. These events are not
explicitly tied to the incoming XML stream and may be raised at any
time. Importantly, these events may be handled in their own thread.
:class:`~sleekxmpp.stanza.Iq` stanza), then custom events must be used.
These events are not explicitly tied to the incoming XML stream and may
be raised at any time. Importantly, these events may be handled in their
own thread.
When the event is raised, a copy of the stanza is created for each
handler registered for the event. In contrast to :term:`stream handlers <stream handler>`,
these functions are referred to as :term:`event handlers <event handler>`.
Each stanza/handler pair is then put into the event queue.
handler registered for the event. In contrast to :term:`stream handlers
<stream handler>`, these functions are referred to as :term:`event
handlers <event handler>`. Each stanza/handler pair is then put into the
event queue.
.. note::
It is possible to skip the event queue and process an event immediately
by using ``direct=True`` when raising the event.
The code for ``BaseXMPP._handle_message`` follows this pattern, and
The code for :meth:`BaseXMPP._handle_message` follows this pattern, and
raises a ``'message'`` event::
self.event('message', msg)
@@ -145,125 +148,30 @@ when this bit of XML is received (with an assumed namespace of
Raising XMPP Awareness: BaseXMPP
--------------------------------
While ``XMLStream`` attempts to shy away from anything too XMPP specific,
``BaseXMPP``'s sole purpose is to provide foundational support for sending
and receiving XMPP stanzas. This support includes registering the basic
message, presence, and iq stanzas, methods for creating and sending
stanzas, and default handlers for incoming messages and keeping track of
presence notifications.
While :class:`~sleekxmpp.xmlstream.xmlstream.XMLStream` attempts to shy away
from anything too XMPP specific, :class:`~sleekxmpp.basexmpp.BaseXMPP`'s
sole purpose is to provide foundational support for sending and receiving
XMPP stanzas. This support includes registering the basic message,
presence, and iq stanzas, methods for creating and sending stanzas, and
default handlers for incoming messages and keeping track of presence
notifications.
The plugin system for adding new XEP support is also maintained by
``BaseXMPP``.
:class:`~sleekxmpp.basexmpp.BaseXMPP`.
.. index:: ClientXMPP, BaseXMPP
ClientXMPP
----------
``ClientXMPP`` extends ``BaseXMPP`` with additional logic for connecting to
an XMPP server by performing DNS lookups. It also adds support for stream
:class:`~sleekxmpp.clientxmpp.ClientXMPP` extends
:class:`~sleekxmpp.clientxmpp.BaseXMPP` with additional logic for connecting
to an XMPP server by performing DNS lookups. It also adds support for stream
features such as STARTTLS and SASL.
.. index:: ComponentXMPP, BaseXMPP
ComponentXMPP
-------------
``ComponentXMPP`` is only a thin layer on top of ``BaseXMPP`` that
implements the component handshake protocol.
.. index::
double: object; stanza
Stanza Objects: A Brief Look
----------------------------
.. seealso::
See :ref:`api-stanza-objects` for a more detailed overview.
Almost worthy of their own standalone library, :term:`stanza objects <stanza object>`
are wrappers for XML objects which expose dictionary like interfaces
for manipulating their XML content. For example, consider the XML:
.. code-block:: xml
<message />
A very plain element to start with, but we can create a :term:`stanza object`
using ``sleekxmpp.stanza.Message`` as so::
msg = Message(xml=ET.fromstring("<message />"))
The ``Message`` stanza class defines interfaces such as ``'body'`` and
``'to'``, so we can assign values to those interfaces to include new XML
content::
msg['body'] = "Following so far?"
msg['to'] = 'user@example.com'
Dumping the XML content of ``msg`` (using ``msg.xml``), we find:
.. code-block:: xml
<message to="user@example.com">
<body>Following so far?</body>
</message>
The process is similar for reading from interfaces and deleting interface
contents. A :term:`stanza object` behaves very similarly to a regular
``dict`` object: you may assign to keys, read from keys, and ``del`` keys.
Stanza interfaces come with built-in behaviours such as adding/removing
attribute and sub element values. However, a lot of the time more custom
logic is needed. This can be provided by defining methods of the form
``get_*``, ``set_*``, and ``del_*`` for any interface which requires custom
behaviour.
Stanza Plugins
~~~~~~~~~~~~~~
Since it is generally possible to embed one XML element inside another,
:term:`stanza objects <stanza object>` may be nested. Nested
:term:`stanza objects <stanza object>` are referred to as :term:`stanza plugins <stanza plugin>`
or :term:`substanzas <substanza>`.
A :term:`stanza plugin` exposes its own interfaces by adding a new
interface to its parent stanza. To demonstrate, consider these two stanza
class definitions using ``sleekxmpp.xmlstream.ElementBase``:
.. code-block:: python
class Parent(ElementBase):
name = "the-parent-xml-element-name"
namespace = "the-parent-namespace"
interfaces = set(('foo', 'bar'))
class Child(ElementBase):
name = "the-child-xml-element-name"
namespace = "the-child-namespace"
plugin_attrib = 'child'
interfaces = set(('baz',))
If we register the ``Child`` stanza as a plugin of the ``Parent`` stanza as
so, using ``sleekxmpp.xmlstream.register_stanza_plugin``::
register_stanza_plugin(Parent, Child)
Then we can access content in the child stanza through the parent.
Note that the interface used to access the child stanza is the same as
``Child.plugin_attrib``::
parent = Parent()
parent['foo'] = 'a'
parent['child']['baz'] = 'b'
The above code would produce:
.. code-block:: xml
<the-parent-xml-element xmlns="the-parent-namespace" foo="a">
<the-child-xml-element xmlsn="the-child-namespace" baz="b" />
</the-parent-xml-element>
It is also possible to allow a :term:`substanza` to appear multiple times
by using ``iterable=True`` in the ``register_stanza_plugin`` call. All
iterable :term:`substanzas <substanza>` can be accessed using a standard
``substanzas`` interface.
:class:`~sleekxmpp.componentxmpp.ComponentXMPP` is only a thin layer on top of
:class:`~sleekxmpp.basexmpp.BaseXMPP` that implements the component handshake
protocol.

View File

@@ -16,7 +16,7 @@ import sys, os
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#sys.path.insert(0, os.path.abspath('.'))
sys.path.insert(0, os.path.abspath('..'))
# -- General configuration -----------------------------------------------------
@@ -25,7 +25,7 @@ import sys, os
# Add any Sphinx extension module names here, as strings. They can be extensions
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
extensions = ['sphinx.ext.autodoc', 'sphinx.ext.viewcode']
extensions = ['sphinx.ext.autodoc', 'sphinx.ext.viewcode', 'sphinx.ext.intersphinx']
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
@@ -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.0RC1'
release = '1.0'
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
@@ -81,7 +81,7 @@ exclude_patterns = ['_build']
#show_authors = False
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'default'
pygments_style = 'tango'
# A list of ignored prefixes for module index sorting.
#modindex_common_prefix = []
@@ -218,3 +218,5 @@ man_pages = [
('index', 'sleekxmpp', u'SleekXMPP Documentation',
[u'Nathan Fritz, Lance Stout'], 1)
]
intersphinx_mapping = {'python': ('http://docs.python.org/3.2', 'python-objects.inv')}

View File

@@ -1,3 +1,5 @@
.. _create-plugin:
Creating a SleekXMPP Plugin
===========================

View File

@@ -1,2 +1,75 @@
.. _echocomponent:
=================================
Create and Run a Server Component
=================================
.. note::
If you have any issues working through this quickstart guide
or the other tutorials here, please either send a message to the
`mailing list <http://groups.google.com/group/sleekxmpp-discussion>`_
or join the chat room at `sleek@conference.jabber.org
<xmpp:sleek@conference.jabber.org?join>`_.
If you have not yet installed SleekXMPP, do so now by either checking out a version
from `Github <http://github.com/fritzy/SleekXMPP>`_, or installing it using ``pip``
or ``easy_install``.
.. code-block:: sh
pip install sleekxmpp # Or: easy_install sleekxmpp
Many XMPP applications eventually graduate to requiring to run as a server
component in order to meet scalability requirements. To demonstrate how to
turn an XMPP client bot into a component, we'll turn the echobot example
(:ref:`echobot`) into a component version.
The first difference is that we will add an additional import statement:
.. code-block:: python
from sleekxmpp.componentxmpp import ComponentXMPP
Likewise, we will change the bot's class definition to match:
.. code-block:: python
class EchoComponent(ComponentXMPP):
def __init__(self, jid, secret, server, port):
ComponentXMPP.__init__(self, jid, secret, server, port)
A component instance requires two extra parameters compared to a client
instance: ``server`` and ``port``. These specifiy the name and port of
the XMPP server that will be accepting the component. For example, for
a MUC component, the following could be used:
.. code-block:: python
muc = ComponentXMPP('muc.sleekxmpp.com', '******', 'sleekxmpp.com', 5555)
.. note::
The ``server`` value is **NOT** derived from the provided JID for the
component, unlike with client connections.
One difference with the component version is that we do not have
to handle the :term:`session_start` event if we don't wish to deal
with presence.
The other, main difference with components is that the
``'from'`` value for every stanza must be explicitly set, since
components may send stanzas from multiple JIDs. To do so,
the :meth:`~sleekxmpp.basexmpp.BaseXMPP.send_message()` and
:meth:`~sleekxmpp.basexmpp.BaseXMPP.send_presence()` accept the parameters
``mfrom`` and ``pfrom``, respectively. For any method that uses
:class:`~sleekxmpp.stanza.iq.Iq` stanzas, ``ifrom`` may be used.
Final Product
-------------
.. include:: ../../examples/echo_component.py
:literal:

View File

@@ -1,2 +1,182 @@
Send/Receive IQ Stanzas
=======================
Unlike :class:`~sleekxmpp.stanza.message.Message` and
:class:`~sleekxmpp.stanza.presence.Presence` stanzas which only use
text data for basic usage, :class:`~sleekxmpp.stanza.iq.Iq` stanzas
require using XML payloads, and generally entail creating a new
SleekXMPP plugin to provide the necessary convenience methods to
make working with them easier.
Basic Use
---------
XMPP's use of :class:`~sleekxmpp.stanza.iq.Iq` stanzas is built around
namespaced ``<query />`` elements. For clients, just sending the
empty ``<query />`` element will suffice for retrieving information. For
example, a very basic implementation of service discovery would just
need to be able to send:
.. code-block:: xml
<iq to="user@example.com" type="get" id="1">
<query xmlns="http://jabber.org/protocol/disco#info" />
</iq>
Creating Iq Stanzas
~~~~~~~~~~~~~~~~~~~
SleekXMPP provides built-in support for creating basic :class:`~sleekxmpp.stanza.iq.Iq`
stanzas this way. The relevant methods are:
* :meth:`~sleekxmpp.basexmpp.BaseXMPP.make_iq`
* :meth:`~sleekxmpp.basexmpp.BaseXMPP.make_iq_get`
* :meth:`~sleekxmpp.basexmpp.BaseXMPP.make_iq_set`
* :meth:`~sleekxmpp.basexmpp.BaseXMPP.make_iq_result`
* :meth:`~sleekxmpp.basexmpp.BaseXMPP.make_iq_error`
* :meth:`~sleekxmpp.basexmpp.BaseXMPP.make_iq_query`
These methods all follow the same pattern: create or modify an existing
:class:`~sleekxmpp.stanza.iq.Iq` stanza, set the ``'type'`` value based
on the method name, and finally add a ``<query />`` element with the given
namespace. For example, to produce the query above, you would use:
.. code-block:: python
self.make_iq_get(queryxmlns='http://jabber.org/protocol/disco#info',
ito='user@example.com')
Sending Iq Stanzas
~~~~~~~~~~~~~~~~~~
Once an :class:`~sleekxmpp.stanza.iq.Iq` stanza is created, sending it
over the wire is done using its :meth:`~sleekxmpp.stanza.iq.Iq.send()`
method, like any other stanza object. However, there are a few extra
options to control how to wait for the query's response.
These options are:
* ``block``: The default behaviour is that :meth:`~sleekxmpp.stanza.iq.Iq.send()`
will block until a response is received and the response stanza will be the
return value. Setting ``block`` to ``False`` will cause the call to return
immediately. In which case, you will need to arrange some way to capture
the response stanza if you need it.
* ``timeout``: When using the blocking behaviour, the call will eventually
timeout with an error. The default timeout is 30 seconds, but this may
be overidden two ways. To change the timeout globally, set:
.. code-block:: python
self.response_timeout = 10
To change the timeout for a single call, the ``timeout`` parameter works:
.. code-block:: python
iq.send(timeout=60)
* ``callback``: When not using a blocking call, using the ``callback``
argument is a simple way to register a handler that will execute
whenever a response is finally received. Using this method, there
is no timeout limit. In case you need to remove the callback, the
name of the newly created callback is returned.
.. code-block:: python
cb_name = iq.send(callback=self.a_callback)
# ... later if we need to cancel
self.remove_handler(cb_name)
Properly working with :class:`~sleekxmpp.stanza.iq.Iq` stanzas requires
handling the intended, normal flow, error responses, and timed out
requests. To make this easier, two exceptions may be thrown by
:meth:`~sleekxmpp.stanza.iq.Iq.send()`: :exc:`~sleekxmpp.exceptions.IqError`
and :exc:`~sleekxmpp.exceptions.IqTimeout`. These exceptions only
apply to the default, blocking calls.
.. code-block:: python
try:
resp = iq.send()
# ... do stuff with expected Iq result
except IqError as e:
err_resp = e.iq
# ... handle error case
except IqTimeout:
# ... no response received in time
pass
If you do not care to distinguish between errors and timeouts, then you
can combine both cases with a generic :exc:`~sleekxmpp.exceptions.XMPPError`
exception:
.. code-block:: python
try:
resp = iq.send()
except XMPPError:
# ... Don't care about the response
pass
Advanced Use
------------
Going beyond the basics provided by SleekXMPP requires building at least a
rudimentary SleekXMPP plugin to create a :term:`stanza object` for
interfacting with the :class:`~sleekxmpp.stanza.iq.Iq` payload.
.. seealso::
* :ref:`create-plugin`
* :ref:`work-with-stanzas`
* :ref:`using-handlers-matchers`
The typical way to respond to :class:`~sleekxmpp.stanza.iq.Iq` requests is
to register stream handlers. As an example, suppose we create a stanza class
named ``CustomXEP`` which uses the XML element ``<query xmlns="custom-xep" />``,
and has a :attr:`~sleekxmpp.xmlstream.stanzabase.ElementBase.plugin_attrib` value
of ``custom_xep``.
There are two types of incoming :class:`~sleekxmpp.stanza.iq.Iq` requests:
``get`` and ``set``. You can register a handler that will accept both and then
filter by type as needed, as so:
.. code-block:: python
self.register_handler(Callback(
'CustomXEP Handler',
StanzaPath('iq/custom_xep'),
self._handle_custom_iq))
# ...
def _handle_custom_iq(self, iq):
if iq['type'] == 'get':
# ...
pass
elif iq['type'] == 'set':
# ...
pass
else:
# ... This will capture error responses too
pass
If you want to filter out query types beforehand, you can adjust the matching
filter by using ``@type=get`` or ``@type=set`` if you are using the recommended
:class:`~sleekxmpp.xmlstream.matcher.stanzapath.StanzaPath` matcher.
.. code-block:: python
self.register_handler(Callback(
'CustomXEP Handler',
StanzaPath('iq@type=get/custom_xep'),
self._handle_custom_iq_get))
# ...
def _handle_custom_iq_get(self, iq):
assert(iq['type'] == 'get')

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

@@ -1,2 +1,42 @@
.. _proxy:
=========================
Enable HTTP Proxy Support
=========================
.. note::
If you have any issues working through this quickstart guide
or the other tutorials here, please either send a message to the
`mailing list <http://groups.google.com/group/sleekxmpp-discussion>`_
or join the chat room at `sleek@conference.jabber.org
<xmpp:sleek@conference.jabber.org?join>`_.
In some instances, you may wish to route XMPP traffic through
an HTTP proxy, probably to get around restrictive firewalls.
SleekXMPP provides support for basic HTTP proxying with DIGEST
authentication.
Enabling proxy support is done in two steps. The first is to instruct SleekXMPP
to use a proxy, and the second is to configure the proxy details:
.. code-block:: python
xmpp = ClientXMPP(...)
xmpp.use_proxy = True
xmpp.proxy_config = {
'host': 'proxy.example.com',
'port': 5555,
'username': 'example_user',
'password': '******'
}
The ``'username'`` and ``'password'`` fields are optional if the proxy does not
require authentication.
The Final Product
-----------------
.. include:: ../../examples/proxy_echo_client.py
:literal:

View File

@@ -1,2 +1,4 @@
.. _using-handlers-matchers:
Using Stream Handlers and Matchers
==================================

30
docs/howto/stanzas.rst Normal file
View File

@@ -0,0 +1,30 @@
.. _work-with-stanzas:
How to Work with Stanza Objects
===============================
.. _create-stanza-interfaces:
Defining Stanza Interfaces
--------------------------
.. _create-stanza-plugins:
Creating Stanza Plugins
-----------------------
.. _create-extension-plugins:
Creating a Stanza Extension
---------------------------
.. _override-parent-interfaces:
Overriding a Parent Stanza
--------------------------

View File

@@ -12,13 +12,8 @@ SleekXMPP
``master`` branch, while the latest development version is in the
``develop`` branch.
**Stable Releases**
- `1.0 Beta6.1 <http://github.com/fritzy/SleekXMPP/zipball/1.0-Beta6.1>`_
- `1.0 Beta5 <http://github.com/fritzy/SleekXMPP/zipball/1.0-Beta5>`_
- `1.0 Beta4 <http://github.com/fritzy/SleekXMPP/zipball/1.0-Beta4>`_
- `1.0 Beta3 <http://github.com/fritzy/SleekXMPP/zipball/1.0-Beta3>`_
- `1.0 Beta2 <http://github.com/fritzy/SleekXMPP/zipball/1.0-Beta2>`_
- `1.0 Beta1 <http://github.com/fritzy/SleekXMPP/zipball/1.0-Beta1>`_
**Latest Stable Release**
- `1.0 <http://github.com/fritzy/SleekXMPP/zipball/1.0>`_
**Develop Releases**
- `Latest Develop Version <http://github.com/fritzy/SleekXMPP/zipball/develop>`_
@@ -64,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::
@@ -84,8 +145,10 @@ Tutorials, FAQs, and How To Guides
.. toctree::
:maxdepth: 1
faq
xeps
xmpp_tdg
howto/stanzas
create_plugin
features
sasl
@@ -113,8 +176,35 @@ API Reference
event_index
api/clientxmpp
api/componentxmpp
api/basexmpp
api/xmlstream
api/exceptions
api/xmlstream/jid
api/xmlstream/stanzabase
api/xmlstream/handler
api/xmlstream/matcher
api/xmlstream/xmlstream
api/xmlstream/scheduler
api/xmlstream/tostring
api/xmlstream/filesocket
Core Stanzas
~~~~~~~~~~~~
.. toctree::
:maxdepth: 2
api/stanza/rootstanza
api/stanza/message
api/stanza/presence
api/stanza/iq
api/stanza/error
api/stanza/stream_error
Plugins
~~~~~~~
.. toctree::
:maxdepth: 2
Additional Info
---------------
@@ -132,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>`_)

BIN
docs/python-objects.inv Normal file

Binary file not shown.

View File

@@ -1,2 +1,50 @@
Supported XEPS
==============
======= ============================= ================
XEP Description Notes
======= ============================= ================
`0004`_ Data forms
`0009`_ Jabber RPC
`0012`_ Last Activity
`0030`_ Service Discovery
`0033`_ Extended Stanza Addressing
`0045`_ Multi-User Chat (MUC) Client-side only
`0050`_ Ad-hoc Commands
`0059`_ Result Set Management
`0060`_ Publish/Subscribe (PubSub) Client-side only
`0066`_ Out-of-band Data
`0078`_ Non-SASL Authentication
`0082`_ XMPP Date and Time Profiles
`0085`_ Chat-State Notifications
`0086`_ Error Condition Mappings
`0092`_ Software Version
`0128`_ Service Discovery Extensions
`0202`_ Entity Time
`0203`_ Delayed Delivery
`0224`_ Attention
`0249`_ Direct MUC Invitations
======= ============================= ================
.. _0004: http://xmpp.org/extensions/xep-0004.html
.. _0009: http://xmpp.org/extensions/xep-0009.html
.. _0012: http://xmpp.org/extensions/xep-0012.html
.. _0030: http://xmpp.org/extensions/xep-0030.html
.. _0033: http://xmpp.org/extensions/xep-0033.html
.. _0045: http://xmpp.org/extensions/xep-0045.html
.. _0050: http://xmpp.org/extensions/xep-0050.html
.. _0059: http://xmpp.org/extensions/xep-0059.html
.. _0060: http://xmpp.org/extensions/xep-0060.html
.. _0066: http://xmpp.org/extensions/xep-0066.html
.. _0078: http://xmpp.org/extensions/xep-0078.html
.. _0082: http://xmpp.org/extensions/xep-0082.html
.. _0085: http://xmpp.org/extensions/xep-0085.html
.. _0086: http://xmpp.org/extensions/xep-0086.html
.. _0092: http://xmpp.org/extensions/xep-0092.html
.. _0128: http://xmpp.org/extensions/xep-0128.html
.. _0199: http://xmpp.org/extensions/xep-0199.html
.. _0202: http://xmpp.org/extensions/xep-0202.html
.. _0203: http://xmpp.org/extensions/xep-0203.html
.. _0224: http://xmpp.org/extensions/xep-0224.html
.. _0249: http://xmpp.org/extensions/xep-0249.html

View File

@@ -11,7 +11,6 @@
import sys
import logging
import time
import getpass
from optparse import OptionParser
@@ -41,7 +40,7 @@ class CommandBot(sleekxmpp.ClientXMPP):
# The session_start event will be triggered when
# the bot establishes its connection with the server
# and the XML streams are ready for use. We want to
# listen for this event so that we we can intialize
# listen for this event so that we we can initialize
# our roster.
self.add_event_handler("session_start", self.start)
@@ -50,7 +49,7 @@ class CommandBot(sleekxmpp.ClientXMPP):
Process the session_start event.
Typical actions for the session_start event are
requesting the roster and broadcasting an intial
requesting the roster and broadcasting an initial
presence stanza.
Arguments:
@@ -72,7 +71,7 @@ class CommandBot(sleekxmpp.ClientXMPP):
def _handle_command(self, iq, session):
"""
Respond to the intial request for a command.
Respond to the initial request for a command.
Arguments:
iq -- The iq stanza containing the command request.
@@ -192,14 +191,14 @@ if __name__ == '__main__':
# Connect to the XMPP server and start processing XMPP stanzas.
if xmpp.connect():
# If you do not have the pydns library installed, you will need
# If you do not have the dnspython library installed, you will need
# to manually specify the name of the server if it does not match
# the one in the JID. For example, to use Google Talk you would
# need to use:
#
# if xmpp.connect(('talk.google.com', 5222)):
# ...
xmpp.process(threaded=False)
xmpp.process(block=True)
print("Done")
else:
print("Unable to connect.")

View File

@@ -11,7 +11,6 @@
import sys
import logging
import time
import getpass
from optparse import OptionParser
@@ -44,7 +43,7 @@ class CommandUserBot(sleekxmpp.ClientXMPP):
# The session_start event will be triggered when
# the bot establishes its connection with the server
# and the XML streams are ready for use. We want to
# listen for this event so that we we can intialize
# listen for this event so that we we can initialize
# our roster.
self.add_event_handler("session_start", self.start)
self.add_event_handler("message", self.message)
@@ -54,7 +53,7 @@ class CommandUserBot(sleekxmpp.ClientXMPP):
Process the session_start event.
Typical actions for the session_start event are
requesting the roster and broadcasting an intial
requesting the roster and broadcasting an initial
presence stanza.
Arguments:
@@ -198,14 +197,14 @@ if __name__ == '__main__':
# Connect to the XMPP server and start processing XMPP stanzas.
if xmpp.connect():
# If you do not have the pydns library installed, you will need
# If you do not have the dnspython library installed, you will need
# to manually specify the name of the server if it does not match
# the one in the JID. For example, to use Google Talk you would
# need to use:
#
# if xmpp.connect(('talk.google.com', 5222)):
# ...
xmpp.process(threaded=False)
xmpp.process(block=True)
print("Done")
else:
print("Unable to connect.")

View File

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

View File

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

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

@@ -10,7 +10,6 @@
"""
import sys
import time
import logging
import getpass
from optparse import OptionParser
@@ -61,16 +60,16 @@ class Disco(sleekxmpp.ClientXMPP):
# The session_start event will be triggered when
# the bot establishes its connection with the server
# and the XML streams are ready for use. We want to
# listen for this event so that we we can intialize
# listen for this event so that we we can initialize
# our roster.
self.add_event_handler("session_start", self.start)
self.add_event_handler("session_start", self.start, 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 intial
requesting the roster and broadcasting an initial
presence stanza.
In this case, we send disco#info and disco#items
@@ -188,13 +187,13 @@ if __name__ == '__main__':
# Connect to the XMPP server and start processing XMPP stanzas.
if xmpp.connect():
# If you do not have the pydns library installed, you will need
# If you do not have the dnspython library installed, you will need
# to manually specify the name of the server if it does not match
# the one in the JID. For example, to use Google Talk you would
# need to use:
#
# if xmpp.connect(('talk.google.com', 5222)):
# ...
xmpp.process(threaded=False)
xmpp.process(block=True)
else:
print("Unable to connect.")

View File

@@ -11,7 +11,6 @@
import sys
import logging
import time
import getpass
from optparse import OptionParser
@@ -41,7 +40,7 @@ class EchoBot(sleekxmpp.ClientXMPP):
# The session_start event will be triggered when
# the bot establishes its connection with the server
# and the XML streams are ready for use. We want to
# listen for this event so that we we can intialize
# listen for this event so that we we can initialize
# our roster.
self.add_event_handler("session_start", self.start)
@@ -55,7 +54,7 @@ class EchoBot(sleekxmpp.ClientXMPP):
Process the session_start event.
Typical actions for the session_start event are
requesting the roster and broadcasting an intial
requesting the roster and broadcasting an initial
presence stanza.
Arguments:
@@ -132,14 +131,14 @@ if __name__ == '__main__':
# Connect to the XMPP server and start processing XMPP stanzas.
if xmpp.connect():
# If you do not have the pydns library installed, you will need
# If you do not have the dnspython library installed, you will need
# to manually specify the name of the server if it does not match
# the one in the JID. For example, to use Google Talk you would
# need to use:
#
# if xmpp.connect(('talk.google.com', 5222)):
# ...
xmpp.process(threaded=False)
xmpp.process(block=True)
print("Done")
else:
print("Unable to connect.")

122
examples/echo_component.py Executable file
View File

@@ -0,0 +1,122 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
SleekXMPP: The Sleek XMPP Library
Copyright (C) 2010 Nathanael C. Fritz
This file is part of SleekXMPP.
See the file LICENSE for copying permission.
"""
import sys
import logging
import getpass
from optparse import OptionParser
import sleekxmpp
from sleekxmpp.componentxmpp import ComponentXMPP
# Python versions before 3.0 do not use UTF-8 encoding
# by default. To ensure that Unicode is handled properly
# throughout SleekXMPP, we will set the default encoding
# ourselves to UTF-8.
if sys.version_info < (3, 0):
reload(sys)
sys.setdefaultencoding('utf8')
else:
raw_input = input
class EchoComponent(ComponentXMPP):
"""
A simple SleekXMPP component that echoes messages.
"""
def __init__(self, jid, secret, server, port):
ComponentXMPP.__init__(self, jid, secret, server, port)
# You don't need a session_start handler, but that is
# where you would broadcast initial presence.
# The message event is triggered whenever a message
# stanza is received. Be aware that that includes
# MUC messages and error messages.
self.add_event_handler("message", self.message)
def message(self, msg):
"""
Process incoming message stanzas. Be aware that this also
includes MUC messages and error messages. It is usually
a good idea to check the messages's type before processing
or sending replies.
Since a component may send messages from any number of JIDs,
it is best to always include a from JID.
Arguments:
msg -- The received message stanza. See the documentation
for stanza objects and the Message stanza to see
how it may be used.
"""
# The reply method will use the messages 'to' JID as the
# outgoing reply's 'from' JID.
msg.reply("Thanks for sending\n%(body)s" % msg).send()
if __name__ == '__main__':
# Setup the command line arguments.
optp = OptionParser()
# Output verbosity options.
optp.add_option('-q', '--quiet', help='set logging to ERROR',
action='store_const', dest='loglevel',
const=logging.ERROR, default=logging.INFO)
optp.add_option('-d', '--debug', help='set logging to DEBUG',
action='store_const', dest='loglevel',
const=logging.DEBUG, default=logging.INFO)
optp.add_option('-v', '--verbose', help='set logging to COMM',
action='store_const', dest='loglevel',
const=5, default=logging.INFO)
# JID and password options.
optp.add_option("-j", "--jid", dest="jid",
help="JID to use")
optp.add_option("-p", "--password", dest="password",
help="password to use")
optp.add_option("-s", "--server", dest="server",
help="server to connect to")
optp.add_option("-P", "--port", dest="port",
help="port to connect to")
opts, args = optp.parse_args()
if opts.jid is None:
opts.jid = raw_input("Component JID: ")
if opts.password is None:
opts.password = getpass.getpass("Password: ")
if opts.server is None:
opts.server = raw_input("Server: ")
if opts.port is None:
opts.port = int(raw_input("Port: "))
# Setup logging.
logging.basicConfig(level=opts.loglevel,
format='%(levelname)-8s %(message)s')
# Setup the EchoComponent and register plugins. Note that while plugins
# may have interdependencies, the order in which you register them does
# not matter.
xmpp = EchoComponent(opts.jid, opts.password, opts.server, opts.port)
xmpp.registerPlugin('xep_0030') # Service Discovery
xmpp.registerPlugin('xep_0004') # Data Forms
xmpp.registerPlugin('xep_0060') # PubSub
xmpp.registerPlugin('xep_0199') # XMPP Ping
# Connect to the XMPP server and start processing XMPP stanzas.
if xmpp.connect():
xmpp.process(block=True)
print("Done")
else:
print("Unable to connect.")

View File

@@ -11,7 +11,7 @@
import sys
import logging
import time
import getpass
from optparse import OptionParser
import sleekxmpp
@@ -44,7 +44,7 @@ class MUCBot(sleekxmpp.ClientXMPP):
# The session_start event will be triggered when
# the bot establishes its connection with the server
# and the XML streams are ready for use. We want to
# listen for this event so that we we can intialize
# listen for this event so that we we can initialize
# our roster.
self.add_event_handler("session_start", self.start)
@@ -68,7 +68,7 @@ class MUCBot(sleekxmpp.ClientXMPP):
Process the session_start event.
Typical actions for the session_start event are
requesting the roster and broadcasting an intial
requesting the roster and broadcasting an initial
presence stanza.
Arguments:
@@ -76,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:
@@ -161,9 +161,14 @@ if __name__ == '__main__':
logging.basicConfig(level=opts.loglevel,
format='%(levelname)-8s %(message)s')
if None in [opts.jid, opts.password, opts.room, opts.nick]:
optp.print_help()
sys.exit(1)
if opts.jid is None:
opts.jid = raw_input("Username: ")
if opts.password is None:
opts.password = getpass.getpass("Password: ")
if opts.room is None:
opts.room = raw_input("MUC room: ")
if opts.nick is None:
opts.nick = raw_input("MUC nickname: ")
# Setup the MUCBot and register plugins. Note that while plugins may
# have interdependencies, the order in which you register them does
@@ -175,14 +180,14 @@ if __name__ == '__main__':
# Connect to the XMPP server and start processing XMPP stanzas.
if xmpp.connect():
# If you do not have the pydns library installed, you will need
# If you do not have the dnspython library installed, you will need
# to manually specify the name of the server if it does not match
# the one in the JID. For example, to use Google Talk you would
# need to use:
#
# if xmpp.connect(('talk.google.com', 5222)):
# ...
xmpp.process(threaded=False)
xmpp.process(block=True)
print("Done")
else:
print("Unable to connect.")

View File

@@ -11,7 +11,6 @@
import sys
import logging
import time
import getpass
from optparse import OptionParser
@@ -44,16 +43,16 @@ class PingTest(sleekxmpp.ClientXMPP):
# The session_start event will be triggered when
# the bot establishes its connection with the server
# and the XML streams are ready for use. We want to
# listen for this event so that we we can intialize
# listen for this event so that we we can initialize
# our roster.
self.add_event_handler("session_start", self.start)
self.add_event_handler("session_start", self.start, 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 intial
requesting the roster and broadcasting an initial
presence stanza.
Arguments:
@@ -72,7 +71,7 @@ class PingTest(sleekxmpp.ClientXMPP):
self.disconnect()
sys.exit(1)
else:
logging.info("Success! RTT: %s" % str(result))
logging.info("Success! RTT: %s", str(result))
self.disconnect()
@@ -129,14 +128,14 @@ if __name__ == '__main__':
# Connect to the XMPP server and start processing XMPP stanzas.
if xmpp.connect():
# If you do not have the pydns library installed, you will need
# If you do not have the dnspython library installed, you will need
# to manually specify the name of the server if it does not match
# the one in the JID. For example, to use Google Talk you would
# need to use:
#
# if xmpp.connect(('talk.google.com', 5222)):
# ...
xmpp.process(threaded=False)
xmpp.process(block=True)
print("Done")
else:
print("Unable to connect.")

View File

@@ -11,7 +11,6 @@
import sys
import logging
import time
import getpass
from optparse import OptionParser
@@ -41,7 +40,7 @@ class EchoBot(sleekxmpp.ClientXMPP):
# The session_start event will be triggered when
# the bot establishes its connection with the server
# and the XML streams are ready for use. We want to
# listen for this event so that we we can intialize
# listen for this event so that we we can initialize
# our roster.
self.add_event_handler("session_start", self.start)
@@ -55,7 +54,7 @@ class EchoBot(sleekxmpp.ClientXMPP):
Process the session_start event.
Typical actions for the session_start event are
requesting the roster and broadcasting an intial
requesting the roster and broadcasting an initial
presence stanza.
Arguments:
@@ -156,14 +155,14 @@ if __name__ == '__main__':
# Connect to the XMPP server and start processing XMPP stanzas.
if xmpp.connect():
# If you do not have the pydns library installed, you will need
# If you do not have the dnspython library installed, you will need
# to manually specify the name of the server if it does not match
# the one in the JID. For example, to use Google Talk you would
# need to use:
#
# if xmpp.connect(('talk.google.com', 5222)):
# ...
xmpp.process(threaded=False)
xmpp.process(block=True)
print("Done")
else:
print("Unable to connect.")

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

@@ -10,7 +10,6 @@
"""
import sys
import time
import logging
import getpass
import threading
@@ -43,7 +42,7 @@ class RosterBrowser(sleekxmpp.ClientXMPP):
# The session_start event will be triggered when
# the bot establishes its connection with the server
# and the XML streams are ready for use. We want to
# listen for this event so that we we can intialize
# listen for this event so that we we can initialize
# our roster. We need threaded=True so that the
# session_start handler doesn't block event processing
# while we wait for presence stanzas to arrive.
@@ -58,7 +57,7 @@ class RosterBrowser(sleekxmpp.ClientXMPP):
Process the session_start event.
Typical actions for the session_start event are
requesting the roster and broadcasting an intial
requesting the roster and broadcasting an initial
presence stanza.
Arguments:
@@ -160,14 +159,14 @@ if __name__ == '__main__':
# Connect to the XMPP server and start processing XMPP stanzas.
if xmpp.connect():
# If you do not have the pydns library installed, you will need
# If you do not have the dnspython library installed, you will need
# to manually specify the name of the server if it does not match
# the one in the JID. For example, to use Google Talk you would
# need to use:
#
# if xmpp.connect(('talk.google.com', 5222)):
# ...
xmpp.process(threaded=False)
xmpp.process(block=True)
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

@@ -11,7 +11,6 @@
import sys
import logging
import time
import getpass
from optparse import OptionParser
@@ -46,16 +45,16 @@ class SendMsgBot(sleekxmpp.ClientXMPP):
# The session_start event will be triggered when
# the bot establishes its connection with the server
# and the XML streams are ready for use. We want to
# listen for this event so that we we can intialize
# listen for this event so that we we can initialize
# our roster.
self.add_event_handler("session_start", self.start)
self.add_event_handler("session_start", self.start, 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 intial
requesting the roster and broadcasting an initial
presence stanza.
Arguments:
@@ -131,14 +130,14 @@ if __name__ == '__main__':
# Connect to the XMPP server and start processing XMPP stanzas.
if xmpp.connect():
# If you do not have the pydns library installed, you will need
# If you do not have the dnspython library installed, you will need
# to manually specify the name of the server if it does not match
# the one in the JID. For example, to use Google Talk you would
# need to use:
#
# if xmpp.connect(('talk.google.com', 5222)):
# ...
xmpp.process(threaded=False)
xmpp.process(block=True)
print("Done")
else:
print("Unable to connect.")

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,7 +8,11 @@
# file, which you should have received as part of this distribution.
import sys
from distutils.core import setup, Command
import codecs
try:
from setuptools import setup, Command
except ImportError:
from distutils.core import setup, Command
# from ez_setup import use_setuptools
from testall import TestCommand
@@ -28,16 +32,16 @@ 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',
'License :: OSI Approved :: MIT License',
'Programming Language :: Python',
'Programming Language :: Python 2.6',
'Programming Language :: Python 2.7',
'Programming Language :: Python 3.1',
'Programming Language :: Python 3.2',
'Programming Language :: Python :: 2.6',
'Programming Language :: Python :: 2.7',
'Programming Language :: Python :: 3.1',
'Programming Language :: Python :: 3.2',
'Topic :: Software Development :: Libraries :: Python Modules',
]
@@ -53,22 +57,37 @@ 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_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_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_0153',
'sleekxmpp/plugins/xep_0172',
'sleekxmpp/plugins/xep_0184',
'sleekxmpp/plugins/xep_0198',
'sleekxmpp/plugins/xep_0199',
'sleekxmpp/plugins/xep_0202',
'sleekxmpp/plugins/xep_0203',
'sleekxmpp/plugins/xep_0224',
'sleekxmpp/plugins/xep_0231',
'sleekxmpp/plugins/xep_0249',
'sleekxmpp/features',
'sleekxmpp/features/feature_mechanisms',
@@ -76,6 +95,7 @@ packages = [ 'sleekxmpp',
'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',
@@ -92,7 +112,7 @@ setup(
license = 'MIT',
platforms = [ 'any' ],
packages = packages,
requires = [ 'dnspython' ],
requires = [ 'dnspython', 'pyasn1', 'pyasn1_modules' ],
classifiers = CLASSIFIERS,
cmdclass = {'test': TestCommand}
)

198
sleekxmpp/api.py Normal file
View File

@@ -0,0 +1,198 @@
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 curried_handler(handler, op, jid=None, node=None, default=False):
register = getattr(self.api, attr)
return register(handler, self.name, op, jid, node, default)
return curried_handler
elif attr == 'register_default':
def curried_handler(handler, op, jid=None, node=None):
return getattr(self.api, attr)(handler, self.name, op)
return curried_handler
elif attr in ('run', 'restore_default', 'unregister'):
def curried_handler(*args, **kwargs):
return getattr(self.api, attr)(self.name, *args, **kwargs)
return curried_handler
return None
def __getitem__(self, attr):
def curried_handler(jid=None, node=None, ifrom=None, args=None):
return self.api.run(self.name, attr, jid, node, ifrom, args)
return curried_handler
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 jid in (None, ''):
jid = self.xmpp.boundjid
if jid and not isinstance(jid, JID):
jid = JID(jid)
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', True):
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

@@ -1,30 +1,39 @@
# -*- coding: utf-8 -*-
"""
SleekXMPP: The Sleek XMPP Library
Copyright (C) 2010 Nathanael C. Fritz
This file is part of SleekXMPP.
sleekxmpp.basexmpp
~~~~~~~~~~~~~~~~~~
See the file LICENSE for copying permission.
This module provides the common XMPP functionality
for both clients and components.
Part of SleekXMPP: The Sleek XMPP Library
:copyright: (c) 2011 Nathanael C. Fritz
:license: MIT, see LICENSE for more details
"""
from __future__ import with_statement, unicode_literals
import sys
import copy
import logging
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.features import *
from sleekxmpp.plugins import PluginManager, register_plugin, load_plugin
log = logging.getLogger(__name__)
@@ -43,74 +52,76 @@ class BaseXMPP(XMLStream):
with XMPP. It also provides a plugin mechanism to easily extend
and add support for new XMPP features.
Attributes:
auto_authorize -- Manage automatically accepting roster
subscriptions.
auto_subscribe -- Manage automatically requesting mutual
subscriptions.
is_component -- Indicates if this stream is for an XMPP component.
jid -- The XMPP JID for this stream.
plugin -- A dictionary of loaded plugins.
plugin_config -- A dictionary of plugin configurations.
plugin_whitelist -- A list of approved plugins.
sentpresence -- Indicates if an initial presence has been sent.
roster -- A dictionary containing subscribed JIDs and
their presence statuses.
Methods:
Iq -- Factory for creating an Iq stanzas.
Message -- Factory for creating Message stanzas.
Presence -- Factory for creating Presence stanzas.
get -- Return a plugin given its name.
make_iq -- Create and initialize an Iq stanza.
make_iq_error -- Create an Iq stanza of type 'error'.
make_iq_get -- Create an Iq stanza of type 'get'.
make_iq_query -- Create an Iq stanza with a given query.
make_iq_result -- Create an Iq stanza of type 'result'.
make_iq_set -- Create an Iq stanza of type 'set'.
make_message -- Create and initialize a Message stanza.
make_presence -- Create and initialize a Presence stanza.
make_query_roster -- Create a roster query.
process -- Overrides XMLStream.process.
register_plugin -- Load and configure a plugin.
register_plugins -- Load and configure multiple plugins.
send_message -- Create and send a Message stanza.
send_presence -- Create and send a Presence stanza.
send_presence_subscribe -- Send a subscription request.
:param default_ns: Ensure that the correct default XML namespace
is used during initialization.
"""
def __init__(self, jid='', default_ns='jabber:client'):
"""
Adapt an XML stream for use with XMPP.
Arguments:
default_ns -- Ensure that the correct default XML namespace
is used during initialization.
"""
XMLStream.__init__(self)
# To comply with PEP8, method names now use underscores.
# Deprecated method names are re-mapped for backwards compatibility.
self.default_ns = default_ns
self.stream_ns = 'http://etherx.jabber.org/streams'
self.namespace_map[self.stream_ns] = 'stream'
self.boundjid = JID(jid)
#: An identifier for the stream as given by the server.
self.stream_id = None
self.plugin = {}
#: The JabberID (JID) used by this connection.
self.boundjid = JID(jid)
self._expected_server_name = self.boundjid.host
#: A dictionary mapping plugin names to plugins.
self.plugin = PluginManager(self)
#: Configuration options for whitelisted plugins.
#: If a plugin is registered without any configuration,
#: and there is an entry here, it will be used.
self.plugin_config = {}
#: A list of plugins that will be loaded if
#: :meth:`register_plugins` is called.
self.plugin_whitelist = []
#: The main roster object. This roster supports multiple
#: owner JIDs, as in the case for components. For clients
#: which only have a single JID, see :attr:`client_roster`.
self.roster = roster.Roster(self)
self.roster.add(self.boundjid.bare)
#: The single roster for the bound JID. This is the
#: equivalent of::
#:
#: self.roster[self.boundjid.bare]
self.client_roster = self.roster[self.boundjid.bare]
#: The distinction between clients and components can be
#: important, primarily for choosing how to handle the
#: ``'to'`` and ``'from'`` JIDs of stanzas.
self.is_component = False
self.auto_authorize = True
self.auto_subscribe = True
#: 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.
self.sentpresence = False
#: A reference to :mod:`sleekxmpp.stanza` to make accessing
#: stanza classes easier.
self.stanza = sleekxmpp.stanza
self.register_handler(
@@ -164,103 +175,77 @@ class BaseXMPP(XMLStream):
register_stanza_plugin(Message, HTMLIM)
def start_stream_handler(self, xml):
"""
Save the stream ID once the streams have been established.
"""Save the stream ID once the streams have been established.
Overrides XMLStream.start_stream_handler.
Arguments:
xml -- The incoming stream's root element.
:param xml: The incoming stream's root element.
"""
self.stream_id = xml.get('id', '')
def process(self, *args, **kwargs):
"""
Overrides XMLStream.process.
Initialize the XML streams and begin processing events.
"""Initialize plugins and begin processing the XML stream.
The number of threads used for processing stream events is determined
by HANDLER_THREADS.
by :data:`HANDLER_THREADS`.
Arguments:
block -- If block=False then event dispatcher will run
in a separate thread, allowing for the stream to be
used in the background for another application.
Otherwise, process(block=True) blocks the current thread.
Defaults to False.
:param bool block: If ``False``, then event dispatcher will run
in a separate thread, allowing for the stream to be
used in the background for another application.
Otherwise, ``process(block=True)`` blocks the current
thread. Defaults to ``False``.
:param bool threaded: **DEPRECATED**
If ``True``, then event dispatcher will run
in a separate thread, allowing for the stream to be
used in the background for another application.
Defaults to ``True``. This does **not** mean that no
threads are used at all if ``threaded=False``.
**threaded is deprecated and included for API compatibility**
threaded -- If threaded=True then event dispatcher will run
in a separate thread, allowing for the stream to be
used in the background for another application.
Defaults to True.
Regardless of these threading options, these threads will
always exist:
Event handlers and the send queue will be threaded
regardless of these parameters.
- The event queue processor
- The send queue processor
- The scheduler
"""
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):
"""
Register and configure a plugin for use in this stream.
"""Register and configure a plugin for use in this stream.
Arguments:
plugin -- The name of the plugin class. Plugin names must
:param plugin: The name of the plugin class. Plugin names must
be unique.
pconfig -- A dictionary of configuration data for the plugin.
Defaults to an empty dictionary.
module -- Optional refence to the module containing the plugin
:param pconfig: A dictionary of configuration data for the plugin.
Defaults to an empty dictionary.
:param module: Optional refence to the module containing the plugin
class if using custom plugins.
"""
try:
# 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) '
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.
"""Register and initialize all built-in plugins.
Optionally, the list of plugins loaded may be limited to those
contained in self.plugin_whitelist.
contained in :attr:`plugin_whitelist`.
Plugin configurations stored in self.plugin_config will be used.
Plugin configurations stored in :attr:`plugin_config` will be used.
"""
if self.plugin_whitelist:
plugin_list = self.plugin_whitelist
@@ -269,29 +254,20 @@ 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.
"""
"""Return a plugin given its name, if it has been registered."""
if key in self.plugin:
return self.plugin[key]
else:
log.warning("""Plugin "%s" is not loaded.""" % key)
log.warning("Plugin '%s' is not loaded.", key)
return False
def get(self, key, default):
"""
Return a plugin given its name, if it has been registered.
"""
"""Return a plugin given its name, if it has been registered."""
return self.plugin.get(key, default)
def Message(self, *args, **kwargs):
@@ -307,16 +283,18 @@ class BaseXMPP(XMLStream):
return Presence(self, *args, **kwargs)
def make_iq(self, id=0, ifrom=None, ito=None, itype=None, iquery=None):
"""
Create a new Iq stanza with a given Id and from JID.
"""Create a new Iq stanza with a given Id and from JID.
Arguments:
id -- An ideally unique ID value for this stanza thread.
Defaults to 0.
ifrom -- The from JID to use for this stanza.
ito -- The destination JID for this stanza.
itype -- The Iq's type, one of: get, set, result, or error.
iquery -- Optional namespace for adding a query element.
:param id: An ideally unique ID value for this stanza thread.
Defaults to 0.
:param ifrom: The from :class:`~sleekxmpp.xmlstream.jid.JID`
to use for this stanza.
:param ito: The destination :class:`~sleekxmpp.xmlstream.jid.JID`
for this stanza.
:param itype: The :class:`~sleekxmpp.stanza.iq.Iq`'s type,
one of: ``'get'``, ``'set'``, ``'result'``,
or ``'error'``.
:param iquery: Optional namespace for adding a query element.
"""
iq = self.Iq()
iq['id'] = str(id)
@@ -327,17 +305,17 @@ class BaseXMPP(XMLStream):
return iq
def make_iq_get(self, queryxmlns=None, ito=None, ifrom=None, iq=None):
"""
Create an Iq stanza of type 'get'.
"""Create an :class:`~sleekxmpp.stanza.iq.Iq` stanza of type ``'get'``.
Optionally, a query element may be added.
Arguments:
queryxmlns -- The namespace of the query to use.
ito -- The destination JID for this stanza.
ifrom -- The from JID to use for this stanza.
iq -- Optionally use an existing stanza instead
of generating a new one.
:param queryxmlns: The namespace of the query to use.
:param ito: The destination :class:`~sleekxmpp.xmlstream.jid.JID`
for this stanza.
:param ifrom: The ``'from'`` :class:`~sleekxmpp.xmlstream.jid.JID`
to use for this stanza.
:param iq: Optionally use an existing stanza instead
of generating a new one.
"""
if not iq:
iq = self.Iq()
@@ -351,14 +329,16 @@ class BaseXMPP(XMLStream):
def make_iq_result(self, id=None, ito=None, ifrom=None, iq=None):
"""
Create an Iq stanza of type 'result' with the given ID value.
Create an :class:`~sleekxmpp.stanza.iq.Iq` stanza of type
``'result'`` with the given ID value.
Arguments:
id -- An ideally unique ID value. May use self.new_id().
ito -- The destination JID for this stanza.
ifrom -- The from JID to use for this stanza.
iq -- Optionally use an existing stanza instead
of generating a new one.
:param id: An ideally unique ID value. May use :meth:`new_id()`.
:param ito: The destination :class:`~sleekxmpp.xmlstream.jid.JID`
for this stanza.
:param ifrom: The ``'from'`` :class:`~sleekxmpp.xmlstream.jid.JID`
to use for this stanza.
:param iq: Optionally use an existing stanza instead
of generating a new one.
"""
if not iq:
iq = self.Iq()
@@ -374,17 +354,22 @@ class BaseXMPP(XMLStream):
def make_iq_set(self, sub=None, ito=None, ifrom=None, iq=None):
"""
Create an Iq stanza of type 'set'.
Create an :class:`~sleekxmpp.stanza.iq.Iq` stanza of type ``'set'``.
Optionally, a substanza may be given to use as the
stanza's payload.
Arguments:
sub -- A stanza or XML object to use as the Iq's payload.
ito -- The destination JID for this stanza.
ifrom -- The from JID to use for this stanza.
iq -- Optionally use an existing stanza instead
of generating a new one.
:param sub: Either an
:class:`~sleekxmpp.xmlstream.stanzabase.ElementBase`
stanza object or an
:class:`~xml.etree.ElementTree.Element` XML object
to use as the :class:`~sleekxmpp.stanza.iq.Iq`'s payload.
:param ito: The destination :class:`~sleekxmpp.xmlstream.jid.JID`
for this stanza.
:param ifrom: The ``'from'`` :class:`~sleekxmpp.xmlstream.jid.JID`
to use for this stanza.
:param iq: Optionally use an existing stanza instead
of generating a new one.
"""
if not iq:
iq = self.Iq()
@@ -401,19 +386,20 @@ class BaseXMPP(XMLStream):
condition='feature-not-implemented',
text=None, ito=None, ifrom=None, iq=None):
"""
Create an Iq stanza of type 'error'.
Create an :class:`~sleekxmpp.stanza.iq.Iq` stanza of type ``'error'``.
Arguments:
id -- An ideally unique ID value. May use self.new_id().
type -- The type of the error, such as 'cancel' or 'modify'.
Defaults to 'cancel'.
condition -- The error condition.
Defaults to 'feature-not-implemented'.
text -- A message describing the cause of the error.
ito -- The destination JID for this stanza.
ifrom -- The from JID to use for this stanza.
iq -- Optionally use an existing stanza instead
of generating a new one.
:param id: An ideally unique ID value. May use :meth:`new_id()`.
:param type: The type of the error, such as ``'cancel'`` or
``'modify'``. Defaults to ``'cancel'``.
:param condition: The error condition. Defaults to
``'feature-not-implemented'``.
:param text: A message describing the cause of the error.
:param ito: The destination :class:`~sleekxmpp.xmlstream.jid.JID`
for this stanza.
:param ifrom: The ``'from'`` :class:`~sleekxmpp.xmlstream.jid.JID`
to use for this stanza.
:param iq: Optionally use an existing stanza instead
of generating a new one.
"""
if not iq:
iq = self.Iq()
@@ -429,15 +415,16 @@ class BaseXMPP(XMLStream):
def make_iq_query(self, iq=None, xmlns='', ito=None, ifrom=None):
"""
Create or modify an Iq stanza to use the given
query namespace.
Create or modify an :class:`~sleekxmpp.stanza.iq.Iq` stanza
to use the given query namespace.
Arguments:
iq -- Optional Iq stanza to modify. A new
stanza is created otherwise.
xmlns -- The query's namespace.
ito -- The destination JID for this stanza.
ifrom -- The from JID to use for this stanza.
:param iq: Optionally use an existing stanza instead
of generating a new one.
:param xmlns: The query's namespace.
:param ito: The destination :class:`~sleekxmpp.xmlstream.jid.JID`
for this stanza.
:param ifrom: The ``'from'`` :class:`~sleekxmpp.xmlstream.jid.JID`
to use for this stanza.
"""
if not iq:
iq = self.Iq()
@@ -449,12 +436,10 @@ class BaseXMPP(XMLStream):
return iq
def make_query_roster(self, iq=None):
"""
Create a roster query element.
"""Create a roster query element.
Arguments:
iq -- Optional Iq stanza to modify. A new stanza
is created otherwise.
:param iq: Optionally use an existing stanza instead
of generating a new one.
"""
if iq:
iq['query'] = 'jabber:iq:roster'
@@ -463,18 +448,19 @@ class BaseXMPP(XMLStream):
def make_message(self, mto, mbody=None, msubject=None, mtype=None,
mhtml=None, mfrom=None, mnick=None):
"""
Create and initialize a new Message stanza.
Create and initialize a new
:class:`~sleekxmpp.stanza.message.Message` stanza.
Arguments:
mto -- The recipient of the message.
mbody -- The main contents of the message.
msubject -- Optional subject for the message.
mtype -- The message's type, such as 'chat' or 'groupchat'.
mhtml -- Optional HTML body content.
mfrom -- The sender of the message. if sending from a client,
be aware that some servers require that the full JID
of the sender be used.
mnick -- Optional nickname of the sender.
:param mto: The recipient of the message.
:param mbody: The main contents of the message.
:param msubject: Optional subject for the message.
:param mtype: The message's type, such as ``'chat'`` or
``'groupchat'``.
:param mhtml: Optional HTML body content in the form of a string.
:param mfrom: The sender of the message. if sending from a client,
be aware that some servers require that the full JID
of the sender be used.
:param mnick: Optional nickname of the sender.
"""
message = self.Message(sto=mto, stype=mtype, sfrom=mfrom)
message['body'] = mbody
@@ -488,16 +474,16 @@ 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 Presence stanza.
Create and initialize a new
:class:`~sleekxmpp.stanza.presence.Presence` stanza.
Arguments:
pshow -- The presence's show value.
pstatus -- The presence's status message.
ppriority -- This connections' priority.
pto -- The recipient of a directed presence.
ptype -- The type of presence, such as 'subscribe'.
pfrom -- The sender of the presence.
pnick -- Optional nickname of the presence's sender.
:param pshow: The presence's show value.
:param pstatus: The presence's status message.
:param ppriority: This connection's priority.
:param pto: The recipient of a directed presence.
:param ptype: The type of presence, such as ``'subscribe'``.
:param pfrom: The sender of the presence.
:param pnick: Optional nickname of the presence's sender.
"""
presence = self.Presence(stype=ptype, sfrom=pfrom, sto=pto)
if pshow is not None:
@@ -512,18 +498,19 @@ class BaseXMPP(XMLStream):
def send_message(self, mto, mbody, msubject=None, mtype=None,
mhtml=None, mfrom=None, mnick=None):
"""
Create, initialize, and send a Message stanza.
Create, initialize, and send a new
:class:`~sleekxmpp.stanza.message.Message` stanza.
Arguments:
mto -- The recipient of the message.
mbody -- The main contents of the message.
msubject -- Optional subject for the message.
mtype -- The message's type, such as 'chat' or 'groupchat'.
mhtml -- Optional HTML body content.
mfrom -- The sender of the message. if sending from a client,
be aware that some servers require that the full JID
of the sender be used.
mnick -- Optional nickname of the sender.
:param mto: The recipient of the message.
:param mbody: The main contents of the message.
:param msubject: Optional subject for the message.
:param mtype: The message's type, such as ``'chat'`` or
``'groupchat'``.
:param mhtml: Optional HTML body content in the form of a string.
:param mfrom: The sender of the message. if sending from a client,
be aware that some servers require that the full JID
of the sender be used.
:param mnick: Optional nickname of the sender.
"""
self.make_message(mto, mbody, msubject, mtype,
mhtml, mfrom, mnick).send()
@@ -531,16 +518,16 @@ 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 Presence stanza.
Create, initialize, and send a new
:class:`~sleekxmpp.stanza.presence.Presence` stanza.
Arguments:
pshow -- The presence's show value.
pstatus -- The presence's status message.
ppriority -- This connections' priority.
pto -- The recipient of a directed presence.
ptype -- The type of presence, such as 'subscribe'.
pfrom -- The sender of the presence.
pnick -- Optional nickname of the presence's sender.
:param pshow: The presence's show value.
:param pstatus: The presence's status message.
:param ppriority: This connection's priority.
:param pto: The recipient of a directed presence.
:param ptype: The type of presence, such as ``'subscribe'``.
:param pfrom: The sender of the presence.
:param pnick: Optional nickname of the presence's sender.
"""
# Python2.6 chokes on Unicode strings for dict keys.
args = {str('pto'): pto,
@@ -558,13 +545,14 @@ class BaseXMPP(XMLStream):
def send_presence_subscription(self, pto, pfrom=None,
ptype='subscribe', pnick=None):
"""
Create, initialize, and send a Presence stanza of type 'subscribe'.
Create, initialize, and send a new
:class:`~sleekxmpp.stanza.presence.Presence` stanza of
type ``'subscribe'``.
Arguments:
pto -- The recipient of a directed presence.
pfrom -- The sender of the presence.
ptype -- The type of presence. Defaults to 'subscribe'.
pnick -- Nickname of the presence's sender.
:param pto: The recipient of a directed presence.
:param pfrom: The sender of the presence.
:param ptype: The type of presence, such as ``'subscribe'``.
:param pnick: Optional nickname of the presence's sender.
"""
presence = self.makePresence(ptype=ptype,
pfrom=pfrom,
@@ -577,9 +565,7 @@ class BaseXMPP(XMLStream):
@property
def jid(self):
"""
Attribute accessor for bare jid
"""
"""Attribute accessor for bare jid"""
log.warning("jid property deprecated. Use boundjid.bare")
return self.boundjid.bare
@@ -590,9 +576,7 @@ class BaseXMPP(XMLStream):
@property
def fulljid(self):
"""
Attribute accessor for full jid
"""
"""Attribute accessor for full jid"""
log.warning("fulljid property deprecated. Use boundjid.full")
return self.boundjid.full
@@ -603,9 +587,7 @@ class BaseXMPP(XMLStream):
@property
def resource(self):
"""
Attribute accessor for jid resource
"""
"""Attribute accessor for jid resource"""
log.warning("resource property deprecated. Use boundjid.resource")
return self.boundjid.resource
@@ -616,9 +598,7 @@ class BaseXMPP(XMLStream):
@property
def username(self):
"""
Attribute accessor for jid usernode
"""
"""Attribute accessor for jid usernode"""
log.warning("username property deprecated. Use boundjid.user")
return self.boundjid.user
@@ -629,9 +609,7 @@ class BaseXMPP(XMLStream):
@property
def server(self):
"""
Attribute accessor for jid host
"""
"""Attribute accessor for jid host"""
log.warning("server property deprecated. Use boundjid.host")
return self.boundjid.server
@@ -640,9 +618,35 @@ class BaseXMPP(XMLStream):
log.warning("server property deprecated. Use boundjid.host")
self.boundjid.server = value
@property
def auto_authorize(self):
"""Auto accept or deny subscription requests.
If ``True``, auto accept subscription requests.
If ``False``, auto deny subscription requests.
If ``None``, don't automatically respond.
"""
return self.roster.auto_authorize
@auto_authorize.setter
def auto_authorize(self, value):
self.roster.auto_authorize = value
@property
def auto_subscribe(self):
"""Auto send requests for mutual subscriptions.
If ``True``, auto send mutual subscription requests.
"""
return self.roster.auto_subscribe
@auto_subscribe.setter
def auto_subscribe(self, value):
self.roster.auto_subscribe = value
def set_jid(self, jid):
"""Rip a JID apart and claim it as our own."""
log.debug("setting jid to %s" % jid)
log.debug("setting jid to %s", jid)
self.boundjid.full = jid
def getjidresource(self, fulljid):
@@ -663,6 +667,8 @@ class BaseXMPP(XMLStream):
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):
@@ -676,17 +682,16 @@ class BaseXMPP(XMLStream):
self.roster[pto][pfrom].handle_unavailable(presence)
def _handle_new_subscription(self, stanza):
"""
Attempt to automatically handle subscription requests.
"""Attempt to automatically handle subscription requests.
Subscriptions will be approved if the request is from
a whitelisted JID, of self.auto_authorize is True. They
will be rejected if self.auto_authorize is False. Setting
self.auto_authorize to None will disable automatic
a whitelisted JID, of :attr:`auto_authorize` is True. They
will be rejected if :attr:`auto_authorize` is False. Setting
:attr:`auto_authorize` to ``None`` will disable automatic
subscription handling (except for whitelisted JIDs).
If a subscription is accepted, a request for a mutual
subscription will be sent if self.auto_subscribe is True.
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]
@@ -725,11 +730,13 @@ class BaseXMPP(XMLStream):
self.roster[pto][pfrom].handle_unsubscribed(presence)
def _handle_presence(self, presence):
"""
Process incoming presence stanzas.
"""Process incoming presence stanzas.
Update the roster with presence information.
"""
if not self.is_component and not presence['to'].bare:
presence['to'] = self.boundjid
self.event("presence_%s" % presence['type'], presence)
# Check for changes in subscription state.
@@ -741,27 +748,27 @@ class BaseXMPP(XMLStream):
not presence['type'] in presence.showtypes:
return
self.event("changed_status", presence)
def exception(self, exception):
"""
Process any uncaught exceptions, notably IqError and
IqTimeout exceptions.
"""Process any uncaught exceptions, notably
:class:`~sleekxmpp.exceptions.IqError` and
:class:`~sleekxmpp.exceptions.IqTimeout` exceptions.
Overrides XMLStream.exception.
Arguments:
exception -- An unhandled exception object.
:param exception: An unhandled :class:`Exception` object.
"""
if isinstance(exception, IqError):
iq = exception.iq
log.error('%s: %s' % (iq['error']['condition'],
iq['error']['text']))
log.error('%s: %s', iq['error']['condition'],
iq['error']['text'])
log.warning('You should catch IqError exceptions')
elif isinstance(exception, IqTimeout):
iq = exception.iq
log.error('Request timed out: %s' % iq)
log.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

@@ -1,30 +1,27 @@
# -*- coding: utf-8 -*-
"""
SleekXMPP: The Sleek XMPP Library
Copyright (C) 2010 Nathanael C. Fritz
This file is part of SleekXMPP.
sleekxmpp.clientxmpp
~~~~~~~~~~~~~~~~~~~~
See the file LICENSE for copying permission.
This module provides XMPP functionality that
is specific to client connections.
Part of SleekXMPP: The Sleek XMPP Library
:copyright: (c) 2011 Nathanael C. Fritz
:license: MIT, see LICENSE for more details
"""
from __future__ import absolute_import, unicode_literals
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:
@@ -41,46 +38,42 @@ log = logging.getLogger(__name__)
class ClientXMPP(BaseXMPP):
"""
SleekXMPP's client class. ( Use only for good, not for evil.)
SleekXMPP's client class. (Use only for good, not for evil.)
Typical Use:
xmpp = ClientXMPP('user@server.tld/resource', 'password')
xmpp.process(block=False) // when block is True, it blocks the current
// thread. False by default.
Typical use pattern:
Attributes:
.. code-block:: python
Methods:
connect -- Overrides XMLStream.connect.
del_roster_item -- Delete a roster item.
get_roster -- Retrieve the roster from the server.
register_feature -- Register a stream feature.
update_roster -- Update a roster item.
xmpp = ClientXMPP('user@server.tld/resource', 'password')
# ... Register plugins and event handlers ...
xmpp.connect()
xmpp.process(block=False) # block=True will block the current
# thread. By default, block=False
:param jid: The JID of the XMPP user account.
:param password: The password for the XMPP user account.
:param ssl: **Deprecated.**
:param plugin_config: A dictionary of plugin configurations.
:param plugin_whitelist: A list of approved plugins that
will be loaded when calling
:meth:`~sleekxmpp.basexmpp.BaseXMPP.register_plugins()`.
:param escape_quotes: **Deprecated.**
"""
def __init__(self, jid, password, ssl=False, plugin_config={},
plugin_whitelist=[], escape_quotes=True):
"""
Create a new SleekXMPP client.
Arguments:
jid -- The JID of the XMPP user account.
password -- The password for the XMPP user account.
ssl -- Deprecated.
plugin_config -- A dictionary of plugin configurations.
plugin_whitelist -- A list of approved plugins that will be loaded
when calling register_plugins.
escape_quotes -- Deprecated.
"""
plugin_whitelist=[], escape_quotes=True, sasl_mech=None):
BaseXMPP.__init__(self, jid, 'jabber:client')
self.set_jid(jid)
self.password = password
self.escape_quotes = escape_quotes
self.plugin_config = plugin_config
self.plugin_whitelist = plugin_whitelist
self.default_port = 5222
self.credentials = {}
self.password = password
self.stream_header = "<stream:stream to='%s' %s %s version='1.0'>" % (
self.boundjid.host,
"xmlns:stream='%s'" % self.stream_ns,
@@ -91,6 +84,8 @@ class ClientXMPP(BaseXMPP):
self._stream_feature_handlers = {}
self._stream_feature_order = []
self.dns_service = 'xmpp-client'
#TODO: Use stream state here
self.authenticated = False
self.sessionstarted = False
@@ -98,6 +93,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)
@@ -107,82 +103,68 @@ 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
self.register_plugin('feature_starttls')
self.register_plugin('feature_mechanisms')
self.register_plugin('feature_bind')
self.register_plugin('feature_session')
self.register_plugin('feature_mechanisms',
pconfig={'use_mech': sasl_mech} if sasl_mech else None)
self.register_plugin('feature_rosterver')
def connect(self, address=tuple(), reattempt=True, use_tls=True):
"""
Connect to the XMPP server.
@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):
"""Connect to the XMPP server.
When no address is given, a SRV lookup for the server will
be attempted. If that fails, the server user in the JID
will be used.
Arguments:
address -- A tuple containing the server's host and port.
reattempt -- If True, reattempt the connection if an
error occurs. Defaults to True.
use_tls -- Indicates if TLS should be used for the
connection. Defaults to True.
:param address -- A tuple containing the server's host and port.
:param reattempt: If ``True``, repeat attempting to connect if an
error occurs. Defaults to ``True``.
:param use_tls: Indicates if TLS should be used for the
connection. Defaults to ``True``.
:param use_ssl: Indicates if the older SSL connection method
should be used. Defaults to ``False``.
"""
self.session_started_event.clear()
if not address:
# 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, reattempt=reattempt)
def get_dns_records(self, domain, port=None):
"""
Get the DNS records for a domain.
Overriddes XMLStream.get_dns_records to use SRV.
Arguments:
domain -- The domain in question.
port -- If the results don't include a port, use this one.
"""
if port is None:
port = self.default_port
if DNSPYTHON:
try:
record = "_xmpp-client._tcp.%s" % domain
answers = []
for answer in dns.resolver.query(record, dns.rdatatype.SRV):
address = (answer.target.to_text()[:-1], answer.port)
answers.append((address, answer.priority, answer.weight))
except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer):
log.warning("No SRV records for %s" % domain)
answers = super(ClientXMPP, self).get_dns_records(domain, port)
except dns.exception.Timeout:
log.warning("DNS resolution timed out " + \
"for SRV record of %s" % domain)
answers = super(ClientXMPP, self).get_dns_records(domain, port)
return answers
else:
log.warning("dnspython is not installed -- " + \
"relying on OS A record resolution")
return [((domain, port), 0, 0)]
use_tls=use_tls, use_ssl=use_ssl,
reattempt=reattempt)
def register_feature(self, name, handler, restart=False, order=5000):
"""
Register a stream feature.
"""Register a stream feature handler.
Arguments:
name -- The name of the stream feature.
handler -- The function to execute if the feature is received.
restart -- Indicates if feature processing should halt with
this feature. Defaults to False.
order -- The relative ordering in which the feature should
be negotiated. Lower values will be attempted
earlier when available.
:param name: The name of the stream feature.
:param handler: The function to execute if the feature is received.
:param restart: Indicates if feature processing should halt with
this feature. Defaults to ``False``.
:param order: The relative ordering in which the feature should
be negotiated. Lower values will be attempted
earlier when available.
"""
self._stream_feature_handlers[name] = (handler, restart)
self._stream_feature_order.append((order, name))
@@ -190,61 +172,67 @@ class ClientXMPP(BaseXMPP):
def update_roster(self, jid, name=None, subscription=None, groups=[],
block=True, timeout=None, callback=None):
"""
Add or change a roster item.
"""Add or change a roster item.
Arguments:
jid -- The JID of the entry to modify.
name -- The user's nickname for this JID.
subscription -- The subscription status. May be one of
'to', 'from', 'both', or 'none'. If set
to 'remove', the entry will be deleted.
groups -- The roster groups that contain this item.
block -- Specify if the roster request will block
until a response is received, or a timeout
occurs. Defaults to True.
timeout -- The length of time (in seconds) to wait
for a response before continuing if blocking
is used. Defaults to self.response_timeout.
callback -- Optional reference to a stream handler function.
Will be executed when the roster is received.
Implies block=False.
:param jid: The JID of the entry to modify.
:param name: The user's nickname for this JID.
:param subscription: The subscription status. May be one of
``'to'``, ``'from'``, ``'both'``, or
``'none'``. If set to ``'remove'``,
the entry will be deleted.
:param groups: The roster groups that contain this item.
:param block: Specify if the roster request will block
until a response is received, or a timeout
occurs. Defaults to ``True``.
:param timeout: The length of time (in seconds) to wait
for a response before continuing if blocking
is used. Defaults to
:attr:`~sleekxmpp.xmlstream.xmlstream.XMLStream.response_timeout`.
:param callback: Optional reference to a stream handler function.
Will be executed when the roster is received.
Implies ``block=False``.
"""
return self.client_roster.updtae(jid, name, subscription, groups,
return self.client_roster.update(jid, name, subscription, groups,
block, timeout, callback)
def del_roster_item(self, jid):
"""
Remove an item from the roster by setting its subscription
status to 'remove'.
"""Remove an item from the roster.
This is done by setting its subscription status to ``'remove'``.
Arguments:
jid -- The JID of the item to remove.
:param jid: The JID of the item to remove.
"""
return self.client_roster.remove(jid)
def get_roster(self, block=True, timeout=None, callback=None):
"""
Request the roster from the server.
"""Request the roster from the server.
Arguments:
block -- Specify if the roster request will block until a
response is received, or a timeout occurs.
Defaults to True.
timeout -- The length of time (in seconds) to wait for a response
:param block: Specify if the roster request will block until a
response is received, or a timeout occurs.
Defaults to ``True``.
:param timeout: The length of time (in seconds) to wait for a response
before continuing if blocking is used.
Defaults to self.response_timeout.
callback -- Optional reference to a stream handler function. Will
be executed when the roster is received.
Implies block=False.
Defaults to
:attr:`~sleekxmpp.xmlstream.xmlstream.XMLStream.response_timeout`.
:param callback: Optional reference to a stream handler function. Will
be executed when the roster is received.
Implies ``block=False``.
"""
iq = self.Iq()
iq['type'] = 'get'
iq.enable('roster')
response = iq.send(block, timeout, callback)
if '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
@@ -255,11 +243,9 @@ class ClientXMPP(BaseXMPP):
self.features = set()
def _handle_stream_features(self, features):
"""
Process the received stream features.
"""Process the received stream features.
Arguments:
features -- The features stanza.
:param features: The features stanza.
"""
for order, name in self._stream_feature_order:
if name in features['features']:
@@ -269,32 +255,43 @@ class ClientXMPP(BaseXMPP):
# restarting the XML stream.
return True
def _handle_roster(self, iq, request=False):
"""
Update the roster after receiving a roster stanza.
def _handle_roster(self, iq):
"""Update the roster after receiving a roster stanza.
Arguments:
iq -- The roster stanza.
request -- Indicates if this stanza is a response
to a request for the roster.
:param iq: The roster stanza.
"""
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']
for jid in iq['roster']['items']:
item = iq['roster']['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

@@ -1,25 +1,28 @@
# -*- coding: utf-8 -*-
"""
SleekXMPP: The Sleek XMPP Library
Copyright (C) 2010 Nathanael C. Fritz
This file is part of SleekXMPP.
sleekxmpp.clientxmpp
~~~~~~~~~~~~~~~~~~~~
See the file LICENSE for copying permission.
This module provides XMPP functionality that
is specific to external server component connections.
Part of SleekXMPP: The Sleek XMPP Library
:copyright: (c) 2011 Nathanael C. Fritz
:license: MIT, see LICENSE for more details
"""
from __future__ import absolute_import
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__)
@@ -32,28 +35,22 @@ class ComponentXMPP(BaseXMPP):
Use only for good, not for evil.
Methods:
connect -- Overrides XMLStream.connect.
incoming_filter -- Overrides XMLStream.incoming_filter.
start_stream_handler -- Overrides XMLStream.start_stream_handler.
:param jid: The JID of the component.
:param secret: The secret or password for the component.
:param host: The server accepting the component.
:param port: The port used to connect to the server.
:param plugin_config: A dictionary of plugin configurations.
:param plugin_whitelist: A list of approved plugins that
will be loaded when calling
:meth:`~sleekxmpp.basexmpp.BaseXMPP.register_plugins()`.
:param use_jc_ns: Indicates if the ``'jabber:client'`` namespace
should be used instead of the standard
``'jabber:component:accept'`` namespace.
Defaults to ``False``.
"""
def __init__(self, jid, secret, host, port,
def __init__(self, jid, secret, host=None, port=None,
plugin_config={}, plugin_whitelist=[], use_jc_ns=False):
"""
Arguments:
jid -- The JID of the component.
secret -- The secret or password for the component.
host -- The server accepting the component.
port -- The port used to connect to the server.
plugin_config -- A dictionary of plugin configurations.
plugin_whitelist -- A list of desired plugins to load
when using register_plugins.
use_js_ns -- Indicates if the 'jabber:client' namespace
should be used instead of the standard
'jabber:component:accept' namespace.
Defaults to False.
"""
if use_jc_ns:
default_ns = 'jabber:client'
else:
@@ -81,26 +78,45 @@ class ComponentXMPP(BaseXMPP):
self.add_event_handler('presence_probe',
self._handle_probe)
def connect(self):
"""
Connect to the server.
def connect(self, host=None, port=None, use_ssl=False,
use_tls=True, reattempt=True):
"""Connect to the server.
Overrides XMLStream.connect.
Setting ``reattempt`` to ``True`` will cause connection attempts to
be made every second until a successful connection is established.
:param host: The name of the desired server for the connection.
Defaults to :attr:`server_host`.
:param port: Port to connect to on the server.
Defauts to :attr:`server_port`.
:param use_ssl: Flag indicating if SSL should be used by connecting
directly to a port using SSL.
:param use_tls: Flag indicating if TLS should be used, allowing for
connecting to a port without using SSL immediately and
later upgrading the connection.
:param reattempt: Flag indicating if the socket should reconnect
after disconnections.
"""
log.debug("Connecting to %s:%s" % (self.server_host,
self.server_port))
return XMLStream.connect(self, self.server_host,
self.server_port)
if host is None:
host = self.server_host
if port is None:
port = self.server_port
self.server_name = self.boundjid.host
log.debug("Connecting to %s:%s", host, port)
return XMLStream.connect(self, host=host, port=port,
use_ssl=use_ssl,
use_tls=use_tls,
reattempt=reattempt)
def incoming_filter(self, xml):
"""
Pre-process incoming XML stanzas by converting any 'jabber:client'
namespaced elements to the component's default namespace.
Pre-process incoming XML stanzas by converting any
``'jabber:client'`` namespaced elements to the component's
default namespace.
Overrides XMLStream.incoming_filter.
Arguments:
xml -- The XML stanza to pre-process.
:param xml: The XML stanza to pre-process.
"""
if xml.tag.startswith('{jabber:client}'):
xml.tag = xml.tag.replace('jabber:client', self.default_ns)
@@ -117,10 +133,7 @@ class ComponentXMPP(BaseXMPP):
Once the streams are established, attempt to handshake
with the server to be accepted as a component.
Overrides BaseXMPP.start_stream_handler.
Arguments:
xml -- The incoming stream's root element.
:param xml: The incoming stream's root element.
"""
BaseXMPP.start_stream_handler(self, xml)
@@ -136,11 +149,9 @@ class ComponentXMPP(BaseXMPP):
self.send_xml(handshake, now=True)
def _handle_handshake(self, xml):
"""
The handshake has been accepted.
"""The handshake has been accepted.
Arguments:
xml -- The reply handshake stanza.
:param xml: The reply handshake stanza.
"""
self.session_started_event.set()
self.event("session_start")

View File

@@ -1,9 +1,12 @@
# -*- coding: utf-8 -*-
"""
SleekXMPP: The Sleek XMPP Library
Copyright (C) 2010 Nathanael C. Fritz
This file is part of SleekXMPP.
sleekxmpp.exceptions
~~~~~~~~~~~~~~~~~~~~
See the file LICENSE for copying permission.
Part of SleekXMPP: The Sleek XMPP Library
:copyright: (c) 2011 Nathanael C. Fritz
:license: MIT, see LICENSE for more details
"""
@@ -13,37 +16,35 @@ class XMPPError(Exception):
A generic exception that may be raised while processing an XMPP stanza
to indicate that an error response stanza should be sent.
The exception method for stanza objects extending RootStanza will create
an error stanza and initialize any additional substanzas using the
extension information included in the exception.
The exception method for stanza objects extending
:class:`~sleekxmpp.stanza.rootstanza.RootStanza` will create an error
stanza and initialize any additional substanzas using the extension
information included in the exception.
Meant for use in SleekXMPP plugins and applications using SleekXMPP.
Extension information can be included to add additional XML elements
to the generated error stanza.
:param condition: The XMPP defined error condition.
Defaults to ``'undefined-condition'``.
:param text: Human readable text describing the error.
:param etype: The XMPP error type, such as ``'cancel'`` or ``'modify'``.
Defaults to ``'cancel'``.
:param extension: Tag name of the extension's XML content.
:param extension_ns: XML namespace of the extensions' XML content.
:param extension_args: Content and attributes for the extension
element. Same as the additional arguments to
the :class:`~xml.etree.ElementTree.Element`
constructor.
:param clear: Indicates if the stanza's contents should be
removed before replying with an error.
Defaults to ``True``.
"""
def __init__(self, condition='undefined-condition', text=None,
etype='cancel', extension=None, extension_ns=None,
extension_args=None, clear=True):
"""
Create a new XMPPError exception.
Extension information can be included to add additional XML elements
to the generated error stanza.
Arguments:
condition -- The XMPP defined error condition.
Defaults to 'undefined-condition'.
text -- Human readable text describing the error.
etype -- The XMPP error type, such as cancel or modify.
Defaults to 'cancel'.
extension -- Tag name of the extension's XML content.
extension_ns -- XML namespace of the extensions' XML content.
extension_args -- Content and attributes for the extension
element. Same as the additional arguments to
the ET.Element constructor.
clear -- Indicates if the stanza's contents should be
removed before replying with an error.
Defaults to True.
"""
if extension_args is None:
extension_args = {}
@@ -68,6 +69,8 @@ class IqTimeout(XMPPError):
condition='remote-server-timeout',
etype='cancel')
#: The :class:`~sleekxmpp.stanza.iq.Iq` stanza whose response
#: did not arrive before the timeout expired.
self.iq = iq
class IqError(XMPPError):
@@ -83,4 +86,5 @@ class IqError(XMPPError):
text=iq['error']['text'],
etype=iq['error']['type'])
#: The :class:`~sleekxmpp.stanza.iq.Iq` error result stanza.
self.iq = iq

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,
@@ -42,7 +40,7 @@ class feature_bind(base_plugin):
Arguments:
features -- The stream features stanza.
"""
log.debug("Requesting resource: %s" % self.xmpp.boundjid.resource)
log.debug("Requesting resource: %s", self.xmpp.boundjid.resource)
iq = self.xmpp.Iq()
iq['type'] = 'set'
iq.enable('bind')
@@ -52,10 +50,11 @@ 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.features.add('bind')
log.info("Node set to: %s" % self.xmpp.boundjid.full)
log.info("Node set to: %s", self.xmpp.boundjid.full)
if 'session' not in features['features']:
log.debug("Established Session")

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,36 +9,48 @@
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
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)
@@ -53,6 +65,9 @@ class feature_mechanisms(base_plugin):
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 +75,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()),
@@ -95,35 +109,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."""
log.info("Authentication failed: %s" % stanza['condition'])
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):

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()),
@@ -58,8 +58,8 @@ class feature_starttls(base_plugin):
self.xmpp.send(features['starttls'], now=True)
return True
else:
log.warning("The module tlslite is required to log in" +\
" to some servers, and has not been found.")
log.warning("The module tlslite is required to log in" + \
" to some servers, and has not been found.")
return False
def _handle_starttls_proceed(self, proceed):

View File

@@ -5,9 +5,50 @@
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_0085', # Chat State Notifications
'xep_0086', # Legacy Error Codes
'xep_0092', # Software Version
'xep_0107', # User Mood
'xep_0108', # User Activity
'xep_0115', # Entity Capabilities
'xep_0118', # User Tune
'xep_0128', # Extended Service Discovery
'xep_0153', # vCard-Based Avatars
'xep_0163', # Personal Eventing Protocol
'xep_0172', # User Nickname
'xep_0184', # Message Receipts
'xep_0198', # Stream Management
'xep_0199', # Ping
'xep_0202', # Entity Time
'xep_0203', # Delayed Delivery
'xep_0224', # Attention
'xep_0231', # Bits of Binary
'xep_0249', # Direct MUC Invitations
]

View File

@@ -1,91 +1,301 @@
"""
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 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:
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:
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.plugin_init()
log.debug("Loaded Plugin: %s", plugin.description)
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.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()
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 = config if config is not None else {}
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 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

@@ -121,7 +121,7 @@ class gmail_notify(base.base_plugin):
def handle_gmail(self, iq):
mailbox = iq['mailbox']
approx = ' approximately' if mailbox['estimated'] else ''
log.info('Gmail: Received%s %s emails' % (approx, mailbox['total-matched']))
log.info('Gmail: Received%s %s emails', approx, mailbox['total-matched'])
self.last_result_time = mailbox['result-time']
self.xmpp.event('gmail_messages', iq)
@@ -140,7 +140,7 @@ class gmail_notify(base.base_plugin):
if query is None:
log.info("Gmail: Checking for new emails")
else:
log.info('Gmail: Searching for emails matching: "%s"' % query)
log.info('Gmail: Searching for emails matching: "%s"', query)
iq = self.xmpp.Iq()
iq['type'] = 'get'
iq['to'] = self.xmpp.boundjid.bare

View File

@@ -43,7 +43,7 @@ class jobs(base.base_plugin):
iq['psstate']['payload'] = state
result = iq.send()
if result is None or type(result) == bool or result['type'] != 'result':
log.error("Unable to change %s:%s to %s" % (node, jobid, state))
log.error("Unable to change %s:%s to %s", node, jobid, state)
return False
return True

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,29 +6,27 @@
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
def plugin_init(self):
self.xmpp.registerHandler(
Callback('Data Form',
StanzaPath('message/form'),
@@ -38,6 +36,8 @@ class xep_0004(base_plugin):
register_stanza_plugin(Form, FormField, iterable=True)
register_stanza_plugin(Message, Form)
self.xmpp['xep_0030'].add_feature('jabber:x:data')
def make_form(self, ftype='form', title='', instructions=''):
f = Form()
f['type'] = ftype
@@ -45,16 +45,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

@@ -96,11 +96,11 @@ class Form(ElementBase):
self.xml.append(itemXML)
reported_vars = self['reported'].keys()
for var in reported_vars:
fieldXML = ET.Element('{%s}field' % FormField.namespace)
itemXML.append(fieldXML)
field = FormField(xml=fieldXML)
field = FormField()
field._type = self['reported'][var]['type']
field['var'] = var
field['value'] = values.get(var, None)
itemXML.append(field.xml)
def add_reported(self, var, ftype=None, label='', desc='', **kwargs):
kwtype = kwargs.get('type', None)
@@ -159,7 +159,7 @@ class Form(ElementBase):
items = []
itemsXML = self.xml.findall('{%s}item' % self.namespace)
for itemXML in itemsXML:
item = {}
item = OrderedDict()
fieldsXML = itemXML.findall('{%s}field' % FormField.namespace)
for fieldXML in fieldsXML:
field = FormField(xml=fieldXML)
@@ -168,7 +168,7 @@ class Form(ElementBase):
return items
def get_reported(self):
fields = {}
fields = OrderedDict()
xml = self.xml.findall('{%s}reported/{%s}field' % (self.namespace,
FormField.namespace))
for field in xml:
@@ -177,7 +177,7 @@ class Form(ElementBase):
return fields
def get_values(self):
values = {}
values = OrderedDict()
fields = self['fields']
for var in fields:
values[var] = fields[var]['value']

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__)
@@ -42,46 +46,46 @@ def py2xml(*args):
def _py2xml(*args):
for x in args:
val = ET.Element("value")
val = ET.Element("{%s}value" % _namespace)
if x is None:
nil = ET.Element("nil")
nil = ET.Element("{%s}nil" % _namespace)
val.append(nil)
elif type(x) is int:
i4 = ET.Element("i4")
i4 = ET.Element("{%s}i4" % _namespace)
i4.text = str(x)
val.append(i4)
elif type(x) is bool:
boolean = ET.Element("boolean")
boolean = ET.Element("{%s}boolean" % _namespace)
boolean.text = str(int(x))
val.append(boolean)
elif type(x) is str:
string = ET.Element("string")
elif type(x) in (str, unicode):
string = ET.Element("{%s}string" % _namespace)
string.text = x
val.append(string)
elif type(x) is float:
double = ET.Element("double")
double = ET.Element("{%s}double" % _namespace)
double.text = str(x)
val.append(double)
elif type(x) is rpcbase64:
b64 = ET.Element("Base64")
b64 = ET.Element("{%s}base64" % _namespace)
b64.text = x.encoded()
val.append(b64)
elif type(x) is rpctime:
iso = ET.Element("dateTime.iso8601")
iso = ET.Element("{%s}dateTime.iso8601" % _namespace)
iso.text = str(x)
val.append(iso)
elif type(x) in (list, tuple):
array = ET.Element("array")
data = ET.Element("data")
array = ET.Element("{%s}array" % _namespace)
data = ET.Element("{%s}data" % _namespace)
for y in x:
data.append(_py2xml(y))
array.append(data)
val.append(array)
elif type(x) is dict:
struct = ET.Element("struct")
struct = ET.Element("{%s}struct" % _namespace)
for y in x.keys():
member = ET.Element("member")
name = ET.Element("name")
member = ET.Element("{%s}member" % _namespace)
name = ET.Element("{%s}name" % _namespace)
name.text = y
member.append(name)
member.append(_py2xml(x[y]))
@@ -105,15 +109,18 @@ def _xml2py(value):
if value.find('{%s}int' % namespace) is not None:
return int(value.find('{%s}int' % namespace).text)
if value.find('{%s}boolean' % namespace) is not None:
return bool(value.find('{%s}boolean' % namespace).text)
return bool(int(value.find('{%s}boolean' % namespace).text))
if value.find('{%s}string' % namespace) is not None:
return value.find('{%s}string' % namespace).text
if value.find('{%s}double' % namespace) is not None:
return float(value.find('{%s}double' % namespace).text)
if value.find('{%s}Base64') is not None:
return rpcbase64(value.find('Base64' % namespace).text)
if value.find('{%s}dateTime.iso8601') is not None:
return rpctime(value.find('{%s}dateTime.iso8601'))
if value.find('{%s}base64' % namespace) is not None:
return rpcbase64(value.find('{%s}base64' % namespace).text.encode())
if value.find('{%s}Base64' % namespace) is not None:
# Older versions of XEP-0009 used Base64
return rpcbase64(value.find('{%s}Base64' % namespace).text.encode())
if value.find('{%s}dateTime.iso8601' % namespace) is not None:
return rpctime(value.find('{%s}dateTime.iso8601' % namespace).text)
if value.find('{%s}struct' % namespace) is not None:
struct = {}
for member in value.find('{%s}struct' % namespace).findall('{%s}member' % namespace):
@@ -135,13 +142,13 @@ class rpcbase64(object):
self.data = data
def decode(self):
return base64.decodestring(self.data)
return base64.b64decode(self.data)
def __str__(self):
return self.decode()
return self.decode().decode()
def encoded(self):
return self.data
return self.data.decode()
@@ -149,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

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