Compare commits

...

143 Commits

Author SHA1 Message Date
Lance Stout
846f2bac5f Bump version to 1.1.11 2012-10-31 13:54:38 -07:00
Lance Stout
2229ad8d8e Relax timing issues in Iq timeout callback test. 2012-10-31 13:41:55 -07:00
Joe Hildebrand
61aff9f49a update JID_CACHE logic again. 2012-10-31 13:27:31 -07:00
Joe Hildebrand
67235c4214 Allow IQ timeouts to be asynchronous, by passing a timeout_callback parameter to send(). An example modification of disco is included. If this approach is approved, I'll go through and update the other plugins. 2012-10-31 13:27:06 -07:00
Lance Stout
12e8bb6ddc Turns out not all data is UTF-8, so don't try to decode it.
Fixes issue #204
2012-10-31 00:16:58 -07:00
Paul Molodowitch
52feabbe76 added setdefaultencoding method so reload(sys) not needed
reload(sys) could cause problem in user code - ie, sys.stdout, excepthook, and displayhook would be reset, etc
2012-10-24 13:06:36 -07:00
Lance Stout
a22ca228cc Lock the bound JID in the JID cache. 2012-10-24 12:56:54 -07:00
Lance Stout
d0666a5eb6 Update JID cache to do extra memoization and locking.
Passing cache_lock=True to JID() will insert the JID into the cache
and prevent it from being dropped from the cache.
2012-10-24 12:47:25 -07:00
Lance Stout
2a4e435228 Enable gevent support.
Closes issues #166 and #167

Thanks to @pvicent, @chason, and @gabriel-samfira
2012-10-24 01:20:23 -07:00
Lance Stout
c5046b9c91 Fix JID cache (wrong in-progress version comitted earlier) 2012-10-22 20:09:35 -07:00
Lance Stout
4598031dd2 Respond to probes when the subscription is 'from', not 'to'. 2012-10-22 19:22:27 -07:00
Lance Stout
5e9266ba90 Optimize generating JIDs with some caching. 2012-10-22 13:57:49 -07:00
Lance Stout
e6c95f0a2a Add support for XEP-0257: Client Certificate Management for SASL EXTERNAL 2012-10-19 00:06:45 -07:00
Lance Stout
63b58edda1 Allow passing form instructions as a list of strings. 2012-10-19 00:06:45 -07:00
Lance Stout
af9632519c Always cache published vcard 2012-10-18 12:26:50 -07:00
Lance Stout
d367fb938d Recognize plugin stanzas when they're appended. 2012-10-18 12:26:17 -07:00
Lance Stout
4190027a78 Prevent xmlns="" in stream output.
This was causing problems for HTML-IM because the HTML is parsed
without a namespaced context.

While xmlns="" technically can be valid, it's usually wrong, so this will work
for now until the HTML-IM parsing is fixed.
2012-10-15 22:22:07 -07:00
Lance Stout
ef48a8c4d9 Simplify xep-0084 avatar metadata publishing. 2012-10-15 22:20:38 -07:00
Lance Stout
829b225053 Fix vcard-temp stanzas to include organization data. 2012-10-15 22:20:13 -07:00
Lance Stout
747a6e94e6 Auto-subscribe to whitelisted JIDs if auto_subscribe is true 2012-10-15 22:19:47 -07:00
Lance Stout
cebc798e72 Merge branch 'stream_features' 2012-10-15 15:00:23 -07:00
Lance Stout
e2e8c4b5dc Remove unneeded ssl_support checks. 2012-10-10 11:42:24 -07:00
Lance Stout
675c0112ac Correct handling deleting plugins when xml:lang is active. 2012-10-10 11:07:25 -07:00
Lance Stout
4dd2c15775 Update carbons plugin to use latest spec. 2012-10-10 10:48:30 -07:00
Lance Stout
9f6decdbc1 Fix XEP-0078 error handling 2012-10-05 09:49:04 -07:00
Lance Stout
4ea328b9f2 Fix empty namespaces in XEP-0045 plugin. 2012-10-05 08:58:04 -07:00
Lance Stout
098714b3c4 Unclobber connected event handler names.
Fixes issue #199
2012-10-02 09:25:30 -07:00
Lance Stout
cf2c94d974 Add stream_negotiated event.
Fires after all stream features have been processed.
2012-10-01 16:28:31 -07:00
Lance Stout
657102e938 Update legacy auth to be used outside of stream features.
Also, add detection of legacy XMPP version.
2012-10-01 16:27:55 -07:00
Lance Stout
94488fa2ea Expand warning for missing ASN1 parser to include pyasn1_modules 2012-09-30 17:14:45 -07:00
Lance Stout
ee9c4abd08 Add support for XEP-0091: Legacy Delayed Delivery 2012-09-26 01:47:45 -07:00
Lance Stout
b5b1c932c7 Add support for XEP-0013: Flexible Offline Message Retrieval 2012-09-26 01:47:05 -07:00
Lance Stout
b8f04983e1 Allow disco queries to got to server when no JID is specified and marked not local. 2012-09-26 01:42:51 -07:00
Lance Stout
90807dd973 Fix RSM tests 2012-09-25 23:22:49 -07:00
Lance Stout
ef974114ea Add support for XEP-0313: Message Archive Management
NOTE: XEP-0313 is still very experimental, and there will likely be
      API changes in the future.
2012-09-25 20:20:43 -07:00
Lance Stout
f6e1fecdf8 Add Collector stanza handler class.
This style of handler is necessary for capturing result sets from
queries that use multiple messages to send the results instead of
in a single result stanza. Notably, XEP-0313 (MAM).
2012-09-25 20:20:22 -07:00
Lance Stout
94e8b2becf Update RSM iterator to specify where to look to count result sizes. 2012-09-25 20:17:37 -07:00
Lance Stout
a6ca6701a0 Add XEP-0308 Last Message Correction support 2012-09-25 12:35:53 -07:00
Lance Stout
c4edb9724b Fix copyright year 2012-09-25 12:27:44 -07:00
Lance Stout
b5c669bdff Add options to auto add ID values to message and presence stanzas. 2012-09-25 12:26:56 -07:00
Lance Stout
e449dce65c Fix handling forwarded stanzas to do proper lookups and deletions. 2012-09-25 12:25:45 -07:00
Lance Stout
671f680bb3 Add support for XEP-0280 Message Carbons 2012-09-25 02:34:51 -07:00
Lance Stout
dfff19ffbf Add XEP-0297: Stanza Forwarding support 2012-09-24 22:59:19 -07:00
Lance Stout
a4abdf9fa6 Fix deleting non-existent stanza plugins. 2012-09-24 21:00:23 -07:00
Lance Stout
6c57bb0553 Simplify stringifying XML 2012-09-24 20:59:51 -07:00
Lance Stout
c2ae1ee891 Remove race condition when aborting while connecting/reconnecting. 2012-09-18 10:35:53 -07:00
Lance Stout
fb3e6b7e35 Don't break checking certs for localhost. 2012-09-13 11:00:29 -07:00
Lance Stout
cf28d4586d Add support for XEP-0049: Private XML Storage 2012-09-11 20:39:32 -07:00
Lance Stout
f65eb5eeea Add support for Google's X-OAUTH2 SASL mechanism 2012-09-11 20:29:22 -07:00
Lance Stout
26fa9bd87e Don't perform caps lookup if the disco info is already known. 2012-09-11 20:28:28 -07:00
Lance Stout
0016d9a638 Add support for XEP-0279: Server IP Check 2012-09-04 20:39:43 -07:00
Lance Stout
a88b9737ff Add support for XEP-0235: OAuth over XMPP 2012-09-04 19:42:49 -07:00
Lance Stout
357406d801 Map <group /> elements with no content to '' instead of None. 2012-09-01 13:56:48 -07:00
Lance Stout
c7ec6a72cd Add catch-all chatstate event. 2012-08-24 11:47:21 -07:00
Florian Fieber
e68b07dbce Fix get_blocked() in XEP-0191 2012-08-24 11:44:56 -07:00
Lance Stout
1ca0c46333 Special plugin loading case for xep_0115 no longer needed. 2012-08-23 00:23:32 -07:00
Florian Fieber
e510875f64 Fix certificate expiration scheduler
timedelta.seconds does not store the total seconds of a time span.
Internally, seconds is the next smaller unit to days, hence
timedelta.seconds will never exceed (or reach) the number of seconds
in a day (60*60*24=86400)
2012-08-23 00:22:22 -07:00
Lance Stout
8a03bd72ae Ensure that auth is done based on the original, requested JID and not on the bound JID. 2012-08-17 10:17:35 -07:00
Lance Stout
f0e1fc5aad Fix using PLAIN over older SSL method. 2012-08-14 11:06:36 -07:00
Lance Stout
c6ac64ed2d Help prevent race condition dealing with auto_reconnect 2012-08-14 09:54:38 -07:00
Lance Stout
92be051450 Handle Iq errors/timeouts in XEP-0153 hash reset. 2012-08-13 11:09:35 -07:00
Lance Stout
779c258e27 Fix ISO date parsing fallback.
Closes issue #194
2012-08-12 22:35:42 -07:00
Lance Stout
f7a710e55b Add abort() method to kill the session and stop all processing without properly closing the stream. 2012-08-10 14:12:05 -07:00
Lance Stout
814a50e36f Fix handling state machine lock when quick exiting. 2012-08-10 14:11:44 -07:00
Lance Stout
230465b946 Fix unicode conversion utility. 2012-08-10 12:41:29 -07:00
Lance Stout
d11a67702e Exit transition immediately if already in the desired state. 2012-08-10 12:41:02 -07:00
Lance Stout
4e12e228cb Fix tracking service name for DIGEST-MD5 2012-08-10 12:40:28 -07:00
Lance Stout
4a94aeba49 Save a user's chosen, persistent nickname in the MUC roster data as 'alt_nick'
The use of <nick /> elements in MUCs is now discouraged in XEP-0172, however.
2012-08-07 19:33:17 -07:00
Lance Stout
295d23ccf3 Fix disco browser example to handle errors. 2012-08-07 16:44:52 -07:00
Lance Stout
aebcf6ff82 Re-add connection delay after exhausting DNS records. 2012-08-07 01:38:15 -07:00
Lance Stout
8c2ece3bca Ensure self._der_cert exists even if no certs are used. 2012-08-04 21:37:46 -07:00
Lance Stout
80a90a6221 Prevent auto_reconnect interference when disconnecting. 2012-08-04 21:10:45 -07:00
Lance Stout
2324c90232 Ensure default authzids are handled. 2012-08-02 13:47:06 -07:00
Lance Stout
59ff08174f Fix SASL exceptions in Py3 2012-08-01 17:43:38 -07:00
Lance Stout
b84e359770 Use the proper mappings for nodeprep. 2012-08-01 11:11:40 -07:00
Lance Stout
475ccfa8dc Use correct method for getting channel binding. 2012-08-01 09:04:58 -07:00
Lance Stout
267c24c8ef Fix encoding issue in Python3. 2012-08-01 09:04:41 -07:00
Lance Stout
1383ca19b5 Fix disco in XEP-0050 plugin.
Closes issue #191
2012-07-31 09:20:57 -07:00
Lance Stout
4c3ff2abab Add XEP-0242 plugin for 2010 Client Compliance 2012-07-30 22:07:49 -07:00
Lance Stout
7c6ef18e4f Add initial support for XEP-0016 Privacy Lists 2012-07-30 22:07:24 -07:00
Lance Stout
f8856467d5 Fix setup.py after moving SASL stuff. 2012-07-30 22:06:55 -07:00
Lance Stout
3bd84b8d27 Ignore roster updates with unrecognized subscription values. 2012-07-30 19:44:13 -07:00
Lance Stout
bc8b5774ac Fix logging of SASL errors. 2012-07-30 19:43:49 -07:00
Lance Stout
8009b0485e Add stream feature for server support of subscription pre-approvals. 2012-07-30 19:30:01 -07:00
Lance Stout
8742a56b3e Actually commit file of byte and hash utilities. 2012-07-30 19:29:33 -07:00
Lance Stout
a792bcdafe Ensure that sasl mechs that don't require security options work. 2012-07-30 19:15:10 -07:00
Lance Stout
167d1ce97b Add fields for setting client cert and key for SASL EXTERNAL. 2012-07-30 19:15:10 -07:00
Lance Stout
695cd95657 Update and integrate Suelta. 2012-07-30 19:15:10 -07:00
Lance Stout
e4b4c67637 Bump version to 1.1.10 2012-07-30 09:04:15 -07:00
Lance Stout
422e77ae40 Don't wait to retry connection if out of DNS records. 2012-07-29 17:26:04 -07:00
Lance Stout
5ae6c8f8fa Add support for XEP-0131: Standard Headers and Internet Metadata 2012-07-28 01:06:21 -07:00
Lance Stout
54656b331a Restrict caps updates to available presences (not subscriptions, etc). 2012-07-27 15:51:35 -07:00
Lance Stout
9047b627a4 Only broadcast vCard hashes for available presences (not subscriptions, etc). 2012-07-27 15:48:15 -07:00
Lance Stout
6645a3be40 Compile JID pattern regex. 2012-07-27 11:24:01 -07:00
Jonas Wielicki
e3fab66dfb Allow tasks to remove themselves during execution
The scheduler class is now capable with dealing with tasks which remove
themselves from the scheduler during execution.

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

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

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

    self.use_cdata = True

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

Also simplified related code to prefer use of JID objects instead of
strings so they don't need to be parsed multiple times.
2012-07-19 23:54:18 -07:00
Lance Stout
f6edaa56a6 Add plugin for XEP-0191: Simple Communications Blocking 2012-07-16 20:10:14 -07:00
Lance Stout
51fee28bf4 Add a warning log if dnspython is not found for SRV lookup.
Closes issue #183
2012-07-16 19:38:50 -07:00
Lance Stout
e8a3e92ceb Update plugins to use session_bind handler for disco, and use plugin_end 2012-07-10 01:37:44 -07:00
Lance Stout
5df3839b7a Add method to remove a filter. 2012-07-10 01:37:23 -07:00
Lance Stout
8dcb441f44 Add default plugin session_bind handler.
All plugins may now simply define a session_bind method where disco
features and other actions which require the bound JID may be done.
2012-07-10 01:36:21 -07:00
Lance Stout
a347cf625a Add session_bind_event threading event. 2012-07-10 01:35:57 -07:00
Lance Stout
46f49c7a12 Add method to unregister stream features. 2012-07-10 01:35:25 -07:00
Lance Stout
99701c947e Prevent None from being added to the schedule from a timing issue. 2012-07-09 22:59:26 -07:00
Lance Stout
1baae1b81e Fix issues of disco info leaking between entities with the same bare JIDs.
To ensure that disco info, or any settings which depend on the bound
JID, are correct, only set such information on or after the
session_bound event has fired.
2012-07-09 22:22:05 -07:00
Lance Stout
7d20f0e9a6 Fix missing import in xep_0256 plugin. 2012-07-09 22:21:40 -07:00
Lance Stout
fbad22a1cd Merge pull request #181 from whooo/upstream
Fix for the RSM iterator
2012-07-09 09:25:09 -07:00
Erik Larsson
5af2f62c04 Make sure that the last RSM stanza is returned from the iterator 2012-07-08 23:27:13 +02:00
Jay Farrimond
4a4a03858e dereference iq stanza only once for roster processing 2012-07-06 14:03:41 -07:00
Lance Stout
6819b57353 Handle converting None to byte data (b''). 2012-07-06 11:05:47 -07:00
Jay Farrimond
88b5e60807 only log cert errors if not handled by user 2012-07-05 13:38:26 -07:00
185 changed files with 5657 additions and 2153 deletions

View File

@@ -45,13 +45,11 @@ The latest source code for SleekXMPP may be found on `Github
``develop`` branch.
**Latest Release**
- `1.1.8 <http://github.com/fritzy/SleekXMPP/zipball/1.1.8>`_
- `1.1.10 <http://github.com/fritzy/SleekXMPP/zipball/1.1.10>`_
**Develop Releases**
- `Latest Develop Version <http://github.com/fritzy/SleekXMPP/zipball/develop>`_
**Older Stable Releases**
- `1.0 <http://github.com/fritzy/SleekXMPP/zipball/1.0>`_
Installing DNSPython
---------------------
@@ -74,6 +72,7 @@ help with SleekXMPP.
**Chat**
`sleek@conference.jabber.org <xmpp:sleek@conference.jabber.org?join>`_
Documentation and Testing
-------------------------
Documentation can be found both inline in the code, and as a Sphinx project in ``/docs``.

View File

@@ -69,8 +69,8 @@ use ASCII. We can get Python to use UTF-8 as the default encoding by including:
.. code-block:: python
if sys.version_info < (3, 0):
reload(sys)
sys.setdefaultencoding('utf8')
from sleekxmpp.util.misc_ops import setdefaultencoding
setdefaultencoding('utf8')
.. warning::

