Compare commits
240 Commits
sleek-1.1.
...
hildjj-dev
Author | SHA1 | Date | |
---|---|---|---|
![]() |
48def71d0c | ||
![]() |
12e8bb6ddc | ||
![]() |
c8c20fff71 | ||
![]() |
75a18b5ffe | ||
![]() |
06a690a259 | ||
![]() |
52feabbe76 | ||
![]() |
14c9e9a9cc | ||
![]() |
a22ca228cc | ||
![]() |
d0666a5eb6 | ||
![]() |
931d49560a | ||
![]() |
2a4e435228 | ||
![]() |
3655827ef2 | ||
![]() |
c5046b9c91 | ||
![]() |
4598031dd2 | ||
![]() |
12e0e1a16b | ||
![]() |
5e9266ba90 | ||
![]() |
0d448b8221 | ||
![]() |
e6c95f0a2a | ||
![]() |
63b58edda1 | ||
![]() |
af9632519c | ||
![]() |
d367fb938d | ||
![]() |
77f2a339e1 | ||
![]() |
4190027a78 | ||
![]() |
ef48a8c4d9 | ||
![]() |
829b225053 | ||
![]() |
747a6e94e6 | ||
![]() |
cebc798e72 | ||
![]() |
7c485c6a8b | ||
![]() |
e2e8c4b5dc | ||
![]() |
675c0112ac | ||
![]() |
4dd2c15775 | ||
![]() |
9f6decdbc1 | ||
![]() |
fc07e23ff8 | ||
![]() |
4ea328b9f2 | ||
![]() |
84a2fc382b | ||
![]() |
098714b3c4 | ||
![]() |
cf2c94d974 | ||
![]() |
657102e938 | ||
![]() |
44e7585bf8 | ||
![]() |
94488fa2ea | ||
![]() |
a2c60a4911 | ||
![]() |
ee9c4abd08 | ||
![]() |
b5b1c932c7 | ||
![]() |
b8f04983e1 | ||
![]() |
90807dd973 | ||
![]() |
ef974114ea | ||
![]() |
f6e1fecdf8 | ||
![]() |
94e8b2becf | ||
![]() |
a6ca6701a0 | ||
![]() |
c4edb9724b | ||
![]() |
b5c669bdff | ||
![]() |
e449dce65c | ||
![]() |
73ce9a5ecc | ||
![]() |
671f680bb3 | ||
![]() |
dfff19ffbf | ||
![]() |
a4abdf9fa6 | ||
![]() |
6c57bb0553 | ||
![]() |
d385b9e708 | ||
![]() |
c2ae1ee891 | ||
![]() |
67147570e9 | ||
![]() |
fb3e6b7e35 | ||
![]() |
cf28d4586d | ||
![]() |
f65eb5eeea | ||
![]() |
26fa9bd87e | ||
![]() |
0016d9a638 | ||
![]() |
a88b9737ff | ||
![]() |
df9ac58d05 | ||
![]() |
357406d801 | ||
![]() |
19a78f63f4 | ||
![]() |
c7ec6a72cd | ||
![]() |
e68b07dbce | ||
![]() |
e20610ab80 | ||
![]() |
1ca0c46333 | ||
![]() |
e510875f64 | ||
![]() |
f52a10b061 | ||
![]() |
7d382a2bfd | ||
![]() |
09bec1c4fe | ||
![]() |
ff28b0a005 | ||
![]() |
8a03bd72ae | ||
![]() |
a249f8736a | ||
![]() |
f0e1fc5aad | ||
![]() |
f09adf0014 | ||
![]() |
c6ac64ed2d | ||
![]() |
04dc68f5f6 | ||
![]() |
92be051450 | ||
![]() |
5c25208fb5 | ||
![]() |
779c258e27 | ||
![]() |
962dfad216 | ||
![]() |
f7a710e55b | ||
![]() |
814a50e36f | ||
![]() |
230465b946 | ||
![]() |
d11a67702e | ||
![]() |
4e12e228cb | ||
![]() |
4a94aeba49 | ||
![]() |
14aa831169 | ||
![]() |
295d23ccf3 | ||
![]() |
75d904ed01 | ||
![]() |
aebcf6ff82 | ||
![]() |
8c2ece3bca | ||
![]() |
80a90a6221 | ||
![]() |
f81d5e4bd6 | ||
![]() |
2324c90232 | ||
![]() |
2f65fdbc76 | ||
![]() |
59ff08174f | ||
![]() |
2f4149c7d0 | ||
![]() |
b84e359770 | ||
![]() |
fb4275648c | ||
![]() |
475ccfa8dc | ||
![]() |
267c24c8ef | ||
![]() |
06a9d9fc30 | ||
![]() |
1383ca19b5 | ||
![]() |
4c3ff2abab | ||
![]() |
7c6ef18e4f | ||
![]() |
f8856467d5 | ||
![]() |
3bd84b8d27 | ||
![]() |
bc8b5774ac | ||
![]() |
8009b0485e | ||
![]() |
8742a56b3e | ||
![]() |
a792bcdafe | ||
![]() |
167d1ce97b | ||
![]() |
695cd95657 | ||
![]() |
44ce01a70b | ||
![]() |
e4b4c67637 | ||
![]() |
422e77ae40 | ||
![]() |
5ae6c8f8fa | ||
![]() |
54656b331a | ||
![]() |
9047b627a4 | ||
![]() |
6645a3be40 | ||
![]() |
c2189b4ecd | ||
![]() |
e3fab66dfb | ||
![]() |
5867f08bf1 | ||
![]() |
a06fa2de67 | ||
![]() |
c9b2cf6043 | ||
![]() |
35396d2977 | ||
![]() |
3bff743d9f | ||
![]() |
5a878f829b | ||
![]() |
16ec0f151a | ||
![]() |
26dc6e90ea | ||
![]() |
94c749fd5a | ||
![]() |
7b80ed0807 | ||
![]() |
98b7e8b10a | ||
![]() |
c42f1ad4c7 | ||
![]() |
9d8de7fc15 | ||
![]() |
70883086b7 | ||
![]() |
9a08dfc7d4 | ||
![]() |
3e43b36a9d | ||
![]() |
352ee2f2fd | ||
![]() |
78aa5c3dfa | ||
![]() |
613323b5fb | ||
![]() |
6c4b01db8a | ||
![]() |
d06897a635 | ||
![]() |
1600bb0aaf | ||
![]() |
b5c9c98a8b | ||
![]() |
e4e18a416f | ||
![]() |
01cc0e6def | ||
![]() |
a3ec1af205 | ||
![]() |
d571d691a7 | ||
![]() |
2e580304f9 | ||
![]() |
fb221a8dc0 | ||
![]() |
459e1ed345 | ||
![]() |
6680c244f5 | ||
![]() |
06423964ec | ||
![]() |
5492e9028d | ||
![]() |
474390fa00 | ||
![]() |
81d3723084 | ||
![]() |
32e798967e | ||
![]() |
060c9ab679 | ||
![]() |
acd9c32a9f | ||
![]() |
b8581b0278 | ||
![]() |
917faecdcb | ||
![]() |
78f0325398 | ||
![]() |
f6edaa56a6 | ||
![]() |
51fee28bf4 | ||
![]() |
e8a3e92ceb | ||
![]() |
5df3839b7a | ||
![]() |
8dcb441f44 | ||
![]() |
a347cf625a | ||
![]() |
46f49c7a12 | ||
![]() |
99701c947e | ||
![]() |
1baae1b81e | ||
![]() |
7d20f0e9a6 | ||
![]() |
fbad22a1cd | ||
![]() |
5af2f62c04 | ||
![]() |
4a4a03858e | ||
![]() |
1efe049959 | ||
![]() |
2393148908 | ||
![]() |
6819b57353 | ||
![]() |
88b5e60807 | ||
![]() |
c7594b3ef0 | ||
![]() |
b210870f48 | ||
![]() |
a26a8bd79c | ||
![]() |
9307a6915f | ||
![]() |
5d6019a962 | ||
![]() |
85ef2d8d0b | ||
![]() |
c2c7cc032b | ||
![]() |
e4911e9391 | ||
![]() |
b11e1ee92d | ||
![]() |
5027d00c10 | ||
![]() |
69ddeceb49 | ||
![]() |
82698672bb | ||
![]() |
9cec284947 | ||
![]() |
dc501d1902 | ||
![]() |
100e504b7f | ||
![]() |
eb5df1aa37 | ||
![]() |
546066d677 | ||
![]() |
3234596974 | ||
![]() |
5820d49cd4 | ||
![]() |
1ab66e5767 | ||
![]() |
aab2682f9a | ||
![]() |
55d332bcc8 | ||
![]() |
f89df6e70c | ||
![]() |
250d28e870 | ||
![]() |
19f65c8510 | ||
![]() |
f70b49882f | ||
![]() |
a7b092a305 | ||
![]() |
daa73a3f3c | ||
![]() |
0b51afe87a | ||
![]() |
2b298766c9 | ||
![]() |
10664d723b | ||
![]() |
c012208a8f | ||
![]() |
0953896d2d | ||
![]() |
cf9e89d0ae | ||
![]() |
48dd01b0bb | ||
![]() |
7247efe055 | ||
![]() |
8def3758e4 | ||
![]() |
1851ab6f5f | ||
![]() |
289b052338 | ||
![]() |
26147f5ae0 | ||
![]() |
ae01f1071a | ||
![]() |
dcdf5dcd09 | ||
![]() |
c59a6d0f51 | ||
![]() |
2cd936318d | ||
![]() |
2f38857681 | ||
![]() |
39505ae1ff | ||
![]() |
44ee0633f2 | ||
![]() |
b52d2768b0 | ||
![]() |
cf24b870b1 | ||
![]() |
69cffce7dc | ||
![]() |
a14979375b | ||
![]() |
40ef4a16b1 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -7,3 +7,4 @@ docs/_build/
|
||||
.tox/
|
||||
.coverage
|
||||
sleekxmpp.egg-info/
|
||||
.ropeproject/
|
||||
|
29
LICENSE
29
LICENSE
@@ -69,8 +69,8 @@ modification, are permitted provided that the following conditions are met:
|
||||
* Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
* Neither the name of Red Innovation nor the names of its contributors
|
||||
may be used to endorse or promote products derived from this software
|
||||
* Neither the name of Red Innovation nor the names of its contributors
|
||||
may be used to endorse or promote products derived from this software
|
||||
without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY RED INNOVATION ``AS IS'' AND ANY
|
||||
@@ -167,3 +167,28 @@ PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
||||
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
|
||||
OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
|
||||
ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
socksipy: A Python SOCKS client module.
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Copyright 2006 Dan-Haim. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
3. Neither the name of Dan Haim nor the names of his contributors may be used
|
||||
to endorse or promote products derived from this software without specific
|
||||
prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY DAN HAIM "AS IS" AND ANY EXPRESS OR IMPLIED
|
||||
WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
|
||||
EVENT SHALL DAN HAIM OR HIS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
|
||||
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA
|
||||
OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
||||
LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
|
||||
OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMANGE.
|
||||
|
@@ -45,13 +45,11 @@ The latest source code for SleekXMPP may be found on `Github
|
||||
``develop`` branch.
|
||||
|
||||
**Latest Release**
|
||||
- `1.1.7 <http://github.com/fritzy/SleekXMPP/zipball/1.1.7>`_
|
||||
- `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``.
|
||||
|
@@ -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::
|
||||
|
||||
|
@@ -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
|
||||
|
||||
|
@@ -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
178
examples/admin_commands.py
Executable 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.")
|
@@ -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)
|
||||
|
@@ -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
|
||||
|
||||
|
@@ -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__':
|
||||
|
184
examples/download_avatars.py
Normal file
184
examples/download_avatars.py
Normal 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.")
|
@@ -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
|
||||
|
||||
@@ -122,6 +122,19 @@ if __name__ == '__main__':
|
||||
xmpp.register_plugin('xep_0060') # PubSub
|
||||
xmpp.register_plugin('xep_0199') # XMPP Ping
|
||||
|
||||
# If you are connecting to Facebook and wish to use the
|
||||
# X-FACEBOOK-PLATFORM authentication mechanism, you will need
|
||||
# your API key and an access token. Then you'll set:
|
||||
# xmpp.credentials['api_key'] = 'THE_API_KEY'
|
||||
# xmpp.credentials['access_token'] = 'THE_ACCESS_TOKEN'
|
||||
|
||||
# If you are connecting to MSN, then you will need an
|
||||
# access token, and it does not matter what JID you
|
||||
# specify other than that the domain is 'messenger.live.com',
|
||||
# so '_@messenger.live.com' will work. You can specify
|
||||
# the access token as so:
|
||||
# xmpp.credentials['access_token'] = 'THE_ACCESS_TOKEN'
|
||||
|
||||
# If you are working with an OpenFire server, you may need
|
||||
# to adjust the SSL version used:
|
||||
# xmpp.ssl_version = ssl.PROTOCOL_SSLv3
|
||||
|
@@ -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
|
||||
|
||||
|
@@ -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
|
||||
|
||||
|
@@ -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.
|
||||
|
@@ -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
|
||||
|
||||
|
@@ -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
|
||||
|
||||
|
@@ -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
|
||||
|
||||
|
@@ -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
|
||||
|
||||
|
@@ -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
|
||||
|
||||
|
@@ -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
|
||||
|
||||
|
@@ -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
|
||||
|
||||
|
@@ -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
|
||||
|
||||
|
@@ -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
174
examples/set_avatar.py
Normal 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.")
|
@@ -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
|
||||
|
||||
|
19
setup.py
19
setup.py
@@ -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,16 +60,20 @@ 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',
|
||||
'sleekxmpp/plugins/xep_0060',
|
||||
'sleekxmpp/plugins/xep_0060/stanza',
|
||||
'sleekxmpp/plugins/xep_0065',
|
||||
'sleekxmpp/plugins/xep_0066',
|
||||
'sleekxmpp/plugins/xep_0077',
|
||||
'sleekxmpp/plugins/xep_0078',
|
||||
@@ -75,16 +81,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 +101,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 +117,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(
|
||||
|
@@ -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 *
|
||||
|
@@ -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
|
||||
|
@@ -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:
|
||||
@@ -134,11 +159,14 @@ class BaseXMPP(XMLStream):
|
||||
Callback('Presence',
|
||||
MatchXPath("{%s}presence" % self.default_ns),
|
||||
self._handle_presence))
|
||||
|
||||
self.register_handler(
|
||||
Callback('Stream Error',
|
||||
MatchXPath("{%s}error" % self.stream_ns),
|
||||
self._handle_stream_error))
|
||||
|
||||
self.add_event_handler('session_start',
|
||||
self._handle_session_start)
|
||||
self.add_event_handler('disconnected',
|
||||
self._handle_disconnected)
|
||||
self.add_event_handler('presence_available',
|
||||
@@ -184,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.
|
||||
|
||||
@@ -209,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'):
|
||||
@@ -586,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
|
||||
@@ -640,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:
|
||||
@@ -651,30 +676,61 @@ class BaseXMPP(XMLStream):
|
||||
def getjidbare(self, fulljid):
|
||||
return fulljid.split('/', 1)[0]
|
||||
|
||||
def _handle_session_start(self, event):
|
||||
"""Reset redirection attempt count."""
|
||||
self._redirect_attempts = 0
|
||||
|
||||
def _handle_disconnected(self, event):
|
||||
"""When disconnected, reset the roster"""
|
||||
self.roster.reset()
|
||||
self.session_bind_event.clear()
|
||||
|
||||
def _handle_stream_error(self, error):
|
||||
self.event('stream_error', error)
|
||||
|
||||
if error['condition'] == 'see-other-host':
|
||||
other_host = error['see_other_host']
|
||||
if not other_host:
|
||||
log.warning("No other host specified.")
|
||||
return
|
||||
|
||||
if self._redirect_attempts > self.max_redirects:
|
||||
log.error("Exceeded maximum number of redirection attempts.")
|
||||
return
|
||||
|
||||
self._redirect_attempts += 1
|
||||
|
||||
host = other_host
|
||||
port = 5222
|
||||
|
||||
if '[' in other_host and ']' in other_host:
|
||||
host = other_host.split(']')[0][1:]
|
||||
elif ':' in other_host:
|
||||
host = other_host.split(':')[0]
|
||||
|
||||
port_sec = other_host.split(']')[-1]
|
||||
if ':' in port_sec:
|
||||
port = int(port_sec.split(':')[1])
|
||||
|
||||
self.address = (host, port)
|
||||
self.default_domain = host
|
||||
self.dns_records = None
|
||||
self.reconnect_delay = None
|
||||
self.reconnect()
|
||||
|
||||
def _handle_message(self, msg):
|
||||
"""Process incoming message stanzas."""
|
||||
if not self.is_component and not msg['to'].bare:
|
||||
msg['to'] = self.boundjid
|
||||
self.event('message', msg)
|
||||
|
||||
def _handle_available(self, presence):
|
||||
pto = presence['to'].bare
|
||||
pfrom = presence['from'].bare
|
||||
self.roster[pto][pfrom].handle_available(presence)
|
||||
def _handle_available(self, pres):
|
||||
self.roster[pres['to']][pres['from']].handle_available(pres)
|
||||
|
||||
def _handle_unavailable(self, presence):
|
||||
pto = presence['to'].bare
|
||||
pfrom = presence['from'].bare
|
||||
self.roster[pto][pfrom].handle_unavailable(presence)
|
||||
def _handle_unavailable(self, pres):
|
||||
self.roster[pres['to']][pres['from']].handle_unavailable(pres)
|
||||
|
||||
def _handle_new_subscription(self, stanza):
|
||||
def _handle_new_subscription(self, pres):
|
||||
"""Attempt to automatically handle subscription requests.
|
||||
|
||||
Subscriptions will be approved if the request is from
|
||||
@@ -686,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:
|
||||
@@ -697,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.
|
||||
|
@@ -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':
|
||||
|
@@ -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)
|
||||
|
@@ -11,5 +11,6 @@ __all__ = [
|
||||
'feature_mechanisms',
|
||||
'feature_bind',
|
||||
'feature_session',
|
||||
'feature_rosterver'
|
||||
'feature_rosterver',
|
||||
'feature_preapproval'
|
||||
]
|
||||
|
@@ -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')
|
||||
|
||||
|
@@ -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."""
|
||||
|
@@ -8,8 +8,7 @@
|
||||
|
||||
import base64
|
||||
|
||||
from sleekxmpp.thirdparty.suelta.util import bytes
|
||||
|
||||
from sleekxmpp.util import bytes
|
||||
from sleekxmpp.xmlstream import StanzaBase
|
||||
|
||||
|
||||
|
@@ -8,8 +8,7 @@
|
||||
|
||||
import base64
|
||||
|
||||
from sleekxmpp.thirdparty.suelta.util import bytes
|
||||
|
||||
from sleekxmpp.util import bytes
|
||||
from sleekxmpp.xmlstream import StanzaBase
|
||||
|
||||
|
||||
|
@@ -8,8 +8,7 @@
|
||||
|
||||
import base64
|
||||
|
||||
from sleekxmpp.thirdparty.suelta.util import bytes
|
||||
|
||||
from sleekxmpp.util import bytes
|
||||
from sleekxmpp.xmlstream import StanzaBase
|
||||
|
||||
|
||||
|
@@ -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 = ''
|
||||
|
15
sleekxmpp/features/feature_preapproval/__init__.py
Normal file
15
sleekxmpp/features/feature_preapproval/__init__.py
Normal 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)
|
42
sleekxmpp/features/feature_preapproval/preapproval.py
Normal file
42
sleekxmpp/features/feature_preapproval/preapproval.py
Normal 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')
|
17
sleekxmpp/features/feature_preapproval/stanza.py
Normal file
17
sleekxmpp/features/feature_preapproval/stanza.py
Normal 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'
|
@@ -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
582
sleekxmpp/jid.py
Normal 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)
|
@@ -18,15 +18,19 @@ __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
|
||||
'xep_0060', # Pubsub (Client)
|
||||
'xep_0065', # SOCKS5 Bytestreams
|
||||
'xep_0066', # Out of Band Data
|
||||
'xep_0077', # In-Band Registration
|
||||
# 'xep_0078', # Non-SASL auth. Don't automatically load
|
||||
@@ -35,17 +39,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,6 +64,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
|
||||
]
|
||||
|
@@ -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.
|
||||
|
||||
|
@@ -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=''):
|
||||
|
@@ -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
|
||||
|
@@ -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)
|
||||
|
||||
|
15
sleekxmpp/plugins/xep_0013/__init__.py
Normal file
15
sleekxmpp/plugins/xep_0013/__init__.py
Normal 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)
|
134
sleekxmpp/plugins/xep_0013/offline.py
Normal file
134
sleekxmpp/plugins/xep_0013/offline.py
Normal 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)
|
53
sleekxmpp/plugins/xep_0013/stanza.py
Normal file
53
sleekxmpp/plugins/xep_0013/stanza.py
Normal 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)
|
16
sleekxmpp/plugins/xep_0016/__init__.py
Normal file
16
sleekxmpp/plugins/xep_0016/__init__.py
Normal 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)
|
110
sleekxmpp/plugins/xep_0016/privacy.py
Normal file
110
sleekxmpp/plugins/xep_0016/privacy.py
Normal 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)
|
103
sleekxmpp/plugins/xep_0016/stanza.py
Normal file
103
sleekxmpp/plugins/xep_0016/stanza.py
Normal 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)
|
@@ -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 \
|
||||
|
@@ -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()
|
||||
|
@@ -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:
|
||||
|
@@ -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)
|
||||
|
||||
|
@@ -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)
|
||||
|
@@ -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):
|
||||
|
@@ -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):
|
||||
|
@@ -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()
|
||||
|
15
sleekxmpp/plugins/xep_0049/__init__.py
Normal file
15
sleekxmpp/plugins/xep_0049/__init__.py
Normal 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)
|
53
sleekxmpp/plugins/xep_0049/private_storage.py
Normal file
53
sleekxmpp/plugins/xep_0049/private_storage.py
Normal 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)
|
17
sleekxmpp/plugins/xep_0049/stanza.py
Normal file
17
sleekxmpp/plugins/xep_0049/stanza.py
Normal 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()
|
@@ -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',
|
||||
|
@@ -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)
|
||||
|
@@ -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()
|
||||
|
@@ -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)
|
||||
|
@@ -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']
|
||||
|
5
sleekxmpp/plugins/xep_0065/__init__.py
Normal file
5
sleekxmpp/plugins/xep_0065/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from sleekxmpp.plugins.base import register_plugin
|
||||
from sleekxmpp.plugins.xep_0065.proxy import XEP_0065
|
||||
|
||||
|
||||
register_plugin(XEP_0065)
|
359
sleekxmpp/plugins/xep_0065/proxy.py
Normal file
359
sleekxmpp/plugins/xep_0065/proxy.py
Normal file
@@ -0,0 +1,359 @@
|
||||
import sys
|
||||
import logging
|
||||
import struct
|
||||
|
||||
from threading import Thread, Event
|
||||
from hashlib import sha1
|
||||
from select import select
|
||||
from uuid import uuid4
|
||||
|
||||
from sleekxmpp.plugins.xep_0065 import stanza
|
||||
|
||||
from sleekxmpp.plugins.base import base_plugin
|
||||
from sleekxmpp.xmlstream.handler import Callback
|
||||
from sleekxmpp.xmlstream.matcher import StanzaPath
|
||||
from sleekxmpp.thirdparty.socks import socksocket, PROXY_TYPE_SOCKS5
|
||||
|
||||
# Registers the sleekxmpp logger
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class XEP_0065(base_plugin):
|
||||
"""
|
||||
XEP-0065 Socks5 Bytestreams
|
||||
"""
|
||||
|
||||
description = "Socks5 Bytestreams"
|
||||
dependencies = set(['xep_0030', ])
|
||||
xep = '0065'
|
||||
name = 'xep_0065'
|
||||
|
||||
# A dict contains for each SID, the proxy thread currently
|
||||
# running.
|
||||
proxy_threads = {}
|
||||
|
||||
def plugin_init(self):
|
||||
""" Initializes the xep_0065 plugin and all event callbacks.
|
||||
"""
|
||||
|
||||
# Shortcuts to access to the xep_0030 plugin.
|
||||
self.disco = self.xmpp['xep_0030']
|
||||
|
||||
# Handler for the streamhost stanza.
|
||||
self.xmpp.registerHandler(
|
||||
Callback('Socks5 Bytestreams',
|
||||
StanzaPath('iq@type=set/socks/streamhost'),
|
||||
self._handle_streamhost))
|
||||
|
||||
# Handler for the streamhost-used stanza.
|
||||
self.xmpp.registerHandler(
|
||||
Callback('Socks5 Bytestreams',
|
||||
StanzaPath('iq@type=result/socks/streamhost-used'),
|
||||
self._handle_streamhost_used))
|
||||
|
||||
def get_socket(self, sid):
|
||||
""" Returns the socket associated to the SID.
|
||||
"""
|
||||
|
||||
proxy = self.proxy_threads.get(sid)
|
||||
if proxy:
|
||||
return proxy.s
|
||||
|
||||
def handshake(self, to, streamer=None):
|
||||
""" Starts the handshake to establish the socks5 bytestreams
|
||||
connection.
|
||||
"""
|
||||
|
||||
# Discovers the proxy.
|
||||
self.streamer = streamer or self.discover_proxy()
|
||||
|
||||
# Requester requests network address from the proxy.
|
||||
streamhost = self.get_network_address(self.streamer)
|
||||
self.proxy_host = streamhost['socks']['streamhost']['host']
|
||||
self.proxy_port = streamhost['socks']['streamhost']['port']
|
||||
|
||||
# Generates the SID for this new handshake.
|
||||
sid = uuid4().hex
|
||||
|
||||
# Requester initiates S5B negotation with Target by sending
|
||||
# IQ-set that includes the JabberID and network address of
|
||||
# StreamHost as well as the StreamID (SID) of the proposed
|
||||
# bytestream.
|
||||
iq = self.xmpp.Iq(sto=to, stype='set')
|
||||
iq['socks']['sid'] = sid
|
||||
iq['socks']['streamhost']['jid'] = self.streamer
|
||||
iq['socks']['streamhost']['host'] = self.proxy_host
|
||||
iq['socks']['streamhost']['port'] = self.proxy_port
|
||||
|
||||
# Sends the new IQ.
|
||||
return iq.send()
|
||||
|
||||
def discover_proxy(self):
|
||||
""" Auto-discovers (using XEP 0030) the available bytestream
|
||||
proxy on the XMPP server.
|
||||
|
||||
Returns the JID of the proxy.
|
||||
"""
|
||||
|
||||
# Gets all disco items.
|
||||
disco_items = self.disco.get_items(self.xmpp.server)
|
||||
|
||||
for item in disco_items['disco_items']['items']:
|
||||
# For each items, gets the disco info.
|
||||
disco_info = self.disco.get_info(item[0])
|
||||
|
||||
# Gets and verifies if the identity is a bytestream proxy.
|
||||
identities = disco_info['disco_info']['identities']
|
||||
for identity in identities:
|
||||
if identity[0] == 'proxy' and identity[1] == 'bytestreams':
|
||||
# Returns when the first occurence is found.
|
||||
return '%s' % disco_info['from']
|
||||
|
||||
def get_network_address(self, streamer):
|
||||
""" Gets the streamhost information of the proxy.
|
||||
|
||||
streamer : The jid of the proxy.
|
||||
"""
|
||||
|
||||
iq = self.xmpp.Iq(sto=streamer, stype='get')
|
||||
iq['socks'] # Adds the query eleme to the iq.
|
||||
|
||||
return iq.send()
|
||||
|
||||
def _handle_streamhost(self, iq):
|
||||
""" Handles all streamhost stanzas.
|
||||
"""
|
||||
|
||||
# Registers the streamhost info.
|
||||
self.streamer = iq['socks']['streamhost']['jid']
|
||||
self.proxy_host = iq['socks']['streamhost']['host']
|
||||
self.proxy_port = iq['socks']['streamhost']['port']
|
||||
|
||||
# Sets the SID, the requester and the target.
|
||||
sid = iq['socks']['sid']
|
||||
requester = '%s' % iq['from']
|
||||
target = '%s' % self.xmpp.boundjid
|
||||
|
||||
# Next the Target attempts to open a standard TCP socket on
|
||||
# the network address of the Proxy.
|
||||
self.proxy_thread = Proxy(sid, requester, target, self.proxy_host,
|
||||
self.proxy_port, self.on_recv)
|
||||
self.proxy_thread.start()
|
||||
|
||||
# Registers the new thread in the proxy_thread dict.
|
||||
self.proxy_threads[sid] = self.proxy_thread
|
||||
|
||||
# Wait until the proxy is connected
|
||||
self.proxy_thread.connected.wait()
|
||||
|
||||
# Replies to the incoming iq with a streamhost-used stanza.
|
||||
res_iq = iq.reply()
|
||||
res_iq['socks']['sid'] = sid
|
||||
res_iq['socks']['streamhost-used']['jid'] = self.streamer
|
||||
|
||||
# Sends the IQ
|
||||
return res_iq.send()
|
||||
|
||||
def _handle_streamhost_used(self, iq):
|
||||
""" Handles all streamhost-used stanzas.
|
||||
"""
|
||||
|
||||
# Sets the SID, the requester and the target.
|
||||
sid = iq['socks']['sid']
|
||||
requester = '%s' % self.xmpp.boundjid
|
||||
target = '%s' % iq['from']
|
||||
|
||||
# The Requester will establish a connection to the SOCKS5
|
||||
# proxy in the same way the Target did.
|
||||
self.proxy_thread = Proxy(sid, requester, target, self.proxy_host,
|
||||
self.proxy_port, self.on_recv)
|
||||
self.proxy_thread.start()
|
||||
|
||||
# Registers the new thread in the proxy_thread dict.
|
||||
self.proxy_threads[sid] = self.proxy_thread
|
||||
|
||||
# Wait until the proxy is connected
|
||||
self.proxy_thread.connected.wait()
|
||||
|
||||
# Requester sends IQ-set to StreamHost requesting that
|
||||
# StreamHost activate the bytestream associated with the
|
||||
# StreamID.
|
||||
self.activate(iq['socks']['sid'], target)
|
||||
|
||||
def activate(self, sid, to):
|
||||
""" IQ-set to StreamHost requesting that StreamHost activate
|
||||
the bytestream associated with the StreamID.
|
||||
"""
|
||||
|
||||
# Creates the activate IQ.
|
||||
act_iq = self.xmpp.Iq(sto=self.streamer, stype='set')
|
||||
act_iq['socks']['sid'] = sid
|
||||
act_iq['socks']['activate'] = to
|
||||
|
||||
# Send the IQ.
|
||||
act_iq.send()
|
||||
|
||||
def deactivate(self, sid):
|
||||
""" Closes the Proxy thread associated to this SID.
|
||||
"""
|
||||
|
||||
proxy = self.proxy_threads.get(sid)
|
||||
if proxy:
|
||||
proxy.s.close()
|
||||
del self.proxy_threads[sid]
|
||||
|
||||
def close(self):
|
||||
""" Closes all Proxy threads.
|
||||
"""
|
||||
|
||||
for sid, proxy in self.proxy_threads.items():
|
||||
proxy.s.close()
|
||||
del self.proxy_threads[sid]
|
||||
|
||||
def send(self, sid, data):
|
||||
""" Sends the data over the Proxy socket associated to the
|
||||
SID.
|
||||
"""
|
||||
|
||||
proxy = self.proxy_threads.get(sid)
|
||||
if proxy:
|
||||
proxy.s.sendall(data)
|
||||
|
||||
def on_recv(self, sid, data):
|
||||
""" Calls when data is recv from the Proxy socket associated
|
||||
to the SID.
|
||||
|
||||
Triggers a socks_closed event if the socket is closed. The sid
|
||||
is passed to this event.
|
||||
|
||||
Triggers a socks_recv event if there's available data. A dict
|
||||
that contains the sid and the data is passed to this event.
|
||||
"""
|
||||
|
||||
proxy = self.proxy_threads.get(sid)
|
||||
if proxy:
|
||||
if not data:
|
||||
self.xmpp.event('socks_closed', sid)
|
||||
else:
|
||||
self.xmpp.event('socks_recv', {'sid': sid, 'data': data})
|
||||
|
||||
|
||||
class Proxy(Thread):
|
||||
""" Establishes in a thread a connection between the client and
|
||||
the server-side Socks5 proxy.
|
||||
"""
|
||||
|
||||
def __init__(self, sid, requester, target, proxy, proxy_port,
|
||||
on_recv):
|
||||
""" Initializes the proxy thread.
|
||||
|
||||
sid : The StreamID. <str>
|
||||
requester : The JID of the requester. <str>
|
||||
target : The JID of the target. <str>
|
||||
proxy_host : The hostname or the IP of the proxy. <str>
|
||||
proxy_port : The port of the proxy. <str> or <int>
|
||||
on_recv : A callback called when data are received from the
|
||||
socket. <Callable>
|
||||
"""
|
||||
|
||||
# Initializes the thread.
|
||||
Thread.__init__(self)
|
||||
|
||||
# Because the xep_0065 plugin uses the proxy_port as string,
|
||||
# the Proxy class accepts the proxy_port argument as a string
|
||||
# or an integer. Here, we force to use the port as an integer.
|
||||
proxy_port = int(proxy_port)
|
||||
|
||||
# Creates a connected event to warn when to proxy is
|
||||
# connected.
|
||||
self.connected = Event()
|
||||
|
||||
# Registers the arguments.
|
||||
self.sid = sid
|
||||
self.requester = requester
|
||||
self.target = target
|
||||
self.proxy = proxy
|
||||
self.proxy_port = proxy_port
|
||||
self.on_recv = on_recv
|
||||
|
||||
def run(self):
|
||||
""" Starts the thread.
|
||||
"""
|
||||
|
||||
# Creates the socks5 proxy socket
|
||||
self.s = socksocket()
|
||||
self.s.setproxy(PROXY_TYPE_SOCKS5, self.proxy, port=self.proxy_port)
|
||||
|
||||
# The hostname MUST be SHA1(SID + Requester JID + Target JID)
|
||||
# where the output is hexadecimal-encoded (not binary).
|
||||
digest = sha1()
|
||||
digest.update(self.sid) # SID
|
||||
digest.update(self.requester) # Requester JID
|
||||
digest.update(self.target) # Target JID
|
||||
|
||||
# Computes the digest in hex.
|
||||
dest = '%s' % digest.hexdigest()
|
||||
|
||||
# The port MUST be 0.
|
||||
self.s.connect((dest, 0))
|
||||
log.info('Socket connected.')
|
||||
self.connected.set()
|
||||
|
||||
# Blocks until the socket need to be closed.
|
||||
self.listen()
|
||||
|
||||
# Closes the socket.
|
||||
self.s.close()
|
||||
log.info('Socket closed.')
|
||||
|
||||
def listen(self):
|
||||
""" Listen for data on the socket. When receiving data, call
|
||||
the callback on_recv callable.
|
||||
"""
|
||||
|
||||
socket_open = True
|
||||
while socket_open:
|
||||
ins = []
|
||||
try:
|
||||
# Wait any read available data on socket. Timeout
|
||||
# after 5 secs.
|
||||
ins, out, err = select([self.s, ], [], [], 5)
|
||||
except Exception as e:
|
||||
# There's an error with the socket (maybe the socket
|
||||
# has been closed and the file descriptor is bad).
|
||||
log.debug('Socket error: %s' % e)
|
||||
break
|
||||
|
||||
for s in ins:
|
||||
data = self.recv_size(self.s)
|
||||
if not data:
|
||||
socket_open = False
|
||||
|
||||
self.on_recv(self.sid, data)
|
||||
|
||||
def recv_size(self, the_socket):
|
||||
total_len = 0
|
||||
total_data = []
|
||||
size = sys.maxint
|
||||
size_data = sock_data = ''
|
||||
recv_size = 8192
|
||||
|
||||
while total_len < size:
|
||||
sock_data = the_socket.recv(recv_size)
|
||||
if not sock_data:
|
||||
return ''.join(total_data)
|
||||
|
||||
if not total_data:
|
||||
if len(sock_data) > 4:
|
||||
size_data += sock_data
|
||||
size = struct.unpack('>i', size_data[:4])[0]
|
||||
recv_size = size
|
||||
if recv_size > 524288:
|
||||
recv_size = 524288
|
||||
total_data.append(size_data[4:])
|
||||
else:
|
||||
size_data += sock_data
|
||||
else:
|
||||
total_data.append(sock_data)
|
||||
total_len = sum([len(i) for i in total_data])
|
||||
return ''.join(total_data)
|
41
sleekxmpp/plugins/xep_0065/stanza.py
Normal file
41
sleekxmpp/plugins/xep_0065/stanza.py
Normal file
@@ -0,0 +1,41 @@
|
||||
from sleekxmpp import Iq
|
||||
from sleekxmpp.xmlstream import ElementBase, register_stanza_plugin
|
||||
|
||||
|
||||
# The protocol namespace defined in the Socks5Bytestream (0065) spec.
|
||||
namespace = 'http://jabber.org/protocol/bytestreams'
|
||||
|
||||
|
||||
class StreamHost(ElementBase):
|
||||
""" The streamhost xml element.
|
||||
"""
|
||||
|
||||
namespace = namespace
|
||||
name = 'streamhost'
|
||||
plugin_attrib = 'streamhost'
|
||||
interfaces = set(('host', 'jid', 'port'))
|
||||
|
||||
|
||||
class StreamHostUsed(ElementBase):
|
||||
""" The streamhost-used xml element.
|
||||
"""
|
||||
|
||||
namespace = namespace
|
||||
name = 'streamhost-used'
|
||||
plugin_attrib = 'streamhost-used'
|
||||
interfaces = set(('jid',))
|
||||
|
||||
|
||||
class Socks5(ElementBase):
|
||||
""" The query xml element.
|
||||
"""
|
||||
|
||||
namespace = namespace
|
||||
name = 'query'
|
||||
plugin_attrib = 'socks'
|
||||
interfaces = set(('sid', 'activate'))
|
||||
sub_interfaces = set(('activate',))
|
||||
|
||||
register_stanza_plugin(Iq, Socks5)
|
||||
register_stanza_plugin(Socks5, StreamHost)
|
||||
register_stanza_plugin(Socks5, StreamHostUsed)
|
@@ -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)
|
||||
|
||||
|
@@ -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
|
||||
|
@@ -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()
|
||||
|
@@ -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):
|
||||
|
@@ -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,
|
||||
|
@@ -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}
|
||||
|
@@ -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)
|
||||
|
@@ -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)
|
||||
|
16
sleekxmpp/plugins/xep_0091/__init__.py
Normal file
16
sleekxmpp/plugins/xep_0091/__init__.py
Normal 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)
|
29
sleekxmpp/plugins/xep_0091/legacy_delay.py
Normal file
29
sleekxmpp/plugins/xep_0091/legacy_delay.py
Normal 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)
|
46
sleekxmpp/plugins/xep_0091/stanza.py
Normal file
46
sleekxmpp/plugins/xep_0091/stanza.py
Normal 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 = ''
|
@@ -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
|
||||
|
26
sleekxmpp/plugins/xep_0106.py
Normal file
26
sleekxmpp/plugins/xep_0106.py
Normal 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)
|
@@ -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,
|
||||
|
@@ -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,
|
||||
|
@@ -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'])
|
||||
|
@@ -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,
|
||||
|
@@ -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)
|
||||
|
||||
|
16
sleekxmpp/plugins/xep_0131/__init__.py
Normal file
16
sleekxmpp/plugins/xep_0131/__init__.py
Normal 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)
|
41
sleekxmpp/plugins/xep_0131/headers.py
Normal file
41
sleekxmpp/plugins/xep_0131/headers.py
Normal 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))
|
51
sleekxmpp/plugins/xep_0131/stanza.py
Normal file
51
sleekxmpp/plugins/xep_0131/stanza.py
Normal 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)
|
54
sleekxmpp/plugins/xep_0133.py
Normal file
54
sleekxmpp/plugins/xep_0133.py
Normal 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)
|
@@ -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)
|
||||
|
||||
# =================================================================
|
||||
|
||||
|
@@ -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:
|
||||
|
@@ -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,
|
||||
|
@@ -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):
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user