View File

@@ -21,8 +21,8 @@ import sleekxmpp
# throughout SleekXMPP, we will set the default encoding
# ourselves to UTF-8.
if sys.version_info < (3, 0):
reload(sys)
sys.setdefaultencoding('utf8')
from sleekxmpp.util.misc_ops import setdefaultencoding
setdefaultencoding('utf8')
else:
raw_input = input

View File

@@ -21,8 +21,8 @@ import sleekxmpp
# throughout SleekXMPP, we will set the default encoding
# ourselves to UTF-8.
if sys.version_info < (3, 0):
reload(sys)
sys.setdefaultencoding('utf8')
from sleekxmpp.util.misc_ops import setdefaultencoding
setdefaultencoding('utf8')
else:
raw_input = input

178
examples/admin_commands.py Executable file
View File

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

View File

@@ -28,8 +28,8 @@ from stanza import Action
# throughout SleekXMPP, we will set the default encoding
# ourselves to UTF-8.
if sys.version_info < (3, 0):
reload(sys)
sys.setdefaultencoding('utf8')
from sleekxmpp.util.misc_ops import setdefaultencoding
setdefaultencoding('utf8')
else:
raw_input = input
@@ -56,8 +56,8 @@ class ActionBot(sleekxmpp.ClientXMPP):
StanzaPath('iq@type=set/action'),
self._handle_action))
self.add_event_handler('custom_action',
self._handle_action_event,
self.add_event_handler('custom_action',
self._handle_action_event,
threaded=True)
register_stanza_plugin(Iq, Action)

View File

@@ -26,8 +26,8 @@ from stanza import Action
# throughout SleekXMPP, we will set the default encoding
# ourselves to UTF-8.
if sys.version_info < (3, 0):
reload(sys)
sys.setdefaultencoding('utf8')
from sleekxmpp.util.misc_ops import setdefaultencoding
setdefaultencoding('utf8')
else:
raw_input = input

View File

@@ -15,6 +15,7 @@ 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
@@ -22,8 +23,8 @@ import sleekxmpp
# throughout SleekXMPP, we will set the default encoding
# ourselves to UTF-8.
if sys.version_info < (3, 0):
reload(sys)
sys.setdefaultencoding('utf8')
from sleekxmpp.util.misc_ops import setdefaultencoding
setdefaultencoding('utf8')
else:
raw_input = input
@@ -83,50 +84,54 @@ class Disco(sleekxmpp.ClientXMPP):
self.get_roster()
self.send_presence()
if self.get in self.info_types:
# By using block=True, the result stanza will be
# returned. Execution will block until the reply is
# received. Non-blocking options would be to listen
# for the disco_info event, or passing a handler
# function using the callback parameter.
info = self['xep_0030'].get_info(jid=self.target_jid,
node=self.target_node,
block=True)
if self.get in self.items_types:
# The same applies from above. Listen for the
# disco_items event or pass a callback function
# if you need to process a non-blocking request.
items = self['xep_0030'].get_items(jid=self.target_jid,
node=self.target_node,
block=True)
try:
if self.get in self.info_types:
# By using block=True, the result stanza will be
# returned. Execution will block until the reply is
# received. Non-blocking options would be to listen
# for the disco_info event, or passing a handler
# function using the callback parameter.
info = self['xep_0030'].get_info(jid=self.target_jid,
node=self.target_node,
block=True)
elif self.get in self.items_types:
# The same applies from above. Listen for the
# disco_items event or pass a callback function
# if you need to process a non-blocking request.
items = self['xep_0030'].get_items(jid=self.target_jid,
node=self.target_node,
block=True)
else:
logging.error("Invalid disco request type.")
return
except IqError as e:
logging.error("Entity returned an error: %s" % e.iq['error']['condition'])
except IqTimeout:
logging.error("No response received.")
else:
logging.error("Invalid disco request type.")
self.disconnect()
return
header = 'XMPP Service Discovery: %s' % self.target_jid
print(header)
print('-' * len(header))
if self.target_node != '':
print('Node: %s' % self.target_node)
header = 'XMPP Service Discovery: %s' % self.target_jid
print(header)
print('-' * len(header))
if self.target_node != '':
print('Node: %s' % self.target_node)
print('-' * len(header))
if self.get in self.identity_types:
print('Identities:')
for identity in info['disco_info']['identities']:
print(' - %s' % str(identity))
if self.get in self.identity_types:
print('Identities:')
for identity in info['disco_info']['identities']:
print(' - %s' % str(identity))
if self.get in self.feature_types:
print('Features:')
for feature in info['disco_info']['features']:
print(' - %s' % feature)
if self.get in self.feature_types:
print('Features:')
for feature in info['disco_info']['features']:
print(' - %s' % feature)
if self.get in self.items_types:
print('Items:')
for item in items['disco_items']['items']:
print(' - %s' % str(item))
self.disconnect()
if self.get in self.items_types:
print('Items:')
for item in items['disco_items']['items']:
print(' - %s' % str(item))
finally:
self.disconnect()
if __name__ == '__main__':

View File

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

View File

@@ -21,8 +21,8 @@ import sleekxmpp
# throughout SleekXMPP, we will set the default encoding
# ourselves to UTF-8.
if sys.version_info < (3, 0):
reload(sys)
sys.setdefaultencoding('utf8')
from sleekxmpp.util.misc_ops import setdefaultencoding
setdefaultencoding('utf8')
else:
raw_input = input

View File

@@ -22,8 +22,8 @@ from sleekxmpp.componentxmpp import ComponentXMPP
# throughout SleekXMPP, we will set the default encoding
# ourselves to UTF-8.
if sys.version_info < (3, 0):
reload(sys)
sys.setdefaultencoding('utf8')
from sleekxmpp.util.misc_ops import setdefaultencoding
setdefaultencoding('utf8')
else:
raw_input = input

View File

@@ -25,8 +25,8 @@ from sleekxmpp.xmlstream import cert
# throughout SleekXMPP, we will set the default encoding
# ourselves to UTF-8.
if sys.version_info < (3, 0):
reload(sys)
sys.setdefaultencoding('utf8')
from sleekxmpp.util.misc_ops import setdefaultencoding
setdefaultencoding('utf8')
else:
raw_input = input

View File

@@ -21,8 +21,8 @@ import sleekxmpp
# throughout SleekXMPP, we will set the default encoding
# ourselves to UTF-8.
if sys.version_info < (3, 0):
reload(sys)
sys.setdefaultencoding('utf8')
from sleekxmpp.util.misc_ops import setdefaultencoding
setdefaultencoding('utf8')
else:
raw_input = input
@@ -85,7 +85,7 @@ class IBBReceiver(sleekxmpp.ClientXMPP):
def stream_opened(self, stream):
# NOTE: IBB streams are bi-directional, so the original sender is
# now the opened stream's receiver.
print('Stream opened: %s from ' % (stream.sid, stream.receiver))
print('Stream opened: %s from %s' % (stream.sid, stream.receiver))
# You could run a loop reading from the stream using stream.recv(),
# or use the ibb_stream_data event.

View File

@@ -21,8 +21,8 @@ import sleekxmpp
# throughout SleekXMPP, we will set the default encoding
# ourselves to UTF-8.
if sys.version_info < (3, 0):
reload(sys)
sys.setdefaultencoding('utf8')
from sleekxmpp.util.misc_ops import setdefaultencoding
setdefaultencoding('utf8')
else:
raw_input = input

View File

@@ -21,8 +21,8 @@ import sleekxmpp
# throughout SleekXMPP, we will set the default encoding
# ourselves to UTF-8.
if sys.version_info < (3, 0):
reload(sys)
sys.setdefaultencoding('utf8')
from sleekxmpp.util.misc_ops import setdefaultencoding
setdefaultencoding('utf8')
else:
raw_input = input

View File

@@ -21,8 +21,8 @@ import sleekxmpp
# throughout SleekXMPP, we will set the default encoding
# ourselves to UTF-8.
if sys.version_info < (3, 0):
reload(sys)
sys.setdefaultencoding('utf8')
from sleekxmpp.util.misc_ops import setdefaultencoding
setdefaultencoding('utf8')
else:
raw_input = input

View File

@@ -21,8 +21,8 @@ import sleekxmpp
# throughout SleekXMPP, we will set the default encoding
# ourselves to UTF-8.
if sys.version_info < (3, 0):
reload(sys)
sys.setdefaultencoding('utf8')
from sleekxmpp.util.misc_ops import setdefaultencoding
setdefaultencoding('utf8')
else:
raw_input = input

View File

@@ -12,8 +12,8 @@ from sleekxmpp.xmlstream import ET, tostring
# throughout SleekXMPP, we will set the default encoding
# ourselves to UTF-8.
if sys.version_info < (3, 0):
reload(sys)
sys.setdefaultencoding('utf8')
from sleekxmpp.util.misc_ops import setdefaultencoding
setdefaultencoding('utf8')
else:
raw_input = input

View File

@@ -14,8 +14,8 @@ from sleekxmpp.xmlstream.handler import Callback
# throughout SleekXMPP, we will set the default encoding
# ourselves to UTF-8.
if sys.version_info < (3, 0):
reload(sys)
sys.setdefaultencoding('utf8')
from sleekxmpp.util.misc_ops import setdefaultencoding
setdefaultencoding('utf8')
else:
raw_input = input

View File

@@ -22,8 +22,8 @@ from sleekxmpp.exceptions import IqError, IqTimeout
# throughout SleekXMPP, we will set the default encoding
# ourselves to UTF-8.
if sys.version_info < (3, 0):
reload(sys)
sys.setdefaultencoding('utf8')
from sleekxmpp.util.misc_ops import setdefaultencoding
setdefaultencoding('utf8')
else:
raw_input = input

View File

@@ -24,8 +24,8 @@ from sleekxmpp.exceptions import IqError, IqTimeout
# throughout SleekXMPP, we will set the default encoding
# ourselves to UTF-8.
if sys.version_info < (3, 0):
reload(sys)
sys.setdefaultencoding('utf8')
from sleekxmpp.util.misc_ops import setdefaultencoding
setdefaultencoding('utf8')
else:
raw_input = input

View File

@@ -21,8 +21,8 @@ import sleekxmpp
# throughout SleekXMPP, we will set the default encoding
# ourselves to UTF-8.
if sys.version_info < (3, 0):
reload(sys)
sys.setdefaultencoding('utf8')
from sleekxmpp.util.misc_ops import setdefaultencoding
setdefaultencoding('utf8')
else:
raw_input = input

174
examples/set_avatar.py Normal file
View File

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

View File

@@ -29,8 +29,8 @@ from sleekxmpp.xmlstream import JID
# throughout SleekXMPP, we will set the default encoding
# ourselves to UTF-8.
if sys.version_info < (3, 0):
reload(sys)
sys.setdefaultencoding('utf8')
from sleekxmpp.util.misc_ops import setdefaultencoding
setdefaultencoding('utf8')
else:
raw_input = input

View File

@@ -49,6 +49,8 @@ packages = [ 'sleekxmpp',
'sleekxmpp/stanza',
'sleekxmpp/test',
'sleekxmpp/roster',
'sleekxmpp/util',
'sleekxmpp/util/sasl',
'sleekxmpp/xmlstream',
'sleekxmpp/xmlstream/matcher',
'sleekxmpp/xmlstream/handler',
@@ -58,11 +60,14 @@ packages = [ 'sleekxmpp',
'sleekxmpp/plugins/xep_0009',
'sleekxmpp/plugins/xep_0009/stanza',
'sleekxmpp/plugins/xep_0012',
'sleekxmpp/plugins/xep_0013',
'sleekxmpp/plugins/xep_0016',
'sleekxmpp/plugins/xep_0027',
'sleekxmpp/plugins/xep_0030',
'sleekxmpp/plugins/xep_0030/stanza',
'sleekxmpp/plugins/xep_0033',
'sleekxmpp/plugins/xep_0047',
'sleekxmpp/plugins/xep_0049',
'sleekxmpp/plugins/xep_0050',
'sleekxmpp/plugins/xep_0054',
'sleekxmpp/plugins/xep_0059',
@@ -75,16 +80,19 @@ packages = [ 'sleekxmpp',
'sleekxmpp/plugins/xep_0084',
'sleekxmpp/plugins/xep_0085',
'sleekxmpp/plugins/xep_0086',
'sleekxmpp/plugins/xep_0091',
'sleekxmpp/plugins/xep_0092',
'sleekxmpp/plugins/xep_0107',
'sleekxmpp/plugins/xep_0108',
'sleekxmpp/plugins/xep_0115',
'sleekxmpp/plugins/xep_0118',
'sleekxmpp/plugins/xep_0128',
'sleekxmpp/plugins/xep_0131',
'sleekxmpp/plugins/xep_0153',
'sleekxmpp/plugins/xep_0172',
'sleekxmpp/plugins/xep_0184',
'sleekxmpp/plugins/xep_0186',
'sleekxmpp/plugins/xep_0191',
'sleekxmpp/plugins/xep_0198',
'sleekxmpp/plugins/xep_0199',
'sleekxmpp/plugins/xep_0202',
@@ -92,8 +100,15 @@ packages = [ 'sleekxmpp',
'sleekxmpp/plugins/xep_0221',
'sleekxmpp/plugins/xep_0224',
'sleekxmpp/plugins/xep_0231',
'sleekxmpp/plugins/xep_0235',
'sleekxmpp/plugins/xep_0249',
'sleekxmpp/plugins/xep_0257',
'sleekxmpp/plugins/xep_0258',
'sleekxmpp/plugins/xep_0279',
'sleekxmpp/plugins/xep_0280',
'sleekxmpp/plugins/xep_0297',
'sleekxmpp/plugins/xep_0308',
'sleekxmpp/plugins/xep_0313',
'sleekxmpp/features',
'sleekxmpp/features/feature_mechanisms',
'sleekxmpp/features/feature_mechanisms/stanza',
@@ -101,9 +116,8 @@ packages = [ 'sleekxmpp',
'sleekxmpp/features/feature_bind',
'sleekxmpp/features/feature_session',
'sleekxmpp/features/feature_rosterver',
'sleekxmpp/features/feature_preapproval',
'sleekxmpp/thirdparty',
'sleekxmpp/thirdparty/suelta',
'sleekxmpp/thirdparty/suelta/mechanisms',
]
setup(

View File

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

View File

@@ -99,10 +99,12 @@ class APIRegistry(object):
"""
self._setup(ctype, op)
if jid in (None, ''):
if not jid:
jid = self.xmpp.boundjid
if jid and not isinstance(jid, JID):
elif jid and not isinstance(jid, JID):
jid = JID(jid)
elif jid == JID(''):
jid = self.xmpp.boundjid
if node is None:
node = ''
@@ -113,7 +115,7 @@ class APIRegistry(object):
else:
jid = jid.full
else:
if self.settings[ctype].get('client_bare', True):
if self.settings[ctype].get('client_bare', False):
jid = jid.bare
else:
jid = jid.full

View File

@@ -16,6 +16,7 @@ from __future__ import with_statement, unicode_literals
import sys
import logging
import threading
import sleekxmpp
from sleekxmpp import plugins, features, roster
@@ -42,8 +43,8 @@ log = logging.getLogger(__name__)
# In order to make sure that Unicode is handled properly
# in Python 2.x, reset the default encoding.
if sys.version_info < (3, 0):
reload(sys)
sys.setdefaultencoding('utf8')
from sleekxmpp.util.misc_ops import setdefaultencoding
setdefaultencoding('utf8')
class BaseXMPP(XMLStream):
@@ -67,9 +68,22 @@ class BaseXMPP(XMLStream):
#: An identifier for the stream as given by the server.
self.stream_id = None
#: The JabberID (JID) used by this connection.
self.boundjid = JID(jid)
#: The JabberID (JID) requested for this connection.
self.requested_jid = JID(jid, cache_lock=True)
#: The JabberID (JID) used by this connection,
#: as set after session binding. This may even be a
#: different bare JID than what was requested.
self.boundjid = JID(jid, cache_lock=True)
self._expected_server_name = self.boundjid.host
self._redirect_attempts = 0
#: The maximum number of consecutive see-other-host
#: redirections that will be followed before quitting.
self.max_redirects = 5
self.session_bind_event = threading.Event()
#: A dictionary mapping plugin names to plugins.
self.plugin = PluginManager(self)
@@ -87,19 +101,30 @@ class BaseXMPP(XMLStream):
#: owner JIDs, as in the case for components. For clients
#: which only have a single JID, see :attr:`client_roster`.
self.roster = roster.Roster(self)
self.roster.add(self.boundjid.bare)
self.roster.add(self.boundjid)
#: The single roster for the bound JID. This is the
#: equivalent of::
#:
#: self.roster[self.boundjid.bare]
self.client_roster = self.roster[self.boundjid.bare]
self.client_roster = self.roster[self.boundjid]
#: The distinction between clients and components can be
#: important, primarily for choosing how to handle the
#: ``'to'`` and ``'from'`` JIDs of stanzas.
self.is_component = False
#: Messages may optionally be tagged with ID values. Setting
#: :attr:`use_message_ids` to `True` will assign all outgoing
#: messages an ID. Some plugin features require enabling
#: this option.
self.use_message_ids = False
#: Presence updates may optionally be tagged with ID values.
#: Setting :attr:`use_message_ids` to `True` will assign all
#: outgoing messages an ID.
self.use_presence_ids = False
#: The API registry is a way to process callbacks based on
#: JID+node combinations. Each callback in the registry is
#: marked with:
@@ -140,6 +165,8 @@ class BaseXMPP(XMLStream):
MatchXPath("{%s}error" % self.stream_ns),
self._handle_stream_error))
self.add_event_handler('session_start',
self._handle_session_start)
self.add_event_handler('disconnected',
self._handle_disconnected)
self.add_event_handler('presence_available',
@@ -185,6 +212,10 @@ class BaseXMPP(XMLStream):
self.stream_version = xml.get('version', '')
self.peer_default_lang = xml.get('{%s}lang' % XML_NS, None)
if not self.is_component and not self.stream_version:
log.warning('Legacy XMPP 0.9 protocol detected.')
self.event('legacy_protocol')
def process(self, *args, **kwargs):
"""Initialize plugins and begin processing the XML stream.
@@ -210,13 +241,6 @@ class BaseXMPP(XMLStream):
- 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 hasattr(self.plugin[name], 'post_inited'):
if hasattr(self.plugin[name], 'post_init'):
@@ -587,7 +611,7 @@ class BaseXMPP(XMLStream):
@resource.setter
def resource(self, value):
log.warning("fulljid property deprecated. Use boundjid.full")
log.warning("fulljid property deprecated. Use boundjid.resource")
self.boundjid.resource = value
@property
@@ -641,7 +665,7 @@ class BaseXMPP(XMLStream):
def set_jid(self, jid):
"""Rip a JID apart and claim it as our own."""
log.debug("setting jid to %s", jid)
self.boundjid.full = jid
self.boundjid = JID(jid, cache_lock=True)
def getjidresource(self, fulljid):
if '/' in fulljid:
@@ -652,15 +676,29 @@ class BaseXMPP(XMLStream):
def getjidbare(self, fulljid):
return fulljid.split('/', 1)[0]
def _handle_session_start(self, event):
"""Reset redirection attempt count."""
self._redirect_attempts = 0
def _handle_disconnected(self, event):
"""When disconnected, reset the roster"""
self.roster.reset()
self.session_bind_event.clear()
def _handle_stream_error(self, error):
self.event('stream_error', error)
if error['condition'] == 'see-other-host':
other_host = error['see_other_host']
if not other_host:
log.warning("No other host specified.")
return
if self._redirect_attempts > self.max_redirects:
log.error("Exceeded maximum number of redirection attempts.")
return
self._redirect_attempts += 1
host = other_host
port = 5222
@@ -686,17 +724,13 @@ class BaseXMPP(XMLStream):
msg['to'] = self.boundjid
self.event('message', msg)
def _handle_available(self, presence):
pto = presence['to'].bare
pfrom = presence['from'].bare
self.roster[pto][pfrom].handle_available(presence)
def _handle_available(self, pres):
self.roster[pres['to']][pres['from']].handle_available(pres)
def _handle_unavailable(self, presence):
pto = presence['to'].bare
pfrom = presence['from'].bare
self.roster[pto][pfrom].handle_unavailable(presence)
def _handle_unavailable(self, pres):
self.roster[pres['to']][pres['from']].handle_unavailable(pres)
def _handle_new_subscription(self, stanza):
def _handle_new_subscription(self, pres):
"""Attempt to automatically handle subscription requests.
Subscriptions will be approved if the request is from
@@ -708,10 +742,12 @@ class BaseXMPP(XMLStream):
If a subscription is accepted, a request for a mutual
subscription will be sent if :attr:`auto_subscribe` is ``True``.
"""
roster = self.roster[stanza['to'].bare]
item = self.roster[stanza['to'].bare][stanza['from'].bare]
roster = self.roster[pres['to']]
item = self.roster[pres['to']][pres['from']]
if item['whitelisted']:
item.authorize()
if roster.auto_subscribe:
item.subscribe()
elif roster.auto_authorize:
item.authorize()
if roster.auto_subscribe:
@@ -719,30 +755,20 @@ class BaseXMPP(XMLStream):
elif roster.auto_authorize == False:
item.unauthorize()
def _handle_removed_subscription(self, presence):
pto = presence['to'].bare
pfrom = presence['from'].bare
self.roster[pto][pfrom].unauthorize()
def _handle_removed_subscription(self, pres):
self.roster[pres['to']][pres['from']].handle_unauthorize(pres)
def _handle_subscribe(self, presence):
pto = presence['to'].bare
pfrom = presence['from'].bare
self.roster[pto][pfrom].handle_subscribe(presence)
def _handle_subscribe(self, pres):
self.roster[pres['to']][pres['from']].handle_subscribe(pres)
def _handle_subscribed(self, presence):
pto = presence['to'].bare
pfrom = presence['from'].bare
self.roster[pto][pfrom].handle_subscribed(presence)
def _handle_subscribed(self, pres):
self.roster[pres['to']][pres['from']].handle_subscribed(pres)
def _handle_unsubscribe(self, presence):
pto = presence['to'].bare
pfrom = presence['from'].bare
self.roster[pto][pfrom].handle_unsubscribe(presence)
def _handle_unsubscribe(self, pres):
self.roster[pres['to']][pres['from']].handle_unsubscribe(pres)
def _handle_unsubscribed(self, presence):
pto = presence['to'].bare
pfrom = presence['from'].bare
self.roster[pto][pfrom].handle_unsubscribed(presence)
def _handle_unsubscribed(self, pres):
self.roster[pres['to']][pres['from']].handle_unsubscribed(pres)
def _handle_presence(self, presence):
"""Process incoming presence stanzas.

View File

@@ -64,7 +64,6 @@ class ClientXMPP(BaseXMPP):
escape_quotes=True, sasl_mech=None, lang='en'):
BaseXMPP.__init__(self, jid, 'jabber:client')
self.set_jid(jid)
self.escape_quotes = escape_quotes
self.plugin_config = plugin_config
self.plugin_whitelist = plugin_whitelist
@@ -95,7 +94,7 @@ class ClientXMPP(BaseXMPP):
self.bound = False
self.bindfail = False
self.add_event_handler('connected', self._handle_connected)
self.add_event_handler('connected', self._reset_connection_state)
self.add_event_handler('session_bind', self._handle_session_bind)
self.register_stanza(StreamFeatures)
@@ -113,9 +112,10 @@ class ClientXMPP(BaseXMPP):
self.register_plugin('feature_starttls')
self.register_plugin('feature_bind')
self.register_plugin('feature_session')
self.register_plugin('feature_rosterver')
self.register_plugin('feature_preapproval')
self.register_plugin('feature_mechanisms',
pconfig={'use_mech': sasl_mech} if sasl_mech else None)
self.register_plugin('feature_rosterver')
@property
def password(self):
@@ -173,8 +173,13 @@ class ClientXMPP(BaseXMPP):
self._stream_feature_order.append((order, name))
self._stream_feature_order.sort()
def update_roster(self, jid, name=None, subscription=None, groups=[],
block=True, timeout=None, callback=None):
def unregister_feature(self, name, order):
if name in self._stream_feature_handlers:
del self._stream_feature_handlers[name]
self._stream_feature_order.remove((order, name))
self._stream_feature_order.sort()
def update_roster(self, jid, **kwargs):
"""Add or change a roster item.
:param jid: The JID of the entry to modify.
@@ -195,6 +200,16 @@ class ClientXMPP(BaseXMPP):
Will be executed when the roster is received.
Implies ``block=False``.
"""
current = self.client_roster[jid]
name = kwargs.get('name', current['name'])
subscription = kwargs.get('subscription', current['subscription'])
groups = kwargs.get('groups', current['groups'])
block = kwargs.get('block', True)
timeout = kwargs.get('timeout', None)
callback = kwargs.get('callback', None)
return self.client_roster.update(jid, name, subscription, groups,
block, timeout, callback)
@@ -237,7 +252,7 @@ class ClientXMPP(BaseXMPP):
self._handle_roster(response)
return response
def _handle_connected(self, event=None):
def _reset_connection_state(self, event=None):
#TODO: Use stream state here
self.authenticated = False
self.sessionstarted = False
@@ -257,6 +272,8 @@ class ClientXMPP(BaseXMPP):
# Don't continue if the feature requires
# restarting the XML stream.
return True
log.debug('Finished processing stream features.')
self.event('stream_negotiated')
def _handle_roster(self, iq):
"""Update the roster after receiving a roster stanza.
@@ -270,15 +287,18 @@ class ClientXMPP(BaseXMPP):
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')
items = iq['roster']['items']
roster[jid].save(remove=(item['subscription'] == 'remove'))
valid_subscriptions = ('to', 'from', 'both', 'none', 'remove')
for jid, item in items.items():
if item['subscription'] in valid_subscriptions:
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':

View File

@@ -156,10 +156,10 @@ class ComponentXMPP(BaseXMPP):
:param xml: The reply handshake stanza.
"""
self.session_bind_event.set()
self.session_started_event.set()
self.event("session_bind", self.boundjid, direct=True)
self.event("session_start")
def _handle_probe(self, presence):
pto = presence['to'].bare
pfrom = presence['from'].bare
self.roster[pto][pfrom].handle_probe(presence)
def _handle_probe(self, pres):
self.roster[pres['to']][pres['from']].handle_probe(pres)

View File

@@ -11,5 +11,6 @@ __all__ = [
'feature_mechanisms',
'feature_bind',
'feature_session',
'feature_rosterver'
'feature_rosterver',
'feature_preapproval'
]

View File

@@ -8,6 +8,7 @@
import logging
from sleekxmpp.jid import JID
from sleekxmpp.stanza import Iq, StreamFeatures
from sleekxmpp.features.feature_bind import stanza
from sleekxmpp.xmlstream import register_stanza_plugin
@@ -48,9 +49,10 @@ class FeatureBind(BasePlugin):
iq['bind']['resource'] = self.xmpp.boundjid.resource
response = iq.send(now=True)
self.xmpp.set_jid(response['bind']['jid'])
self.xmpp.boundjid = JID(response['bind']['jid'], cache_lock=True)
self.xmpp.bound = True
self.xmpp.event('session_bind', self.xmpp.boundjid, direct=True)
self.xmpp.session_bind_event.set()
self.xmpp.features.add('bind')

View File

@@ -6,12 +6,12 @@
See the file LICENSE for copying permission.
"""
import sys
import ssl
import logging
from sleekxmpp.thirdparty import suelta
from sleekxmpp.thirdparty.suelta.exceptions import SASLCancelled, SASLError
from sleekxmpp.thirdparty.suelta.exceptions import SASLPrepFailure
from sleekxmpp.util import sasl
from sleekxmpp.util.stringprep_profiles import StringPrepError
from sleekxmpp.stanza import StreamFeatures
from sleekxmpp.xmlstream import RestartStream, register_stanza_plugin
from sleekxmpp.plugins import BasePlugin
@@ -29,42 +29,31 @@ class FeatureMechanisms(BasePlugin):
description = 'RFC 6120: Stream Feature: SASL'
dependencies = set()
stanza = stanza
default_config = {
'use_mech': None,
'use_mechs': None,
'min_mech': None,
'sasl_callback': None,
'security_callback': None,
'encrypted_plain': True,
'unencrypted_plain': False,
'unencrypted_digest': False,
'unencrypted_cram': False,
'unencrypted_scram': True,
'order': 100
}
def plugin_init(self):
self.use_mech = self.config.get('use_mech', None)
if not self.use_mech and not self.xmpp.boundjid.user:
if not self.use_mech and not self.xmpp.requested_jid.user:
self.use_mech = 'ANONYMOUS'
def tls_active():
return 'starttls' in self.xmpp.features
if self.sasl_callback is None:
self.sasl_callback = self._default_credentials
def basic_callback(mech, values):
creds = self.xmpp.credentials
for value in values:
if value == 'username':
values['username'] = self.xmpp.boundjid.user
elif value == 'password':
values['password'] = creds['password']
elif value == 'email':
jid = self.xmpp.boundjid.bare
values['email'] = creds.get('email', jid)
elif value in creds:
values[value] = creds[value]
mech.fulfill(values)
sasl_callback = self.config.get('sasl_callback', None)
if sasl_callback is None:
sasl_callback = basic_callback
if self.security_callback is None:
self.security_callback = self._default_security
self.mech = None
self.sasl = suelta.SASL(self.xmpp.boundjid.domain, 'xmpp',
username=self.xmpp.boundjid.user,
sec_query=suelta.sec_query_allow,
request_values=sasl_callback,
tls_active=tls_active,
mech=self.use_mech)
self.mech_list = set()
self.attempted_mechs = set()
@@ -95,7 +84,52 @@ class FeatureMechanisms(BasePlugin):
self.xmpp.register_feature('mechanisms',
self._handle_sasl_auth,
restart=True,
order=self.config.get('order', 100))
order=self.order)
def _default_credentials(self, required_values, optional_values):
creds = self.xmpp.credentials
result = {}
values = required_values.union(optional_values)
for value in values:
if value == 'username':
result[value] = self.xmpp.requested_jid.user
elif value == 'password':
result[value] = creds['password']
elif value == 'authzid':
result[value] = creds.get('authzid', '')
elif value == 'email':
jid = self.xmpp.requested_jid.bare
result[value] = creds.get('email', jid)
elif value == 'channel_binding':
if sys.version_info >= (3, 3):
result[value] = self.xmpp.socket.get_channel_binding()
else:
result[value] = None
elif value == 'host':
result[value] = self.xmpp.requested_jid.domain
elif value == 'realm':
result[value] = self.xmpp.requested_jid.domain
elif value == 'service-name':
result[value] = self.xmpp._service_name
elif value == 'service':
result[value] = 'xmpp'
elif value in creds:
result[value] = creds[value]
return result
def _default_security(self, values):
result = {}
for value in values:
if value == 'encrypted':
if 'starttls' in self.xmpp.features:
result[value] = True
elif isinstance(self.xmpp.socket, ssl.SSLSocket):
result[value] = True
else:
result[value] = False
else:
result[value] = self.config.get(value, False)
return result
def _handle_sasl_auth(self, features):
"""
@@ -109,37 +143,61 @@ class FeatureMechanisms(BasePlugin):
# server has incorrectly offered it again.
return False
if not self.use_mech:
self.mech_list = set(features['mechanisms'])
else:
self.mech_list = set([self.use_mech])
enforce_limit = False
limited_mechs = self.use_mechs
if limited_mechs is None:
limited_mechs = set()
elif limited_mechs and not isinstance(limited_mechs, set):
limited_mechs = set(limited_mechs)
enforce_limit = True
if self.use_mech:
limited_mechs.add(self.use_mech)
enforce_limit = True
if enforce_limit:
self.use_mechs = limited_mechs
self.mech_list = set(features['mechanisms'])
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 mech_list and self.mech is not None:
resp = stanza.Auth(self.xmpp)
resp['mechanism'] = self.mech.name
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:
try:
self.mech = sasl.choose(mech_list,
self.sasl_callback,
self.security_callback,
limit=self.use_mechs,
min_mech=self.min_mech)
except sasl.SASLNoAppropriateMechanism:
log.error("No appropriate login method.")
self.xmpp.event("no_auth", direct=True)
self.attempted_mechs = set()
return self.xmpp.disconnect()
resp = stanza.Auth(self.xmpp)
resp['mechanism'] = self.mech.name
try:
resp['value'] = self.mech.process()
except sasl.SASLCancelled:
self.attempted_mechs.add(self.mech.name)
self._send_auth()
except sasl.SASLFailed:
self.attempted_mechs.add(self.mech.name)
self._send_auth()
except sasl.SASLMutualAuthFailed:
log.error("Mutual authentication failed! " + \
"A security breach is possible.")
self.attempted_mechs.add(self.mech.name)
self.xmpp.disconnect()
except StringPrepError:
log.exception("A credential value did not pass SASLprep.")
self.xmpp.disconnect()
else:
resp.send(now=True)
return True
def _handle_challenge(self, stanza):
@@ -147,20 +205,33 @@ class FeatureMechanisms(BasePlugin):
resp = self.stanza.Response(self.xmpp)
try:
resp['value'] = self.mech.process(stanza['value'])
except SASLCancelled:
except sasl.SASLCancelled:
self.stanza.Abort(self.xmpp).send()
except SASLError:
except sasl.SASLFailed:
self.stanza.Abort(self.xmpp).send()
except sasl.SASLMutualAuthFailed:
log.error("Mutual authentication failed! " + \
"A security breach is possible.")
self.attempted_mechs.add(self.mech.name)
self.xmpp.disconnect()
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()
try:
final = self.mech.process(stanza['value'])
except sasl.SASLMutualAuthFailed:
log.error("Mutual authentication failed! " + \
"A security breach is possible.")
self.attempted_mechs.add(self.mech.name)
self.xmpp.disconnect()
else:
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."""

View File

@@ -8,8 +8,7 @@
import base64
from sleekxmpp.thirdparty.suelta.util import bytes
from sleekxmpp.util import bytes
from sleekxmpp.xmlstream import StanzaBase

View File

@@ -8,8 +8,7 @@
import base64
from sleekxmpp.thirdparty.suelta.util import bytes
from sleekxmpp.util import bytes
from sleekxmpp.xmlstream import StanzaBase

View File

@@ -8,8 +8,7 @@
import base64
from sleekxmpp.thirdparty.suelta.util import bytes
from sleekxmpp.util import bytes
from sleekxmpp.xmlstream import StanzaBase

View File

@@ -6,8 +6,10 @@
See the file LICENSE for copying permission.
"""
from sleekxmpp.xmlstream import StanzaBase
import base64
from sleekxmpp.util import bytes
from sleekxmpp.xmlstream import StanzaBase
class Success(StanzaBase):
@@ -16,9 +18,21 @@ class Success(StanzaBase):
name = 'success'
namespace = 'urn:ietf:params:xml:ns:xmpp-sasl'
interfaces = set()
interfaces = set(['value'])
plugin_attrib = name
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))
def set_value(self, values):
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

@@ -0,0 +1,15 @@
"""
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_preapproval.preapproval import FeaturePreApproval
from sleekxmpp.features.feature_preapproval.stanza import PreApproval
register_plugin(FeaturePreApproval)

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_preapproval import stanza
from sleekxmpp.xmlstream import register_stanza_plugin
from sleekxmpp.plugins.base import BasePlugin
log = logging.getLogger(__name__)
class FeaturePreApproval(BasePlugin):
name = 'feature_preapproval'
description = 'RFC 6121: Stream Feature: Subscription Pre-Approval'
dependences = set()
stanza = stanza
def plugin_init(self):
self.xmpp.register_feature('preapproval',
self._handle_preapproval,
restart=False,
order=9001)
register_stanza_plugin(StreamFeatures, stanza.PreApproval)
def _handle_preapproval(self, features):
"""Save notice that the server support subscription pre-approvals.
Arguments:
features -- The stream features stanza.
"""
log.debug("Server supports subscription pre-approvals.")
self.xmpp.features.add('preapproval')

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 PreApproval(ElementBase):
name = 'sub'
namespace = 'urn:xmpp:features:pre-approval'
interfaces = set()
plugin_attrib = 'preapproval'

View File

@@ -54,13 +54,9 @@ class FeatureSTARTTLS(BasePlugin):
return False
elif not self.xmpp.use_tls:
return False
elif self.xmpp.ssl_support:
else:
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.")
return False
def _handle_starttls_proceed(self, proceed):
"""Restart the XML stream when TLS is accepted."""

582
sleekxmpp/jid.py Normal file
View File

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

View File

@@ -18,11 +18,14 @@ __all__ = [
'xep_0004', # Data Forms
'xep_0009', # Jabber-RPC
'xep_0012', # Last Activity
'xep_0013', # Flexible Offline Message Retrieval
'xep_0016', # Privacy Lists
'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_0049', # Private XML Storage
'xep_0050', # Ad-hoc Commands
'xep_0054', # vcard-temp
'xep_0059', # Result Set Management
@@ -35,17 +38,22 @@ __all__ = [
'xep_0084', # User Avatar
'xep_0085', # Chat State Notifications
'xep_0086', # Legacy Error Codes
'xep_0091', # Legacy Delayed Delivery
'xep_0092', # Software Version
'xep_0106', # JID Escaping
'xep_0107', # User Mood
'xep_0108', # User Activity
'xep_0115', # Entity Capabilities
'xep_0118', # User Tune
'xep_0128', # Extended Service Discovery
'xep_0131', # Standard Headers and Internet Metadata
'xep_0133', # Service Administration
'xep_0153', # vCard-Based Avatars
'xep_0163', # Personal Eventing Protocol
'xep_0172', # User Nickname
'xep_0184', # Message Receipts
'xep_0186', # Invisible Command
'xep_0191', # Blocking Command
'xep_0198', # Stream Management
'xep_0199', # Ping
'xep_0202', # Entity Time
@@ -55,9 +63,17 @@ __all__ = [
'xep_0223', # Persistent Storage of Private Data via Pubsub
'xep_0224', # Attention
'xep_0231', # Bits of Binary
'xep_0235', # OAuth Over XMPP
'xep_0242', # XMPP Client Compliance 2009
'xep_0249', # Direct MUC Invitations
'xep_0256', # Last Activity in Presence
'xep_0257', # Client Certificate Management for SASL EXTERNAL
'xep_0258', # Security Labels in XMPP
'xep_0270', # XMPP Compliance Suites 2010
'xep_0279', # Server IP Check
'xep_0280', # Message Carbons
'xep_0297', # Stanza Forwarding
'xep_0302', # XMPP Compliance Suites 2012
'xep_0308', # Last Message Correction
'xep_0313', # Message Archive Management
]

View File

@@ -14,6 +14,7 @@
"""
import sys
import copy
import logging
import threading
@@ -167,8 +168,7 @@ class PluginManager(object):
self._plugins[name] = plugin
for dep in plugin.dependencies:
self.enable(dep, enabled=enabled)
plugin.plugin_init()
log.debug("Loaded Plugin: %s", plugin.description)
plugin._init()
if top_level:
for name in enabled:
@@ -229,7 +229,7 @@ class PluginManager(object):
raise PluginNotFound(name)
for dep in PLUGIN_DEPENDENTS[name]:
self.disable(dep, _disabled)
plugin.plugin_end()
plugin._end()
if name in self._enabled:
self._enabled.remove(name)
del self._plugins[name]
@@ -273,6 +273,14 @@ class BasePlugin(object):
#: be initialized as needed if this plugin is enabled.
dependencies = set()
#: The basic, standard configuration for the plugin, which may
#: be overridden when initializing the plugin. The configuration
#: fields included here may be accessed directly as attributes of
#: the plugin. For example, including the configuration field 'foo'
#: would mean accessing `plugin.foo` returns the current value of
#: `plugin.config['foo']`.
default_config = {}
def __init__(self, xmpp, config=None):
self.xmpp = xmpp
if self.xmpp:
@@ -280,7 +288,54 @@ class BasePlugin(object):
#: 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 {}
self.config = copy.copy(self.default_config)
if config:
self.config.update(config)
def __getattr__(self, key):
"""Provide direct access to configuration fields.
If the standard configuration includes the option `'foo'`, then
accessing `self.foo` should be the same as `self.config['foo']`.
"""
if key in self.default_config:
return self.config.get(key, None)
else:
return object.__getattribute__(self, key)
def __setattr__(self, key, value):
"""Provide direct assignment to configuration fields.
If the standard configuration includes the option `'foo'`, then
assigning to `self.foo` should be the same as assigning to
`self.config['foo']`.
"""
if key in self.default_config:
self.config[key] = value
else:
super(BasePlugin, self).__setattr__(key, value)
def _init(self):
"""Initialize plugin state, such as registering event handlers.
Also sets up required event handlers.
"""
if self.xmpp is not None:
self.xmpp.add_event_handler('session_bind', self.session_bind)
if self.xmpp.session_bind_event.is_set():
self.session_bind(self.xmpp.boundjid.full)
self.plugin_init()
log.debug('Loaded Plugin: %s', self.description)
def _end(self):
"""Cleanup plugin state, and prepare for plugin removal.
Also removes required event handlers.
"""
if self.xmpp is not None:
self.xmpp.del_event_handler('session_bind', self.session_bind)
self.plugin_end()
log.debug('Disabled Plugin: %s' % self.description)
def plugin_init(self):
"""Initialize plugin state, such as registering event handlers."""
@@ -290,6 +345,10 @@ class BasePlugin(object):
"""Cleanup plugin state, and prepare for plugin removal."""
pass
def session_bind(self, jid):
"""Initialize plugin state based on the bound JID."""
pass
def post_init(self):
"""Initialize any cross-plugin state.

View File

@@ -27,7 +27,7 @@ class XEP_0004(BasePlugin):
stanza = stanza
def plugin_init(self):
self.xmpp.registerHandler(
self.xmpp.register_handler(
Callback('Data Form',
StanzaPath('message/form'),
self.handle_form))
@@ -36,6 +36,11 @@ class XEP_0004(BasePlugin):
register_stanza_plugin(Form, FormField, iterable=True)
register_stanza_plugin(Message, Form)
def plugin_end(self):
self.xmpp.remove_handler('Data Form')
self.xmpp['xep_0030'].del_feature(feature='jabber:x:data')
def session_bind(self, jid):
self.xmpp['xep_0030'].add_feature('jabber:x:data')
def make_form(self, ftype='form', title='', instructions=''):

View File

@@ -201,7 +201,8 @@ class Form(ElementBase):
del self['instructions']
if instructions in [None, '']:
return
instructions = instructions.split('\n')
if not isinstance(instructions, list):
instructions = instructions.split('\n')
for instruction in instructions:
inst = ET.Element('{%s}instructions' % self.namespace)
inst.text = instruction

View File

@@ -37,13 +37,11 @@ class XEP_0012(BasePlugin):
self._last_activities = {}
self.xmpp.registerHandler(
self.xmpp.register_handler(
Callback('Last Activity',
StanzaPath('iq@type=get/last_activity'),
self._handle_get_last_activity))
self.xmpp.plugin['xep_0030'].add_feature('jabber:iq:last')
self.api.register(self._default_get_last_activity,
'get_last_activity',
default=True)
@@ -54,6 +52,13 @@ class XEP_0012(BasePlugin):
'del_last_activity',
default=True)
def plugin_end(self):
self.xmpp.remove_handler('Last Activity')
self.xmpp['xep_0030'].del_feature(feature='jabber:iq:last')
def session_bind(self, jid):
self.xmpp['xep_0030'].add_feature('jabber:iq:last')
def begin_idle(self, jid=None, status=None):
self.set_last_activity(jid, 0, status)

View File

@@ -0,0 +1,15 @@
"""
SleekXMPP: The Sleek XMPP Library
Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
This file is part of SleekXMPP.
See the file LICENSE for copying permissio
"""
from sleekxmpp.plugins.base import register_plugin
from sleekxmpp.plugins.xep_0013.stanza import Offline
from sleekxmpp.plugins.xep_0013.offline import XEP_0013
register_plugin(XEP_0013)

View File

@@ -0,0 +1,134 @@
"""
SleekXMPP: The Sleek XMPP Library
Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
This file is part of SleekXMPP.
See the file LICENSE for copying permissio
"""
import logging
import sleekxmpp
from sleekxmpp.stanza import Message, Iq
from sleekxmpp.exceptions import XMPPError
from sleekxmpp.xmlstream.handler import Collector
from sleekxmpp.xmlstream.matcher import StanzaPath
from sleekxmpp.xmlstream import register_stanza_plugin
from sleekxmpp.plugins import BasePlugin
from sleekxmpp.plugins.xep_0013 import stanza
log = logging.getLogger(__name__)
class XEP_0013(BasePlugin):
"""
XEP-0013 Flexible Offline Message Retrieval
"""
name = 'xep_0013'
description = 'XEP-0013: Flexible Offline Message Retrieval'
dependencies = set(['xep_0030'])
stanza = stanza
def plugin_init(self):
register_stanza_plugin(Iq, stanza.Offline)
register_stanza_plugin(Message, stanza.Offline)
def get_count(self, **kwargs):
return self.xmpp['xep_0030'].get_info(
node='http://jabber.org/protocol/offline',
local=False,
**kwargs)
def get_headers(self, **kwargs):
return self.xmpp['xep_0030'].get_items(
node='http://jabber.org/protocol/offline',
local=False,
**kwargs)
def view(self, nodes, ifrom=None, block=True, timeout=None, callback=None):
if not isinstance(nodes, (list, set)):
nodes = [nodes]
iq = self.xmpp.Iq()
iq['type'] = 'get'
iq['from'] = ifrom
offline = iq['offline']
for node in nodes:
item = stanza.Item()
item['node'] = node
item['action'] = 'view'
offline.append(item)
collector = Collector(
'Offline_Results_%s' % iq['id'],
StanzaPath('message/offline'))
self.xmpp.register_handler(collector)
if not block and callback is not None:
def wrapped_cb(iq):
results = collector.stop()
if iq['type'] == 'result':
iq['offline']['results'] = results
callback(iq)
return iq.send(block=block, timeout=timeout, callback=wrapped_cb)
else:
try:
resp = iq.send(block=block, timeout=timeout, callback=callback)
resp['offline']['results'] = collector.stop()
return resp
except XMPPError as e:
collector.stop()
raise e
def remove(self, nodes, ifrom=None, block=True, timeout=None, callback=None):
if not isinstance(nodes, (list, set)):
nodes = [nodes]
iq = self.xmpp.Iq()
iq['type'] = 'set'
iq['from'] = ifrom
offline = iq['offline']
for node in nodes:
item = stanza.Item()
item['node'] = node
item['action'] = 'remove'
offline.append(item)
return iq.send(block=block, timeout=timeout, callback=callback)
def fetch(self, ifrom=None, block=True, timeout=None, callback=None):
iq = self.xmpp.Iq()
iq['type'] = 'set'
iq['from'] = ifrom
iq['offline']['fetch'] = True
collector = Collector(
'Offline_Results_%s' % iq['id'],
StanzaPath('message/offline'))
self.xmpp.register_handler(collector)
if not block and callback is not None:
def wrapped_cb(iq):
results = collector.stop()
if iq['type'] == 'result':
iq['offline']['results'] = results
callback(iq)
return iq.send(block=block, timeout=timeout, callback=wrapped_cb)
else:
try:
resp = iq.send(block=block, timeout=timeout, callback=callback)
resp['offline']['results'] = collector.stop()
return resp
except XMPPError as e:
collector.stop()
raise e
def purge(self, ifrom=None, block=True, timeout=None, callback=None):
iq = self.xmpp.Iq()
iq['type'] = 'set'
iq['from'] = ifrom
iq['offline']['purge'] = True
return iq.send(block=block, timeout=timeout, callback=callback)

View File

@@ -0,0 +1,53 @@
"""
SleekXMPP: The Sleek XMPP Library
Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
This file is part of SleekXMPP.
See the file LICENSE for copying permissio
"""
from sleekxmpp.jid import JID
from sleekxmpp.xmlstream import ElementBase, register_stanza_plugin
class Offline(ElementBase):
name = 'offline'
namespace = 'http://jabber.org/protocol/offline'
plugin_attrib = 'offline'
interfaces = set(['fetch', 'purge', 'results'])
bool_interfaces = interfaces
def setup(self, xml=None):
ElementBase.setup(self, xml)
self._results = []
# The results interface is meant only as an easy
# way to access the set of collected message responses
# from the query.
def get_results(self):
return self._results
def set_results(self, values):
self._results = values
def del_results(self):
self._results = []
class Item(ElementBase):
name = 'item'
namespace = 'http://jabber.org/protocol/offline'
plugin_attrib = 'item'
interfaces = set(['action', 'node', 'jid'])
actions = set(['view', 'remove'])
def get_jid(self):
return JID(self._get_attr('jid'))
def set_jid(self, value):
self._set_attr('jid', str(value))
register_stanza_plugin(Offline, Item, iterable=True)

View File

@@ -0,0 +1,16 @@
"""
SleekXMPP: The Sleek XMPP Library
Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
This file is part of SleekXMPP.
See the file LICENSE for copying permission.
"""
from sleekxmpp.plugins.base import register_plugin
from sleekxmpp.plugins.xep_0016 import stanza
from sleekxmpp.plugins.xep_0016.stanza import Privacy
from sleekxmpp.plugins.xep_0016.privacy import XEP_0016
register_plugin(XEP_0016)

View File

@@ -0,0 +1,110 @@
"""
SleekXMPP: The Sleek XMPP Library
Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout
This file is part of SleekXMPP.
See the file LICENSE for copying permission.
"""
from sleekxmpp import Iq
from sleekxmpp.xmlstream import register_stanza_plugin
from sleekxmpp.plugins import BasePlugin
from sleekxmpp.plugins.xep_0016 import stanza
from sleekxmpp.plugins.xep_0016.stanza import Privacy, Item
class XEP_0016(BasePlugin):
name = 'xep_0016'
description = 'XEP-0016: Privacy Lists'
dependencies = set(['xep_0030'])
stanza = stanza
def plugin_init(self):
register_stanza_plugin(Iq, Privacy)
def plugin_end(self):
self.xmpp['xep_0030'].del_feature(feature=Privacy.namespace)
def session_bind(self, jid):
self.xmpp['xep_0030'].add_feature(Privacy.namespace)
def get_privacy_lists(self, block=True, timeout=None, callback=None):
iq = self.xmpp.Iq()
iq['type'] = 'get'
iq.enable('privacy')
return iq.send(block=block, timeout=timeout, callback=callback)
def get_list(self, name, block=True, timeout=None, callback=None):
iq = self.xmpp.Iq()
iq['type'] = 'get'
iq['privacy']['list']['name'] = name
return iq.send(block=block, timeout=timeout, callback=callback)
def get_active(self, block=True, timeout=None, callback=None):
iq = self.xmpp.Iq()
iq['type'] = 'get'
iq['privacy'].enable('active')
return iq.send(block=block, timeout=timeout, callback=callback)
def get_default(self, block=True, timeout=None, callback=None):
iq = self.xmpp.Iq()
iq['type'] = 'get'
iq['privacy'].enable('default')
return iq.send(block=block, timeout=timeout, callback=callback)
def activate(self, name, block=True, timeout=None, callback=None):
iq = self.xmpp.Iq()
iq['type'] = 'set'
iq['privacy']['active']['name'] = name
return iq.send(block=block, timeout=timeout, callback=callback)
def deactivate(self, block=True, timeout=None, callback=None):
iq = self.xmpp.Iq()
iq['type'] = 'set'
iq['privacy'].enable('active')
return iq.send(block=block, timeout=timeout, callback=callback)
def make_default(self, name, block=True, timeout=None, callback=None):
iq = self.xmpp.Iq()
iq['type'] = 'set'
iq['privacy']['default']['name'] = name
return iq.send(block=block, timeout=timeout, callback=callback)
def remove_default(self, block=True, timeout=None, callback=None):
iq = self.xmpp.Iq()
iq['type'] = 'set'
iq['privacy'].enable('default')
return iq.send(block=block, timeout=timeout, callback=callback)
def edit_list(self, name, rules, block=True, timeout=None, callback=None):
iq = self.xmpp.Iq()
iq['type'] = 'set'
iq['privacy']['list']['name'] = name
priv_list = iq['privacy']['list']
if not rules:
rules = []
for rule in rules:
if isinstance(rule, Item):
priv_list.append(rule)
continue
priv_list.add_item(
rule['value'],
rule['action'],
rule['order'],
itype=rule.get('type', None),
iq=rule.get('iq', None),
message=rule.get('message', None),
presence_in=rule.get('presence_in',
rule.get('presence-in', None)),
presence_out=rule.get('presence_out',
rule.get('presence-out', None)))
def remove_list(self, name, block=True, timeout=None, callback=None):
iq = self.xmpp.Iq()
iq['type'] = 'set'
iq['privacy']['list']['name'] = name
return iq.send(block=block, timeout=timeout, callback=callback)

View File

@@ -0,0 +1,103 @@
from sleekxmpp.xmlstream import ET, ElementBase, register_stanza_plugin
class Privacy(ElementBase):
name = 'query'
namespace = 'jabber:iq:privacy'
plugin_attrib = 'privacy'
interfaces = set()
def add_list(self, name):
priv_list = List()
priv_list['name'] = name
self.append(priv_list)
return priv_list
class Active(ElementBase):
name = 'active'
namespace = 'jabber:iq:privacy'
plugin_attrib = name
interfaces = set(['name'])
class Default(ElementBase):
name = 'default'
namespace = 'jabber:iq:privacy'
plugin_attrib = name
interfaces = set(['name'])
class List(ElementBase):
name = 'list'
namespace = 'jabber:iq:privacy'
plugin_attrib = name
plugin_multi_attrib = 'lists'
interfaces = set(['name'])
def add_item(self, value, action, order, itype=None, iq=False,
message=False, presence_in=False, presence_out=False):
item = Item()
item.values = {'type': itype,
'value': value,
'action': action,
'order': order,
'message': message,
'iq': iq,
'presence_in': presence_in,
'presence_out': presence_out}
self.append(item)
return item
class Item(ElementBase):
name = 'item'
namespace = 'jabber:iq:privacy'
plugin_attrib = name
plugin_multi_attrib = 'items'
interfaces = set(['type', 'value', 'action', 'order', 'iq',
'message', 'presence_in', 'presence_out'])
bool_interfaces = set(['message', 'iq', 'presence_in', 'presence_out'])
type_values = ('', 'jid', 'group', 'subscription')
action_values = ('allow', 'deny')
def set_type(self, value):
if value and value not in self.type_values:
raise ValueError('Unknown type value: %s' % value)
else:
self._set_attr('type', value)
def set_action(self, value):
if value not in self.action_values:
raise ValueError('Unknown action value: %s' % value)
else:
self._set_attr('action', value)
def set_presence_in(self, value):
keep = True if value else False
self._set_sub_text('presence-in', '', keep=keep)
def get_presence_in(self):
pres = self.xml.find('{%s}presence-in' % self.namespace)
return pres is not None
def del_presence_in(self):
self._del_sub('{%s}presence-in' % self.namespace)
def set_presence_out(self, value):
keep = True if value else False
self._set_sub_text('presence-in', '', keep=keep)
def get_presence_out(self):
pres = self.xml.find('{%s}presence-in' % self.namespace)
return pres is not None
def del_presence_out(self):
self._del_sub('{%s}presence-in' % self.namespace)
register_stanza_plugin(Privacy, Active)
register_stanza_plugin(Privacy, Default)
register_stanza_plugin(Privacy, List, iterable=True)
register_stanza_plugin(List, Item, iterable=True)

View File

@@ -24,7 +24,7 @@ def _extract_data(data, kind):
if not begin_headers and 'BEGIN PGP %s' % kind in line:
begin_headers = True
continue
if begin_headers and line == '':
if begin_headers and line.stripped() == '':
begin_data = True
continue
if 'END PGP %s' % kind in line:
@@ -40,14 +40,15 @@ class XEP_0027(BasePlugin):
description = 'XEP-0027: Current Jabber OpenPGP Usage'
dependencies = set()
stanza = stanza
default_config = {
'gpg_binary': 'gpg',
'gpg_home': '',
'use_agent': True,
'keyring': None,
'key_server': 'pgp.mit.edu'
}
def plugin_init(self):
self.gpg_binary = self.config.get('gpg_binary', 'gpg')
self.gpg_home = self.config.get('gpg_home', '')
self.use_agent = self.config.get('use_agent', True)
self.keyring = self.config.get('keyring', None)
self.key_server = self.config.get('key_server', 'pgp.mit.edu')
self.gpg = GPG(gnupghome=self.gpg_home,
gpgbinary=self.gpg_binary,
use_agent=self.use_agent,
@@ -79,6 +80,13 @@ class XEP_0027(BasePlugin):
StanzaPath('message/encrypted'),
self._handle_encrypted_message))
def plugin_end(self):
self.xmpp.remove_handler('Encrypted Message')
self.xmpp.remove_handler('Signed Presence')
self.xmpp.del_filter('out', self._sign_presence)
self.xmpp.del_event_handler('unverified_signed_presence',
self._handle_unverified_signed_presence)
def _sign_presence(self, stanza):
if isinstance(stanza, Presence):
if stanza['type'] == 'available' or \

View File

@@ -88,6 +88,10 @@ class XEP_0030(BasePlugin):
description = 'XEP-0030: Service Discovery'
dependencies = set()
stanza = stanza
default_config = {
'use_cache': True,
'wrap_results': False
}
def plugin_init(self):
"""
@@ -108,9 +112,6 @@ class XEP_0030(BasePlugin):
self.static = StaticDisco(self.xmpp, self)
self.use_cache = self.config.get('use_cache', True)
self.wrap_results = self.config.get('wrap_results', False)
self._disco_ops = [
'get_info', 'set_info', 'set_identities', 'set_features',
'get_items', 'set_items', 'del_items', 'add_identity',
@@ -287,7 +288,7 @@ class XEP_0030(BasePlugin):
'cached': cached}
return self.api['has_identity'](jid, node, ifrom, data)
def get_info(self, jid=None, node=None, local=False,
def get_info(self, jid=None, node=None, local=None,
cached=None, **kwargs):
"""
Retrieve the disco#info results from a given JID/node combination.
@@ -323,18 +324,21 @@ class XEP_0030(BasePlugin):
callback -- Optional callback to execute when a reply is
received instead of blocking and waiting for
the reply.
timeout_callback -- Optional callback to execute when no result
has been received in timeout seconds.
"""
if jid is not None and not isinstance(jid, JID):
jid = JID(jid)
if self.xmpp.is_component:
if jid.domain == self.xmpp.boundjid.domain:
local = True
else:
if str(jid) == str(self.xmpp.boundjid):
local = True
jid = jid.full
elif jid in (None, ''):
local = True
if local is None:
if jid is not None and not isinstance(jid, JID):
jid = JID(jid)
if self.xmpp.is_component:
if jid.domain == self.xmpp.boundjid.domain:
local = True
else:
if str(jid) == str(self.xmpp.boundjid):
local = True
jid = jid.full
elif jid in (None, ''):
local = True
if local:
log.debug("Looking up local disco#info data " + \
@@ -362,7 +366,8 @@ class XEP_0030(BasePlugin):
iq['disco_info']['node'] = node if node else ''
return iq.send(timeout=kwargs.get('timeout', None),
block=kwargs.get('block', True),
callback=kwargs.get('callback', None))
callback=kwargs.get('callback', None),
timeout_callback=kwargs.get('timeout_callback', None))
def set_info(self, jid=None, node=None, info=None):
"""
@@ -403,8 +408,10 @@ class XEP_0030(BasePlugin):
iterator -- If True, return a result set iterator using
the XEP-0059 plugin, if the plugin is loaded.
Otherwise the parameter is ignored.
timeout_callback -- Optional callback to execute when no result
has been received in timeout seconds.
"""
if local or jid is None:
if local or local is None and jid is None:
items = self.api['get_items'](jid, node,
kwargs.get('ifrom', None),
kwargs)
@@ -421,7 +428,8 @@ class XEP_0030(BasePlugin):
else:
return iq.send(timeout=kwargs.get('timeout', None),
block=kwargs.get('block', True),
callback=kwargs.get('callback', None))
callback=kwargs.get('callback', None),
timeout_callback=kwargs.get('timeout_callback', None))
def set_items(self, jid=None, node=None, **kwargs):
"""
@@ -622,11 +630,7 @@ class XEP_0030(BasePlugin):
if iq['type'] == 'get':
log.debug("Received disco info query from " + \
"<%s> to <%s>.", iq['from'], iq['to'])
if self.xmpp.is_component:
jid = iq['to'].full
else:
jid = iq['to'].bare
info = self.api['get_info'](jid,
info = self.api['get_info'](iq['to'],
iq['disco_info']['node'],
iq['from'],
iq)
@@ -649,7 +653,7 @@ class XEP_0030(BasePlugin):
ito = iq['to'].full
else:
ito = None
self.api['cache_info'](iq['from'].full,
self.api['cache_info'](iq['from'],
iq['disco_info']['node'],
ito,
iq)
@@ -667,13 +671,9 @@ class XEP_0030(BasePlugin):
if iq['type'] == 'get':
log.debug("Received disco items query from " + \
"<%s> to <%s>.", iq['from'], iq['to'])
if self.xmpp.is_component:
jid = iq['to'].full
else:
jid = iq['to'].bare
items = self.api['get_items'](jid,
items = self.api['get_items'](iq['to'],
iq['disco_items']['node'],
iq['from'].full,
iq['from'],
iq)
if isinstance(items, Iq):
items.send()

View File

@@ -237,7 +237,7 @@ class StaticDisco(object):
with self.lock:
if not self.node_exists(jid, node):
if not node:
return DiscoInfo()
return DiscoItems()
else:
raise XMPPError(condition='item-not-found')
else:
@@ -424,9 +424,6 @@ class StaticDisco(object):
The data parameter is not used.
"""
with self.lock:
if isinstance(jid, JID):
jid = jid.full
if not self.node_exists(jid, node, ifrom):
return None
else:

View File

@@ -26,7 +26,12 @@ class XEP_0033(BasePlugin):
stanza = stanza
def plugin_init(self):
self.xmpp['xep_0030'].add_feature(Addresses.namespace)
register_stanza_plugin(Message, Addresses)
register_stanza_plugin(Presence, Addresses)
def plugin_end(self):
self.xmpp['xep_0030'].del_feature(feature=Addresses.namespace)
def session_bind(self, jid):
self.xmpp['xep_0030'].add_feature(Addresses.namespace)

View File

@@ -137,7 +137,7 @@ class XEP_0045(BasePlugin):
def handle_groupchat_invite(self, inv):
""" Handle an invite into a muc.
"""
logging.debug("MUC invite to %s from %s: %s", inv['from'], inv["from"], inv)
logging.debug("MUC invite to %s from %s: %s", inv['to'], inv["from"], inv)
if inv['from'] not in self.rooms.keys():
self.xmpp.event("groupchat_invite", inv)
@@ -156,6 +156,7 @@ class XEP_0045(BasePlugin):
entry = pr['muc'].getStanzaValues()
entry['show'] = pr['show']
entry['status'] = pr['status']
entry['alt_nick'] = pr['nick']
if pr['type'] == 'unavailable':
if entry['nick'] in self.rooms[entry['room']]:
del self.rooms[entry['room']][entry['nick']]
@@ -244,11 +245,11 @@ class XEP_0045(BasePlugin):
stanza = self.xmpp.makePresence(pto="%s/%s" % (room, nick), pstatus=pstatus, pshow=pshow, pfrom=pfrom)
x = ET.Element('{http://jabber.org/protocol/muc}x')
if password:
passelement = ET.Element('password')
passelement = ET.Element('{http://jabber.org/protocol/muc}password')
passelement.text = password
x.append(passelement)
if maxhistory:
history = ET.Element('history')
history = ET.Element('{http://jabber.org/protocol/muc}history')
if maxhistory == "0":
history.attrib['maxchars'] = maxhistory
else:
@@ -270,10 +271,10 @@ class XEP_0045(BasePlugin):
iq['from'] = ifrom
iq['to'] = room
query = ET.Element('{http://jabber.org/protocol/muc#owner}query')
destroy = ET.Element('destroy')
destroy = ET.Element('{http://jabber.org/protocol/muc#owner}destroy')
if altroom:
destroy.attrib['jid'] = altroom
xreason = ET.Element('reason')
xreason = ET.Element('{http://jabber.org/protocol/muc#owner}reason')
xreason.text = reason
destroy.append(xreason)
query.append(destroy)
@@ -293,9 +294,9 @@ class XEP_0045(BasePlugin):
raise TypeError
query = ET.Element('{http://jabber.org/protocol/muc#admin}query')
if nick is not None:
item = ET.Element('item', {'affiliation':affiliation, 'nick':nick})
item = ET.Element('{http://jabber.org/protocol/muc#admin}item', {'affiliation':affiliation, 'nick':nick})
else:
item = ET.Element('item', {'affiliation':affiliation, 'jid':jid})
item = ET.Element('{http://jabber.org/protocol/muc#admin}item', {'affiliation':affiliation, 'jid':jid})
query.append(item)
iq = self.xmpp.makeIqSet(query)
iq['to'] = room
@@ -316,7 +317,7 @@ class XEP_0045(BasePlugin):
x = ET.Element('{http://jabber.org/protocol/muc#user}x')
invite = ET.Element('{http://jabber.org/protocol/muc#user}invite', {'to': jid})
if reason:
rxml = ET.Element('reason')
rxml = ET.Element('{http://jabber.org/protocol/muc#user}reason')
rxml.text = reason
invite.append(rxml)
x.append(invite)

View File

@@ -20,18 +20,19 @@ class XEP_0047(BasePlugin):
description = 'XEP-0047: In-band Bytestreams'
dependencies = set(['xep_0030'])
stanza = stanza
default_config = {
'max_block_size': 8192,
'window_size': 1,
'auto_accept': True,
'accept_stream': None
}
def plugin_init(self):
self.streams = {}
self.pending_streams = {3: 5}
self.pending_streams = {}
self.pending_close_streams = {}
self._stream_lock = threading.Lock()
self.max_block_size = self.config.get('max_block_size', 8192)
self.window_size = self.config.get('window_size', 1)
self.auto_accept = self.config.get('auto_accept', True)
self.accept_stream = self.config.get('accept_stream', None)
register_stanza_plugin(Iq, Open)
register_stanza_plugin(Iq, Close)
register_stanza_plugin(Iq, Data)
@@ -51,6 +52,13 @@ class XEP_0047(BasePlugin):
StanzaPath('iq@type=set/ibb_data'),
self._handle_data))
def plugin_end(self):
self.xmpp.remove_handler('IBB Open')
self.xmpp.remove_handler('IBB Close')
self.xmpp.remove_handler('IBB Data')
self.xmpp['xep_0030'].del_feature(feature='http://jabber.org/protocol/ibb')
def session_bind(self, jid):
self.xmpp['xep_0030'].add_feature('http://jabber.org/protocol/ibb')
def _accept_stream(self, iq):

View File

@@ -1,9 +1,9 @@
import re
import base64
from sleekxmpp.util import bytes
from sleekxmpp.exceptions import XMPPError
from sleekxmpp.xmlstream import ElementBase
from sleekxmpp.thirdparty.suelta.util import bytes
VALID_B64 = re.compile(r'[A-Za-z0-9\+\/]*=*')
@@ -14,7 +14,7 @@ def to_b64(data):
def from_b64(data):
return bytes(base64.b64decode(bytes(data))).decode('utf-8')
return bytes(base64.b64decode(bytes(data)))
class Open(ElementBase):

View File

@@ -1,11 +1,8 @@
import socket
import threading
import logging
try:
import queue
except ImportError:
import Queue as queue
from sleekxmpp.util import Queue
from sleekxmpp.exceptions import XMPPError
@@ -33,7 +30,7 @@ class IBBytestream(object):
self.stream_in_closed = threading.Event()
self.stream_out_closed = threading.Event()
self.recv_queue = queue.Queue()
self.recv_queue = Queue()
self.send_window = threading.BoundedSemaphore(value=self.window_size)
self.window_ids = set()

View File

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

View File

@@ -0,0 +1,53 @@
"""
SleekXMPP: The Sleek XMPP Library
Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
This file is part of SleekXMPP.
See the file LICENSE for copying permission.
"""
import logging
from sleekxmpp import Iq
from sleekxmpp.plugins import BasePlugin
from sleekxmpp.xmlstream.handler import Callback
from sleekxmpp.xmlstream.matcher import StanzaPath
from sleekxmpp.xmlstream import register_stanza_plugin
from sleekxmpp.plugins.xep_0049 import stanza, PrivateXML
log = logging.getLogger(__name__)
class XEP_0049(BasePlugin):
name = 'xep_0049'
description = 'XEP-0049: Private XML Storage'
dependencies = set([])
stanza = stanza
def plugin_init(self):
register_stanza_plugin(Iq, PrivateXML)
def register(self, stanza):
register_stanza_plugin(PrivateXML, stanza, iterable=True)
def store(self, data, ifrom=None, block=True, timeout=None, callback=None):
iq = self.xmpp.Iq()
iq['type'] = 'set'
iq['from'] = ifrom
if not isinstance(data, list):
data = [data]
for elem in data:
iq['private'].append(elem)
return iq.send(block=block, timeout=timeout, callback=callback)
def retrieve(self, name, ifrom=None, block=True, timeout=None, callback=None):
iq = self.xmpp.Iq()
iq['type'] = 'get'
iq['from'] = ifrom
iq['private'].enable(name)
return iq.send(block=block, timeout=timeout, callback=callback)

View File

@@ -0,0 +1,17 @@
"""
SleekXMPP: The Sleek XMPP Library
Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
This file is part of SleekXMPP.
See the file LICENSE for copying permission.
"""
from sleekxmpp.xmlstream import ET, ElementBase
class PrivateXML(ElementBase):
name = 'query'
namespace = 'jabber:iq:private'
plugin_attrib = 'private'
interfaces = set()

View File

@@ -82,12 +82,18 @@ class XEP_0050(BasePlugin):
description = 'XEP-0050: Ad-Hoc Commands'
dependencies = set(['xep_0030', 'xep_0004'])
stanza = stanza
default_config = {
'threaded': True,
'session_db': None
}
def plugin_init(self):
"""Start the XEP-0050 plugin."""
self.threaded = self.config.get('threaded', True)
self.sessions = self.session_db
if self.sessions is None:
self.sessions = {}
self.commands = {}
self.sessions = self.config.get('session_db', {})
self.xmpp.register_handler(
Callback("Ad-Hoc Execute",
@@ -110,6 +116,20 @@ class XEP_0050(BasePlugin):
self._handle_command_complete,
threaded=self.threaded)
def plugin_end(self):
self.xmpp.del_event_handler('command_execute',
self._handle_command_start)
self.xmpp.del_event_handler('command_next',
self._handle_command_next)
self.xmpp.del_event_handler('command_cancel',
self._handle_command_cancel)
self.xmpp.del_event_handler('command_complete',
self._handle_command_complete)
self.xmpp.remove_handler('Ad-Hoc Execute')
self.xmpp['xep_0030'].del_feature(feature=Command.namespace)
self.xmpp['xep_0030'].set_items(node=Command.namespace, items=tuple())
def session_bind(self, jid):
self.xmpp['xep_0030'].add_feature(Command.namespace)
self.xmpp['xep_0030'].set_items(node=Command.namespace, items=tuple())
@@ -167,12 +187,6 @@ class XEP_0050(BasePlugin):
jid = JID(jid)
item_jid = jid.full
# Client disco uses only the bare JID
if self.xmpp.is_component:
jid = jid.full
else:
jid = jid.bare
self.xmpp['xep_0030'].add_identity(category='automation',
itype='command-list',
name='Ad-Hoc commands',

View File

@@ -1,8 +1,7 @@
import base64
import datetime as dt
from sleekxmpp.thirdparty.suelta.util import bytes
from sleekxmpp.util import bytes
from sleekxmpp.xmlstream import ElementBase, ET, register_stanza_plugin, JID
from sleekxmpp.plugins import xep_0082
@@ -542,6 +541,7 @@ register_stanza_plugin(VCardTemp, Logo, iterable=True)
register_stanza_plugin(VCardTemp, Mailer, iterable=True)
register_stanza_plugin(VCardTemp, Note, iterable=True)
register_stanza_plugin(VCardTemp, Nickname, iterable=True)
register_stanza_plugin(VCardTemp, Org, iterable=True)
register_stanza_plugin(VCardTemp, Photo, iterable=True)
register_stanza_plugin(VCardTemp, ProdID, iterable=True)
register_stanza_plugin(VCardTemp, Rev, iterable=True)

View File

@@ -37,7 +37,6 @@ class XEP_0054(BasePlugin):
"""
register_stanza_plugin(Iq, VCardTemp)
self.xmpp['xep_0030'].add_feature('vcard-temp')
self.api.register(self._set_vcard, 'set_vcard', default=True)
self.api.register(self._get_vcard, 'get_vcard', default=True)
@@ -50,6 +49,13 @@ class XEP_0054(BasePlugin):
StanzaPath('iq/vcard_temp'),
self._handle_get_vcard))
def plugin_end(self):
self.xmpp.remove_handler('VCardTemp')
self.xmpp['xep_0030'].del_feature(feature='vcard-temp')
def session_bind(self, jid):
self.xmpp['xep_0030'].add_feature('vcard-temp')
def make_vcard(self):
return VCardTemp()
@@ -91,8 +97,8 @@ class XEP_0054(BasePlugin):
def publish_vcard(self, vcard=None, jid=None, block=True, ifrom=None,
callback=None, timeout=None):
self.api['set_vcard'](jid, None, ifrom, vcard)
if self.xmpp.is_component:
self.api['set_vcard'](jid, None, ifrom, vcard)
return
iq = self.xmpp.Iq()

View File

@@ -25,11 +25,14 @@ class ResultIterator():
An iterator for Result Set Managment
"""
def __init__(self, query, interface, amount=10, start=None, reverse=False):
def __init__(self, query, interface, results='substanzas', amount=10,
start=None, reverse=False):
"""
Arguments:
query -- The template query
interface -- The substanza of the query, for example disco_items
results -- The query stanza's interface which provides a
countable list of query results.
amount -- The max amounts of items to request per iteration
start -- From which item id to start
reverse -- If True, page backwards through the results
@@ -46,7 +49,9 @@ class ResultIterator():
self.amount = amount
self.start = start
self.interface = interface
self.results = results
self.reverse = reverse
self._stop = False
def __iter__(self):
return self
@@ -62,6 +67,8 @@ class ResultIterator():
results will be the items before the current page
of items.
"""
if self._stop:
raise StopIteration
self.query[self.interface]['rsm']['before'] = self.reverse
self.query['id'] = self.query.stream.new_id()
self.query[self.interface]['rsm']['max'] = str(self.amount)
@@ -82,9 +89,9 @@ class ResultIterator():
r[self.interface]['rsm']['first_index']:
count = int(r[self.interface]['rsm']['count'])
first = int(r[self.interface]['rsm']['first_index'])
num_items = len(r[self.interface]['substanzas'])
num_items = len(r[self.interface][self.results])
if first + num_items == count:
raise StopIteration
self._stop = True
if self.reverse:
self.start = r[self.interface]['rsm']['first']
@@ -111,11 +118,16 @@ class XEP_0059(BasePlugin):
"""
Start the XEP-0059 plugin.
"""
self.xmpp['xep_0030'].add_feature(Set.namespace)
register_stanza_plugin(self.xmpp['xep_0030'].stanza.DiscoItems,
self.stanza.Set)
def iterate(self, stanza, interface):
def plugin_end(self):
self.xmpp['xep_0030'].del_feature(feature=Set.namespace)
def session_bind(self, jid):
self.xmpp['xep_0030'].add_feature(Set.namespace)
def iterate(self, stanza, interface, results='substanzas'):
"""
Create a new result set iterator for a given stanza query.
@@ -127,5 +139,7 @@ class XEP_0059(BasePlugin):
result set management stanza should be
appended. For example, for disco#items queries
the interface 'disco_items' should be used.
results -- The name of the interface containing the
query results (typically just 'substanzas').
"""
return ResultIterator(stanza, interface)
return ResultIterator(stanza, interface, results)

View File

@@ -26,7 +26,7 @@ class XEP_0060(BasePlugin):
name = 'xep_0060'
description = 'XEP-0060: Publish-Subscribe'
dependencies = set(['xep_0030', 'xep_0004'])
dependencies = set(['xep_0030', 'xep_0004', 'xep_0082', 'xep_0131'])
stanza = stanza
def plugin_init(self):
@@ -53,6 +53,15 @@ class XEP_0060(BasePlugin):
StanzaPath('message/pubsub_event/subscription'),
self._handle_event_subscription))
self.xmpp['xep_0131'].supported_headers.add('SubID')
def plugin_end(self):
self.xmpp.remove_handler('Pubsub Event: Items')
self.xmpp.remove_handler('Pubsub Event: Purge')
self.xmpp.remove_handler('Pubsub Event: Delete')
self.xmpp.remove_handler('Pubsub Event: Configuration')
self.xmpp.remove_handler('Pubsub Event: Subscription')
def _handle_event_items(self, msg):
"""Raise events for publish and retraction notifications."""
node = msg['pubsub_event']['items']['node']

View File

@@ -62,6 +62,12 @@ class XEP_0066(BasePlugin):
StanzaPath('iq@type=set/oob_transfer'),
self._handle_transfer))
def plugin_end(self):
self.xmpp.remove_handler('OOB Transfer')
self.xmpp['xep_0030'].del_feature(feature=stanza.OOBTransfer.namespace)
self.xmpp['xep_0030'].del_feature(feature=stanza.OOB.namespace)
def session_bind(self, jid):
self.xmpp['xep_0030'].add_feature(stanza.OOBTransfer.namespace)
self.xmpp['xep_0030'].add_feature(stanza.OOB.namespace)

View File

@@ -27,24 +27,28 @@ class XEP_0077(BasePlugin):
description = 'XEP-0077: In-Band Registration'
dependencies = set(['xep_0004', 'xep_0066'])
stanza = stanza
default_config = {
'create_account': True,
'order': 50
}
def plugin_init(self):
self.create_account = self.config.get('create_account', True)
register_stanza_plugin(StreamFeatures, RegisterFeature)
register_stanza_plugin(Iq, Register)
if self.xmpp.is_component:
pass
else:
if not self.xmpp.is_component:
self.xmpp.register_feature('register',
self._handle_register_feature,
restart=False,
order=self.config.get('order', 50))
order=self.order)
register_stanza_plugin(Register, self.xmpp['xep_0004'].stanza.Form)
register_stanza_plugin(Register, self.xmpp['xep_0066'].stanza.OOB)
def plugin_end(self):
if not self.xmpp.is_component:
self.xmpp.unregister_feature('register', self.order)
def _handle_register_feature(self, features):
if 'mechanisms' in self.xmpp.features:
# We have already logged in with an account

View File

@@ -11,6 +11,7 @@ import hashlib
import random
import sys
from sleekxmpp.jid import JID
from sleekxmpp.exceptions import IqError, IqTimeout
from sleekxmpp.stanza import Iq, StreamFeatures
from sleekxmpp.xmlstream import ElementBase, ET, register_stanza_plugin
@@ -34,20 +35,37 @@ class XEP_0078(BasePlugin):
description = 'XEP-0078: Non-SASL Authentication'
dependencies = set()
stanza = stanza
default_config = {
'order': 15
}
def plugin_init(self):
self.xmpp.register_feature('auth',
self._handle_auth,
restart=False,
order=self.config.get('order', 15))
order=self.order)
self.xmpp.add_event_handler('legacy_protocol',
self._handle_legacy_protocol)
register_stanza_plugin(Iq, stanza.IqAuth)
register_stanza_plugin(StreamFeatures, stanza.AuthFeature)
def plugin_end(self):
self.xmpp.del_event_handler('legacy_protocol',
self._handle_legacy_protocol)
self.xmpp.unregister_feature('auth', self.order)
def _handle_auth(self, features):
# If we can or have already authenticated with SASL, do nothing.
if 'mechanisms' in features['features']:
return False
return self.authenticate()
def _handle_legacy_protocol(self, event):
self.authenticate()
def authenticate(self):
if self.xmpp.authenticated:
return False
@@ -56,13 +74,13 @@ class XEP_0078(BasePlugin):
# Step 1: Request the auth form
iq = self.xmpp.Iq()
iq['type'] = 'get'
iq['to'] = self.xmpp.boundjid.host
iq['auth']['username'] = self.xmpp.boundjid.user
iq['to'] = self.xmpp.requested_jid.host
iq['auth']['username'] = self.xmpp.requested_jid.user
try:
resp = iq.send(now=True)
except IqError:
log.info("Authentication failed: %s", resp['error']['condition'])
except IqError as err:
log.info("Authentication failed: %s", err.iq['error']['condition'])
self.xmpp.event('failed_auth', direct=True)
self.xmpp.disconnect()
return True
@@ -75,13 +93,14 @@ class XEP_0078(BasePlugin):
# Step 2: Fill out auth form for either password or digest auth
iq = self.xmpp.Iq()
iq['type'] = 'set'
iq['auth']['username'] = self.xmpp.boundjid.user
iq['auth']['username'] = self.xmpp.requested_jid.user
# A resource is required, so create a random one if necessary
if self.xmpp.boundjid.resource:
iq['auth']['resource'] = self.xmpp.boundjid.resource
else:
iq['auth']['resource'] = '%s' % random.random()
resource = self.xmpp.requested_jid.resource
if not resource:
resource = uuid.uuid4()
iq['auth']['resource'] = resource
if 'digest' in resp['auth']['fields']:
log.debug('Authenticating via jabber:iq:auth Digest')
@@ -103,16 +122,22 @@ class XEP_0078(BasePlugin):
result = iq.send(now=True)
except IqError as err:
log.info("Authentication failed")
self.xmpp.disconnect()
self.xmpp.event("failed_auth", direct=True)
self.xmpp.disconnect()
except IqTimeout:
log.info("Authentication failed")
self.xmpp.disconnect()
self.xmpp.event("failed_auth", direct=True)
self.xmpp.disconnect()
self.xmpp.features.add('auth')
self.xmpp.authenticated = True
self.xmpp.boundjid = JID(self.xmpp.requested_jid,
resource=resource,
cache_lock=True)
self.xmpp.event('session_bind', self.xmpp.boundjid, direct=True)
log.debug("Established Session")
self.xmpp.sessionstarted = True
self.xmpp.session_started_event.set()

View File

@@ -28,8 +28,11 @@ class XEP_0080(BasePlugin):
dependencies = set(['xep_0163'])
stanza = stanza
def plugin_init(self):
"""Start the XEP-0080 plugin."""
def plugin_end(self):
self.xmpp['xep_0163'].remove_interest(Geoloc.namespace)
self.xmpp['xep_0030'].del_feature(feature=Geoloc.namespace)
def session_bind(self, jid):
self.xmpp['xep_0163'].register_pep('user_location', Geoloc)
def publish_location(self, **kwargs):

View File

@@ -28,14 +28,22 @@ class XEP_0084(BasePlugin):
stanza = stanza
def plugin_init(self):
self.xmpp['xep_0163'].register_pep('avatar_metadata', MetaData)
pubsub_stanza = self.xmpp['xep_0060'].stanza
register_stanza_plugin(pubsub_stanza.Item, Data)
register_stanza_plugin(pubsub_stanza.EventItem, Data)
self.xmpp['xep_0060'].map_node_event(Data.namespace, 'avatar_data')
def plugin_end(self):
self.xmpp['xep_0030'].del_feature(feature=MetaData.namespace)
self.xmpp['xep_0163'].remove_interest(MetaData.namespace)
def session_bind(self, jid):
self.xmpp['xep_0163'].register_pep('avatar_metadata', MetaData)
def generate_id(self, data):
return hashlib.sha1(data).hexdigest()
def retrieve_avatar(self, jid, id, url=None, ifrom=None, block=True,
callback=None, timeout=None):
return self.xmpp['xep_0060'].get_item(jid, Data.namespace, id,
@@ -49,8 +57,7 @@ class XEP_0084(BasePlugin):
payload = Data()
payload['value'] = data
return self.xmpp['xep_0163'].publish(payload,
node=Data.namespace,
id=hashlib.sha1(data).hexdigest(),
id=self.generate_id(data),
ifrom=ifrom,
block=block,
callback=callback,
@@ -62,17 +69,19 @@ class XEP_0084(BasePlugin):
metadata = MetaData()
if items is None:
items = []
if not isinstance(items, (list, set)):
items = [items]
for info in items:
metadata.add_info(info['id'], info['type'], info['bytes'],
height=info.get('height', ''),
width=info.get('width', ''),
url=info.get('url', ''))
for pointer in pointers:
metadata.add_pointer(pointer)
return self.xmpp['xep_0163'].publish(payload,
node=Data.namespace,
id=hashlib.sha1(data).hexdigest(),
if pointers is not None:
for pointer in pointers:
metadata.add_pointer(pointer)
return self.xmpp['xep_0163'].publish(metadata,
ifrom=ifrom,
block=block,
callback=callback,

View File

@@ -7,8 +7,8 @@
"""
from base64 import b64encode, b64decode
from sleekxmpp.thirdparty.suelta.util import bytes
from sleekxmpp.util import bytes
from sleekxmpp.xmlstream import ET, ElementBase, register_stanza_plugin
@@ -43,7 +43,7 @@ class MetaData(ElementBase):
info = Info()
info.values = {'id': id,
'type': itype,
'bytes': ibytes,
'bytes': '%s' % ibytes,
'height': height,
'width': width,
'url': url}

View File

@@ -43,9 +43,14 @@ class XEP_0085(BasePlugin):
register_stanza_plugin(Message, stanza.Inactive)
register_stanza_plugin(Message, stanza.Paused)
def plugin_end(self):
self.xmpp.remove_handler('Chat State')
def session_bind(self, jid):
self.xmpp.plugin['xep_0030'].add_feature(ChatState.namespace)
def _handle_chat_state(self, msg):
state = msg['chat_state']
log.debug("Chat State: %s, %s", state, msg['from'].jid)
self.xmpp.event('chatstate', msg)
self.xmpp.event('chatstate_%s' % state, msg)

View File

@@ -37,7 +37,10 @@ class XEP_0086(BasePlugin):
description = 'XEP-0086: Error Condition Mappings'
dependencies = set()
stanza = stanza
default_config = {
'override': True
}
def plugin_init(self):
register_stanza_plugin(Error, LegacyError,
overrides=self.config.get('override', True))
overrides=self.override)

View File

@@ -0,0 +1,16 @@
"""
SleekXMPP: The Sleek XMPP Library
Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
This file is part of SleekXMPP.
See the file LICENSE for copying permission.
"""
from sleekxmpp.plugins.base import register_plugin
from sleekxmpp.plugins.xep_0091 import stanza
from sleekxmpp.plugins.xep_0091.stanza import LegacyDelay
from sleekxmpp.plugins.xep_0091.legacy_delay import XEP_0091
register_plugin(XEP_0091)

View File

@@ -0,0 +1,29 @@
"""
SleekXMPP: The Sleek XMPP Library
Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
This file is part of SleekXMPP.
See the file LICENSE for copying permission.
"""
from sleekxmpp.stanza import Message, Presence
from sleekxmpp.xmlstream import register_stanza_plugin
from sleekxmpp.plugins import BasePlugin
from sleekxmpp.plugins.xep_0091 import stanza
class XEP_0091(BasePlugin):
"""
XEP-0091: Legacy Delayed Delivery
"""
name = 'xep_0091'
description = 'XEP-0091: Legacy Delayed Delivery'
dependencies = set()
stanza = stanza
def plugin_init(self):
register_stanza_plugin(Message, stanza.LegacyDelay)
register_stanza_plugin(Presence, stanza.LegacyDelay)

View File

@@ -0,0 +1,46 @@
"""
SleekXMPP: The Sleek XMPP Library
Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
This file is part of SleekXMPP.
See the file LICENSE for copying permission.
"""
import datetime as dt
from sleekxmpp.jid import JID
from sleekxmpp.xmlstream import ElementBase
from sleekxmpp.plugins import xep_0082
class LegacyDelay(ElementBase):
name = 'x'
namespace = 'jabber:x:delay'
plugin_attrib = 'legacy_delay'
interfaces = set(('from', 'stamp', 'text'))
def get_from(self):
return JID(self._get_attr('from'))
def set_from(self, value):
self._set_attr('from', str(value))
def get_stamp(self):
timestamp = self._get_attr('stamp')
return xep_0082.parse('%sZ' % timestamp)
def set_stamp(self, value):
if isinstance(value, dt.datetime):
value = value.astimezone(xep_0082.tzutc)
value = xep_0082.format_datetime(value)
self._set_attr('stamp', value[0:19].replace('-', ''))
def get_text(self):
return self.xml.text
def set_text(self, value):
self.xml.text = value
def del_text(self):
self.xml.text = ''

View File

@@ -30,16 +30,18 @@ class XEP_0092(BasePlugin):
description = 'XEP-0092: Software Version'
dependencies = set(['xep_0030'])
stanza = stanza
default_config = {
'software_name': 'SleekXMPP',
'version': sleekxmpp.__version__,
'os': ''
}
def plugin_init(self):
"""
Start the XEP-0092 plugin.
"""
self.name = self.config.get('name', 'SleekXMPP')
self.version = self.config.get('version', sleekxmpp.__version__)
self.os = self.config.get('os', '')
self.getVersion = self.get_version
if 'name' in self.config:
self.software_name = self.config['name']
self.xmpp.register_handler(
Callback('Software Version',
@@ -48,6 +50,11 @@ class XEP_0092(BasePlugin):
register_stanza_plugin(Iq, Version)
def plugin_end(self):
self.xmpp.remove_handler('Software Version')
self.xmpp['xep_0030'].del_feature(feature='jabber:iq:version')
def session_bind(self, jid):
self.xmpp.plugin['xep_0030'].add_feature('jabber:iq:version')
def _handle_version(self, iq):
@@ -58,7 +65,7 @@ class XEP_0092(BasePlugin):
iq -- The Iq stanza containing the software version query.
"""
iq.reply()
iq['software_version']['name'] = self.name
iq['software_version']['name'] = self.software_name
iq['software_version']['version'] = self.version
iq['software_version']['os'] = self.os
iq.send()
@@ -83,3 +90,6 @@ class XEP_0092(BasePlugin):
del values['lang']
return values
return False
XEP_0092.getVersion = XEP_0092.get_version

View File

@@ -0,0 +1,26 @@
"""
SleekXMPP: The Sleek XMPP Library
Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
This file is part of SleekXMPP.
See the file LICENSE for copying permission.
"""
from sleekxmpp.plugins import BasePlugin, register_plugin
class XEP_0106(BasePlugin):
name = 'xep_0106'
description = 'XEP-0106: JID Escaping'
dependencies = set(['xep_0030'])
def session_bind(self, jid):
self.xmpp['xep_0030'].add_feature(feature='jid\\20escaping')
def plugin_end(self):
self.xmpp['xep_0030'].del_feature(feature='jid\\20escaping')
register_plugin(XEP_0106)

View File

@@ -32,6 +32,12 @@ class XEP_0107(BasePlugin):
def plugin_init(self):
register_stanza_plugin(Message, UserMood)
def plugin_end(self):
self.xmpp['xep_0030'].del_feature(feature=UserMood.namespace)
self.xmpp['xep_0163'].remove_interest(UserMood.namespace)
def session_bind(self, jid):
self.xmpp['xep_0163'].register_pep('user_mood', UserMood)
def publish_mood(self, value=None, text=None, options=None,

View File

@@ -26,7 +26,11 @@ class XEP_0108(BasePlugin):
dependencies = set(['xep_0163'])
stanza = stanza
def plugin_init(self):
def plugin_end(self):
self.xmpp['xep_0030'].del_feature(feature=UserActivity.namespace)
self.xmpp['xep_0163'].remove_interest(UserActivity.namespace)
def session_bind(self, jid):
self.xmpp['xep_0163'].register_pep('user_activity', UserActivity)
def publish_activity(self, general, specific=None, text=None, options=None,

View File

@@ -33,16 +33,17 @@ class XEP_0115(BasePlugin):
description = 'XEP-0115: Entity Capabilities'
dependencies = set(['xep_0030', 'xep_0128', 'xep_0004'])
stanza = stanza
default_config = {
'hash': 'sha-1',
'caps_node': None,
'broadcast': True
}
def plugin_init(self):
self.hashes = {'sha-1': hashlib.sha1,
'sha1': hashlib.sha1,
'md5': hashlib.md5}
self.hash = self.config.get('hash', 'sha-1')
self.caps_node = self.config.get('caps_node', None)
self.broadcast = self.config.get('broadcast', True)
if self.caps_node is None:
ver = sleekxmpp.__version__
self.caps_node = 'http://sleekxmpp.com/ver/%s' % ver
@@ -73,16 +74,15 @@ class XEP_0115(BasePlugin):
restart=False,
order=10010)
self.xmpp['xep_0030'].add_feature(stanza.Capabilities.namespace)
disco = self.xmpp['xep_0030']
self.static = StaticCaps(self.xmpp, disco.static)
self.api.settings['client_bare'] = False
self.api.settings['component_bare'] = False
for op in self._disco_ops:
self.api.register(getattr(self.static, op), op, default=True)
for op in ('supports', 'has_identity'):
self.xmpp['xep_0030'].api.register(getattr(self.static, op), op)
self._run_node_handler = disco._run_node_handler
disco.cache_caps = self.cache_caps
@@ -90,13 +90,31 @@ class XEP_0115(BasePlugin):
disco.assign_verstring = self.assign_verstring
disco.get_verstring = self.get_verstring
def plugin_end(self):
self.xmpp['xep_0030'].del_feature(feature=stanza.Capabilities.namespace)
self.xmpp.del_filter('out', self._filter_add_caps)
self.xmpp.del_event_handler('entity_caps', self._process_caps)
self.xmpp.remove_handler('Entity Capabilities')
if not self.xmpp.is_component:
self.xmpp.unregister_feature('caps', 10010)
for op in ('supports', 'has_identity'):
self.xmpp['xep_0030'].restore_defaults(op)
def session_bind(self, jid):
self.xmpp['xep_0030'].add_feature(stanza.Capabilities.namespace)
def _filter_add_caps(self, stanza):
if isinstance(stanza, Presence) and self.broadcast:
ver = self.get_verstring(stanza['from'])
if ver:
stanza['caps']['node'] = self.caps_node
stanza['caps']['hash'] = self.hash
stanza['caps']['ver'] = ver
if not isinstance(stanza, Presence) or not self.broadcast:
return stanza
if stanza['type'] not in ('available', 'chat', 'away', 'dnd', 'xa'):
return stanza
ver = self.get_verstring(stanza['from'])
if ver:
stanza['caps']['node'] = self.caps_node
stanza['caps']['hash'] = self.hash
stanza['caps']['ver'] = ver
return stanza
def _handle_caps(self, presence):
@@ -125,6 +143,11 @@ class XEP_0115(BasePlugin):
if str(existing_verstring) == str(pres['caps']['ver']):
return
existing_caps = self.get_caps(verstring=pres['caps']['ver'])
if existing_caps is not None:
self.assign_verstring(pres['from'], pres['caps']['ver'])
return
if pres['caps']['hash'] not in self.hashes:
try:
log.debug("Unknown caps hash: %s", pres['caps']['hash'])

View File

@@ -26,7 +26,11 @@ class XEP_0118(BasePlugin):
dependencies = set(['xep_0163'])
stanza = stanza
def plugin_init(self):
def plugin_end(self):
self.xmpp['xep_0030'].del_feature(feature=UserTune.namespace)
self.xmpp['xep_0163'].remove_interest(UserTune.namespace)
def session_bind(self, jid):
self.xmpp['xep_0163'].register_pep('user_tune', UserTune)
def publish_tune(self, artist=None, length=None, rating=None, source=None,

View File

@@ -51,8 +51,6 @@ class XEP_0128(BasePlugin):
register_stanza_plugin(DiscoInfo, Form, iterable=True)
def post_init(self):
"""Handle cross-plugin dependencies."""
self.disco = self.xmpp['xep_0030']
self.static = StaticExtendedDisco(self.disco.static)

View File

@@ -0,0 +1,16 @@
"""
SleekXMPP: The Sleek XMPP Library
Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
This file is part of SleekXMPP.
See the file LICENSE for copying permission.
"""
from sleekxmpp.plugins.base import register_plugin
from sleekxmpp.plugins.xep_0131 import stanza
from sleekxmpp.plugins.xep_0131.stanza import Headers
from sleekxmpp.plugins.xep_0131.headers import XEP_0131
register_plugin(XEP_0131)

View File

@@ -0,0 +1,41 @@
"""
SleekXMPP: The Sleek XMPP Library
Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout
This file is part of SleekXMPP.
See the file LICENSE for copying permission.
"""
from sleekxmpp import Message, Presence
from sleekxmpp.xmlstream import register_stanza_plugin
from sleekxmpp.plugins import BasePlugin
from sleekxmpp.plugins.xep_0131 import stanza
from sleekxmpp.plugins.xep_0131.stanza import Headers
class XEP_0131(BasePlugin):
name = 'xep_0131'
description = 'XEP-0131: Stanza Headers and Internet Metadata'
dependencies = set(['xep_0030'])
stanza = stanza
default_config = {
'supported_headers': set()
}
def plugin_init(self):
register_stanza_plugin(Message, Headers)
register_stanza_plugin(Presence, Headers)
def plugin_end(self):
self.xmpp['xep_0030'].del_feature(feature=Headers.namespace)
for header in self.supported_headers:
self.xmpp['xep_0030'].del_feature(
feature='%s#%s' % (Headers.namespace, header))
def session_bind(self, jid):
self.xmpp['xep_0030'].add_feature(Headers.namespace)
for header in self.supported_headers:
self.xmpp['xep_0030'].add_feature('%s#%s' % (
Headers.namespace,
header))

View File

@@ -0,0 +1,51 @@
"""
SleekXMPP: The Sleek XMPP Library
Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
This file is part of SleekXMPP.
See the file LICENSE for copying permission.
"""
from sleekxmpp.thirdparty import OrderedDict
from sleekxmpp.xmlstream import ET, ElementBase
class Headers(ElementBase):
name = 'headers'
namespace = 'http://jabber.org/protocol/shim'
plugin_attrib = 'headers'
interfaces = set(['headers'])
is_extension = True
def get_headers(self):
result = OrderedDict()
headers = self.xml.findall('{%s}header' % self.namespace)
for header in headers:
name = header.attrib.get('name', '')
value = header.text
if name in result:
if not isinstance(result[name], set):
result[name] = [result[name]]
else:
result[name] = []
result[name].add(value)
else:
result[name] = value
return result
def set_headers(self, values):
self.del_headers()
for name in values:
vals = values[name]
if not isinstance(vals, (list, set)):
vals = [values[name]]
for value in vals:
header = ET.Element('{%s}header' % self.namespace)
header.attrib['name'] = name
header.text = value
self.xml.append(header)
def del_headers(self):
headers = self.xml.findall('{%s}header' % self.namespace)
for header in headers:
self.xml.remove(header)

View File

@@ -0,0 +1,54 @@
"""
SleekXMPP: The Sleek XMPP Library
Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
This file is part of SleekXMPP.
See the file LICENSE for copying permission.
"""
from sleekxmpp.plugins import BasePlugin, register_plugin
class XEP_0133(BasePlugin):
name = 'xep_0133'
description = 'XEP-0133: Service Administration'
dependencies = set(['xep_0030', 'xep_0004', 'xep_0050'])
commands = set(['add-user', 'delete-user', 'disable-user',
'reenable-user', 'end-user-session', 'get-user-password',
'change-user-password', 'get-user-roster',
'get-user-lastlogin', 'user-stats', 'edit-blacklist',
'edit-whitelist', 'get-registered-users-num',
'get-disabled-users-num', 'get-online-users-num',
'get-active-users-num', 'get-idle-users-num',
'get-registered-users-list', 'get-disabled-users-list',
'get-online-users-list', 'get-online-users',
'get-active-users', 'get-idle-userslist', 'announce',
'set-motd', 'edit-motd', 'delete-motd', 'set-welcome',
'delete-welcome', 'edit-admin', 'restart', 'shutdown'])
def get_commands(self, jid=None, **kwargs):
if jid is None:
jid = self.xmpp.boundjid.server
return self.xmpp['xep_0050'].get_commands(jid, **kwargs)
def create_command(name):
def admin_command(self, jid=None, session=None, ifrom=None, block=False):
if jid is None:
jid = self.xmpp.boundjid.server
self.xmpp['xep_0050'].start_command(
jid=jid,
node='http://jabber.org/protocol/admin#%s' % name,
session=session,
ifrom=ifrom,
block=block)
return admin_command
for cmd in XEP_0133.commands:
setattr(XEP_0133, cmd.replace('-', '_'), create_command(cmd))
register_plugin(XEP_0133)

View File

@@ -8,8 +8,11 @@
import hashlib
import logging
import threading
from sleekxmpp import JID
from sleekxmpp.stanza import Presence
from sleekxmpp.exceptions import XMPPError
from sleekxmpp.xmlstream import register_stanza_plugin
from sleekxmpp.xmlstream.matcher import StanzaPath
from sleekxmpp.xmlstream.handler import Callback
@@ -30,11 +33,14 @@ class XEP_0153(BasePlugin):
def plugin_init(self):
self._hashes = {}
self._allow_advertising = threading.Event()
register_stanza_plugin(Presence, VCardTempUpdate)
self.xmpp.add_filter('out', self._update_presence)
self.xmpp.add_event_handler('session_start', self._start)
self.xmpp.add_event_handler('session_end', self._end)
self.xmpp.add_event_handler('presence_available', self._recv_presence)
self.xmpp.add_event_handler('presence_dnd', self._recv_presence)
@@ -44,59 +50,85 @@ class XEP_0153(BasePlugin):
self.api.register(self._set_hash, 'set_hash', default=True)
self.api.register(self._get_hash, 'get_hash', default=True)
self.api.register(self._reset_hash, 'reset_hash', default=True)
def plugin_end(self):
self.xmpp.del_filter('out', self._update_presence)
self.xmpp.del_event_handler('session_start', self._start)
self.xmpp.del_event_handler('session_end', self._end)
self.xmpp.del_event_handler('presence_available', self._recv_presence)
self.xmpp.del_event_handler('presence_dnd', self._recv_presence)
self.xmpp.del_event_handler('presence_xa', self._recv_presence)
self.xmpp.del_event_handler('presence_chat', self._recv_presence)
self.xmpp.del_event_handler('presence_away', self._recv_presence)
def set_avatar(self, jid=None, avatar=None, mtype=None, block=True,
timeout=None, callback=None):
if jid is None:
jid = self.xmpp.boundjid.bare
vcard = self.xmpp['xep_0054'].get_vcard(jid, cached=True)
vcard = vcard['vcard_temp']
vcard['PHOTO']['TYPE'] = mtype
vcard['PHOTO']['BINVAL'] = avatar
self.xmpp['xep_0054'].publish_vcard(jid=jid, vcard=vcard)
self._reset_hash(jid)
self.api['reset_hash'](jid)
self.xmpp.roster[jid].send_last_presence()
def _start(self, event):
self.xmpp['xep_0054'].get_vcard()
vcard = self.xmpp['xep_0054'].get_vcard()
self._allow_advertising.set()
def _end(self, event):
self._allow_advertising.clear()
def _update_presence(self, stanza):
if not isinstance(stanza, Presence):
return stanza
if stanza['type'] not in ('available', 'dnd', 'chat', 'away', 'xa'):
return stanza
current_hash = self.api['get_hash'](stanza['from'])
stanza['vcard_temp_update']['photo'] = current_hash
return stanza
def _reset_hash(self, jid=None):
def _reset_hash(self, jid, node, ifrom, args):
own_jid = (jid.bare == self.xmpp.boundjid.bare)
if self.xmpp.is_component:
own_jid = (jid.domain == self.xmpp.boundjid.domain)
if jid is not None:
jid = jid.bare
self.api['set_hash'](jid, args=None)
if own_jid:
self.xmpp.roster[jid].send_last_presence()
iq = self.xmpp['xep_0054'].get_vcard(
jid=jid,
ifrom=self.xmpp.boundjid)
data = iq['vcard_temp']['PHOTO']['BINVAL']
if not data:
new_hash = ''
else:
new_hash = hashlib.sha1(data).hexdigest()
self.api['set_hash'](jid, args=new_hash)
if own_jid:
self.xmpp.roster[jid].send_last_presence()
try:
iq = self.xmpp['xep_0054'].get_vcard(jid=jid.bare, ifrom=ifrom)
data = iq['vcard_temp']['PHOTO']['BINVAL']
if not data:
new_hash = ''
else:
new_hash = hashlib.sha1(data).hexdigest()
self.api['set_hash'](jid, args=new_hash)
except XMPPError:
log.debug('Could not retrieve vCard for %s' % jid)
def _recv_presence(self, pres):
if not pres.match('presence/vcard_temp_update'):
self.api['set_hash'](pres['from'], args=None)
return
data = pres['vcard_temp_update']['photo']
if data is None:
return
elif data == '' or data != self.api['get_hash'](pres['to']):
self._reset_hash(pres['from'])
ifrom = pres['to'] if self.xmpp.is_component else None
self.api['reset_hash'](pres['from'], ifrom=ifrom)
self.xmpp.event('vcard_avatar_update', pres)
# =================================================================

View File

@@ -74,7 +74,7 @@ class XEP_0163(BasePlugin):
be a list of such namespaces.
jid -- Optionally specify the JID.
"""
if not isinstance(namespace, set) and not isinstance(namespace, list):
if not isinstance(namespace, (set, list)):
namespace = [namespace]
for ns in namespace:

View File

@@ -34,6 +34,12 @@ class XEP_0172(BasePlugin):
def plugin_init(self):
register_stanza_plugin(Message, UserNick)
register_stanza_plugin(Presence, UserNick)
def plugin_end(self):
self.xmpp['xep_0030'].del_feature(feature=UserNick.namespace)
self.xmpp['xep_0163'].remove_interest(UserNick.namespace)
def session_bind(self, jid):
self.xmpp['xep_0163'].register_pep('user_nick', UserNick)
def publish_nick(self, nick=None, options=None, ifrom=None, block=True,

View File

@@ -26,13 +26,14 @@ class XEP_0184(BasePlugin):
description = 'XEP-0184: Message Delivery Receipts'
dependencies = set(['xep_0030'])
stanza = stanza
default_config = {
'auto_ack': True,
'auto_request': False
}
ack_types = ('normal', 'chat', 'headline')
def plugin_init(self):
self.auto_ack = self.config.get('auto_ack', True)
self.auto_request = self.config.get('auto_request', False)
register_stanza_plugin(Message, Request)
register_stanza_plugin(Message, Received)
@@ -48,6 +49,13 @@ class XEP_0184(BasePlugin):
StanzaPath('message/request_receipt'),
self._handle_receipt_request))
def plugin_end(self):
self.xmpp['xep_0030'].del_feature('urn:xmpp:receipts')
self.xmpp.del_filter('out', self._filter_add_receipt_request)
self.xmpp.remove_handler('Message Receipt')
self.xmpp.remove_handler('Message Receipt Request')
def session_bind(self, jid):
self.xmpp['xep_0030'].add_feature('urn:xmpp:receipts')
def ack(self, msg):
@@ -61,7 +69,7 @@ class XEP_0184(BasePlugin):
ack['to'] = msg['from']
ack['from'] = msg['to']
ack['receipt'] = msg['id']
ack['id'] = self.xmpp.new_id()
ack['id'] = msg['id']
ack.send()
def _handle_receipt_received(self, msg):

View File

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

View File

@@ -0,0 +1,83 @@
"""
SleekXMPP: The Sleek XMPP Library
Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
This file is part of SleekXMPP.
See the file LICENSE for copying permission.
"""
import logging
from sleekxmpp import Iq
from sleekxmpp.plugins import BasePlugin
from sleekxmpp.xmlstream.handler import Callback
from sleekxmpp.xmlstream.matcher import StanzaPath
from sleekxmpp.xmlstream import register_stanza_plugin, JID
from sleekxmpp.plugins.xep_0191 import stanza, Block, Unblock, BlockList
log = logging.getLogger(__name__)
class XEP_0191(BasePlugin):
name = 'xep_0191'
description = 'XEP-0191: Blocking Command'
dependencies = set(['xep_0030'])
stanza = stanza
def plugin_init(self):
register_stanza_plugin(Iq, BlockList)
register_stanza_plugin(Iq, Block)
register_stanza_plugin(Iq, Unblock)
self.xmpp.register_handler(
Callback('Blocked Contact',
StanzaPath('iq@type=set/block'),
self._handle_blocked))
self.xmpp.register_handler(
Callback('Unblocked Contact',
StanzaPath('iq@type=set/unblock'),
self._handle_unblocked))
def plugin_end(self):
self.xmpp.remove_handler('Blocked Contact')
self.xmpp.remove_handler('Unblocked Contact')
def get_blocked(self, ifrom=None, block=True, timeout=None, callback=None):
iq = self.xmpp.Iq()
iq['type'] = 'get'
iq['from'] = ifrom
iq.enable('blocklist')
return iq.send(block=block, timeout=timeout, callback=callback)
def block(self, jids, ifrom=None, block=True, timeout=None, callback=None):
iq = self.xmpp.Iq()
iq['type'] = 'set'
iq['from'] = ifrom
if not isinstance(jids, (set, list)):
jids = [jids]
iq['block']['items'] = jids
return iq.send(block=block, timeout=timeout, callback=callback)
def unblock(self, jids=None, ifrom=None, block=True, timeout=None, callback=None):
iq = self.xmpp.Iq()
iq['type'] = 'set'
iq['from'] = ifrom
if jids is None:
jids = []
if not isinstance(jids, (set, list)):
jids = [jids]
iq['unblock']['items'] = jids
return iq.send(block=block, timeout=timeout, callback=callback)
def _handle_blocked(self, iq):
self.xmpp.event('blocked', iq)
def _handle_unblocked(self, iq):
self.xmpp.event('unblocked', iq)

View File

@@ -0,0 +1,50 @@
"""
SleekXMPP: The Sleek XMPP Library
Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
This file is part of SleekXMPP.
See the file LICENSE for copying permission.
"""
from sleekxmpp.xmlstream import ET, ElementBase, JID
class BlockList(ElementBase):
name = 'blocklist'
namespace = 'urn:xmpp:blocking'
plugin_attrib = 'blocklist'
interfaces = set(['items'])
def get_items(self):
result = set()
items = self.xml.findall('{%s}item' % self.namespace)
if items is not None:
for item in items:
jid = JID(item.attrib.get('jid', ''))
if jid:
result.add(jid)
return result
def set_items(self, values):
self.del_items()
for jid in values:
if jid:
item = ET.Element('{%s}item' % self.namespace)
item.attrib['jid'] = JID(jid).full
self.xml.append(item)
def del_items(self):
items = self.xml.findall('{%s}item' % self.namespace)
if items is not None:
for item in items:
self.xml.remove(item)
class Block(BlockList):
name = 'block'
plugin_attrib = 'block'
class Unblock(BlockList):
name = 'unblock'
plugin_attrib = 'unblock'

View File

@@ -34,6 +34,32 @@ class XEP_0198(BasePlugin):
description = 'XEP-0198: Stream Management'
dependencies = set()
stanza = stanza
default_config = {
#: The last ack number received from the server.
'last_ack': 0,
#: The number of stanzas to wait between sending ack requests to
#: the server. Setting this to ``1`` will send an ack request after
#: every sent stanza. Defaults to ``5``.
'window': 5,
#: The stream management ID for the stream. Knowing this value is
#: required in order to do stream resumption.
'sm_id': None,
#: A counter of handled incoming stanzas, mod 2^32.
'handled': 0,
#: A counter of unacked outgoing stanzas, mod 2^32.
'seq': 0,
#: Control whether or not the ability to resume the stream will be
#: requested when enabling stream management. Defaults to ``True``.
'allow_resume': True,
'order': 10100,
'resume_order': 9000
}
def plugin_init(self):
"""Start the XEP-0198 plugin."""
@@ -43,30 +69,9 @@ class XEP_0198(BasePlugin):
if self.xmpp.is_component:
return
#: The stream management ID for the stream. Knowing this value is
#: required in order to do stream resumption.
self.sm_id = self.config.get('sm_id', None)
#: A counter of handled incoming stanzas, mod 2^32.
self.handled = self.config.get('handled', 0)
#: A counter of unacked outgoing stanzas, mod 2^32.
self.seq = self.config.get('seq', 0)
#: The last ack number received from the server.
self.last_ack = self.config.get('last_ack', 0)
#: The number of stanzas to wait between sending ack requests to
#: the server. Setting this to ``1`` will send an ack request after
#: every sent stanza. Defaults to ``5``.
self.window = self.config.get('window', 5)
self.window_counter = self.window
self.window_counter_lock = threading.Lock()
#: Control whether or not the ability to resume the stream will be
#: requested when enabling stream management. Defaults to ``True``.
self.allow_resume = self.config.get('allow_resume', True)
self.enabled = threading.Event()
self.unacked_queue = collections.deque()
@@ -92,11 +97,11 @@ class XEP_0198(BasePlugin):
self.xmpp.register_feature('sm',
self._handle_sm_feature,
restart=True,
order=self.config.get('order', 10100))
order=self.order)
self.xmpp.register_feature('sm',
self._handle_sm_feature,
restart=True,
order=self.config.get('resume_order', 9000))
order=self.resume_order)
self.xmpp.register_handler(
Callback('Stream Management Enabled',
@@ -133,6 +138,27 @@ class XEP_0198(BasePlugin):
self.xmpp.add_event_handler('session_end', self.session_end)
def plugin_end(self):
if self.xmpp.is_component:
return
self.xmpp.unregister_feature('sm', self.order)
self.xmpp.unregister_feature('sm', self.resume_order)
self.xmpp.del_event_handler('session_end', self.session_end)
self.xmpp.del_filter('in', self._handle_incoming)
self.xmpp.del_filter('out_sync', self._handle_outgoing)
self.xmpp.remove_handler('Stream Management Enabled')
self.xmpp.remove_handler('Stream Management Resumed')
self.xmpp.remove_handler('Stream Management Failed')
self.xmpp.remove_handler('Stream Management Ack')
self.xmpp.remove_handler('Stream Management Request Ack')
self.xmpp.remove_stanza(stanza.Enable)
self.xmpp.remove_stanza(stanza.Enabled)
self.xmpp.remove_stanza(stanza.Resume)
self.xmpp.remove_stanza(stanza.Resumed)
self.xmpp.remove_stanza(stanza.Ack)
self.xmpp.remove_stanza(stanza.RequestAck)
def session_end(self, event):
"""Reset stream management state."""
self.enabled.clear()

View File

@@ -51,15 +51,16 @@ class XEP_0199(BasePlugin):
description = 'XEP-0199: XMPP Ping'
dependencies = set(['xep_0030'])
stanza = stanza
default_config = {
'keepalive': False,
'frequency': 300,
'timeout': 30
}
def plugin_init(self):
"""
Start the XEP-0199 plugin.
"""
self.keepalive = self.config.get('keepalive', False)
self.frequency = float(self.config.get('frequency', 300))
self.timeout = self.config.get('timeout', 30)
register_stanza_plugin(Iq, Ping)
self.xmpp.register_handler(
@@ -74,6 +75,16 @@ class XEP_0199(BasePlugin):
self.xmpp.add_event_handler('session_end',
self._handle_session_end)
def plugin_end(self):
self.xmpp['xep_0030'].del_feature(feature=Ping.namespace)
self.xmpp.remove_handler('Ping')
if self.keepalive:
self.xmpp.del_event_handler('session_start',
self._handle_keepalive)
self.xmpp.del_event_handler('session_end',
self._handle_session_end)
def session_bind(self, jid):
self.xmpp['xep_0030'].add_feature(Ping.namespace)
def _handle_keepalive(self, event):

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