Compare commits
67 Commits
sleek-1.1.
...
sleek-1.1.
Author | SHA1 | Date | |
---|---|---|---|
![]() |
e4b4c67637 | ||
![]() |
422e77ae40 | ||
![]() |
5ae6c8f8fa | ||
![]() |
54656b331a | ||
![]() |
9047b627a4 | ||
![]() |
6645a3be40 | ||
![]() |
e3fab66dfb | ||
![]() |
5867f08bf1 | ||
![]() |
a06fa2de67 | ||
![]() |
35396d2977 | ||
![]() |
3bff743d9f | ||
![]() |
5a878f829b | ||
![]() |
26dc6e90ea | ||
![]() |
94c749fd5a | ||
![]() |
7b80ed0807 | ||
![]() |
98b7e8b10a | ||
![]() |
9d8de7fc15 | ||
![]() |
70883086b7 | ||
![]() |
9a08dfc7d4 | ||
![]() |
3e43b36a9d | ||
![]() |
352ee2f2fd | ||
![]() |
78aa5c3dfa | ||
![]() |
613323b5fb | ||
![]() |
6c4b01db8a | ||
![]() |
d06897a635 | ||
![]() |
1600bb0aaf | ||
![]() |
b5c9c98a8b | ||
![]() |
e4e18a416f | ||
![]() |
01cc0e6def | ||
![]() |
d571d691a7 | ||
![]() |
fb221a8dc0 | ||
![]() |
459e1ed345 | ||
![]() |
6680c244f5 | ||
![]() |
06423964ec | ||
![]() |
474390fa00 | ||
![]() |
81d3723084 | ||
![]() |
32e798967e | ||
![]() |
acd9c32a9f | ||
![]() |
b8581b0278 | ||
![]() |
917faecdcb | ||
![]() |
f6edaa56a6 | ||
![]() |
51fee28bf4 | ||
![]() |
e8a3e92ceb | ||
![]() |
5df3839b7a | ||
![]() |
8dcb441f44 | ||
![]() |
a347cf625a | ||
![]() |
46f49c7a12 | ||
![]() |
99701c947e | ||
![]() |
1baae1b81e | ||
![]() |
7d20f0e9a6 | ||
![]() |
fbad22a1cd | ||
![]() |
5af2f62c04 | ||
![]() |
4a4a03858e | ||
![]() |
6819b57353 | ||
![]() |
88b5e60807 | ||
![]() |
a26a8bd79c | ||
![]() |
9307a6915f | ||
![]() |
85ef2d8d0b | ||
![]() |
c2c7cc032b | ||
![]() |
e4911e9391 | ||
![]() |
b11e1ee92d | ||
![]() |
5027d00c10 | ||
![]() |
69ddeceb49 | ||
![]() |
82698672bb | ||
![]() |
9cec284947 | ||
![]() |
dc501d1902 | ||
![]() |
100e504b7f |
@@ -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``.
|
||||
|
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):
|
||||
reload(sys)
|
||||
sys.setdefaultencoding('utf8')
|
||||
else:
|
||||
raw_input = input
|
||||
|
||||
|
||||
class AdminCommands(sleekxmpp.ClientXMPP):
|
||||
|
||||
"""
|
||||
A simple SleekXMPP bot that uses admin commands to
|
||||
add a new user to a server.
|
||||
"""
|
||||
|
||||
def __init__(self, jid, password, command):
|
||||
sleekxmpp.ClientXMPP.__init__(self, jid, password)
|
||||
|
||||
self.command = command
|
||||
|
||||
self.add_event_handler("session_start", self.start)
|
||||
|
||||
def start(self, event):
|
||||
"""
|
||||
Process the session_start event.
|
||||
|
||||
Typical actions for the session_start event are
|
||||
requesting the roster and broadcasting an initial
|
||||
presence stanza.
|
||||
|
||||
Arguments:
|
||||
event -- An empty dictionary. The session_start
|
||||
event does not provide any additional
|
||||
data.
|
||||
"""
|
||||
self.send_presence()
|
||||
self.get_roster()
|
||||
|
||||
def command_success(iq, session):
|
||||
print('Command completed')
|
||||
if iq['command']['form']:
|
||||
for var, field in iq['command']['form']['fields'].items():
|
||||
print('%s: %s' % (var, field['value']))
|
||||
if iq['command']['notes']:
|
||||
print('Command Notes:')
|
||||
for note in iq['command']['notes']:
|
||||
print('%s: %s' % note)
|
||||
self.disconnect()
|
||||
|
||||
def command_error(iq, session):
|
||||
print('Error completing command')
|
||||
print('%s: %s' % (iq['error']['condition'],
|
||||
iq['error']['text']))
|
||||
self['xep_0050'].terminate_command(session)
|
||||
self.disconnect()
|
||||
|
||||
def process_form(iq, session):
|
||||
form = iq['command']['form']
|
||||
answers = {}
|
||||
for var, field in form['fields'].items():
|
||||
if var != 'FORM_TYPE':
|
||||
if field['type'] == 'boolean':
|
||||
answers[var] = raw_input('%s (y/n): ' % field['label'])
|
||||
if answers[var].lower() in ('1', 'true', 'y', 'yes'):
|
||||
answers[var] = '1'
|
||||
else:
|
||||
answers[var] = '0'
|
||||
else:
|
||||
answers[var] = raw_input('%s: ' % field['label'])
|
||||
else:
|
||||
answers['FORM_TYPE'] = field['value']
|
||||
form['type'] = 'submit'
|
||||
form['values'] = answers
|
||||
|
||||
session['next'] = command_success
|
||||
session['payload'] = form
|
||||
|
||||
self['xep_0050'].complete_command(session)
|
||||
|
||||
session = {'next': process_form,
|
||||
'error': command_error}
|
||||
|
||||
command = self.command.replace('-', '_')
|
||||
handler = getattr(self['xep_0133'], command, None)
|
||||
|
||||
if handler:
|
||||
handler(session={
|
||||
'next': process_form,
|
||||
'error': command_error
|
||||
})
|
||||
else:
|
||||
print('Invalid command name: %s' % self.command)
|
||||
self.disconnect()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# Setup the command line arguments.
|
||||
optp = OptionParser()
|
||||
|
||||
# Output verbosity options.
|
||||
optp.add_option('-q', '--quiet', help='set logging to ERROR',
|
||||
action='store_const', dest='loglevel',
|
||||
const=logging.ERROR, default=logging.INFO)
|
||||
optp.add_option('-d', '--debug', help='set logging to DEBUG',
|
||||
action='store_const', dest='loglevel',
|
||||
const=logging.DEBUG, default=logging.INFO)
|
||||
optp.add_option('-v', '--verbose', help='set logging to COMM',
|
||||
action='store_const', dest='loglevel',
|
||||
const=5, default=logging.INFO)
|
||||
|
||||
# JID and password options.
|
||||
optp.add_option("-j", "--jid", dest="jid",
|
||||
help="JID to use")
|
||||
optp.add_option("-p", "--password", dest="password",
|
||||
help="password to use")
|
||||
optp.add_option("-c", "--command", dest="command",
|
||||
help="admin command to use")
|
||||
|
||||
opts, args = optp.parse_args()
|
||||
|
||||
# Setup logging.
|
||||
logging.basicConfig(level=opts.loglevel,
|
||||
format='%(levelname)-8s %(message)s')
|
||||
|
||||
if opts.jid is None:
|
||||
opts.jid = raw_input("Username: ")
|
||||
if opts.password is None:
|
||||
opts.password = getpass.getpass("Password: ")
|
||||
if opts.command is None:
|
||||
opts.command = raw_input("Admin command: ")
|
||||
|
||||
# Setup the CommandBot and register plugins. Note that while plugins may
|
||||
# have interdependencies, the order in which you register them does
|
||||
# not matter.
|
||||
xmpp = AdminCommands(opts.jid, opts.password, opts.command)
|
||||
xmpp.register_plugin('xep_0133') # Service Administration
|
||||
|
||||
# If you are working with an OpenFire server, you may need
|
||||
# to adjust the SSL version used:
|
||||
# xmpp.ssl_version = ssl.PROTOCOL_SSLv3
|
||||
|
||||
# If you want to verify the SSL certificates offered by a server:
|
||||
# xmpp.ca_certs = "path/to/ca/cert"
|
||||
|
||||
# Connect to the XMPP server and start processing XMPP stanzas.
|
||||
if xmpp.connect():
|
||||
# If you do not have the dnspython library installed, you will need
|
||||
# to manually specify the name of the server if it does not match
|
||||
# the one in the JID. For example, to use Google Talk you would
|
||||
# need to use:
|
||||
#
|
||||
# if xmpp.connect(('talk.google.com', 5222)):
|
||||
# ...
|
||||
xmpp.process(block=True)
|
||||
print("Done")
|
||||
else:
|
||||
print("Unable to connect.")
|
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):
|
||||
reload(sys)
|
||||
sys.setdefaultencoding('utf8')
|
||||
else:
|
||||
raw_input = input
|
||||
|
||||
|
||||
FILE_TYPES = {
|
||||
'image/png': 'png',
|
||||
'image/gif': 'gif',
|
||||
'image/jpeg': 'jpg'
|
||||
}
|
||||
|
||||
|
||||
class AvatarDownloader(sleekxmpp.ClientXMPP):
|
||||
|
||||
"""
|
||||
A basic script for downloading the avatars for a user's contacts.
|
||||
"""
|
||||
|
||||
def __init__(self, jid, password):
|
||||
sleekxmpp.ClientXMPP.__init__(self, jid, password)
|
||||
self.add_event_handler("session_start", self.start, threaded=True)
|
||||
self.add_event_handler("changed_status", self.wait_for_presences)
|
||||
|
||||
self.add_event_handler('vcard_avatar_update', self.on_vcard_avatar)
|
||||
self.add_event_handler('avatar_metadata_publish', self.on_avatar)
|
||||
|
||||
self.received = set()
|
||||
self.presences_received = threading.Event()
|
||||
|
||||
def start(self, event):
|
||||
"""
|
||||
Process the session_start event.
|
||||
|
||||
Typical actions for the session_start event are
|
||||
requesting the roster and broadcasting an initial
|
||||
presence stanza.
|
||||
|
||||
Arguments:
|
||||
event -- An empty dictionary. The session_start
|
||||
event does not provide any additional
|
||||
data.
|
||||
"""
|
||||
self.send_presence()
|
||||
self.get_roster()
|
||||
|
||||
print('Waiting for presence updates...\n')
|
||||
self.presences_received.wait(15)
|
||||
self.disconnect(wait=True)
|
||||
|
||||
def on_vcard_avatar(self, pres):
|
||||
print("Received vCard avatar update from %s" % pres['from'].bare)
|
||||
try:
|
||||
result = self['xep_0054'].get_vcard(pres['from'], cached=True)
|
||||
except XMPPError:
|
||||
print("Error retrieving avatar for %s" % pres['from'])
|
||||
return
|
||||
avatar = result['vcard_temp']['PHOTO']
|
||||
|
||||
filetype = FILE_TYPES.get(avatar['TYPE'], 'png')
|
||||
filename = 'vcard_avatar_%s_%s.%s' % (
|
||||
pres['from'].bare,
|
||||
pres['vcard_temp_update']['photo'],
|
||||
filetype)
|
||||
with open(filename, 'w+') as img:
|
||||
img.write(avatar['BINVAL'])
|
||||
|
||||
def on_avatar(self, msg):
|
||||
print("Received avatar update from %s" % msg['from'])
|
||||
metadata = msg['pubsub_event']['items']['item']['avatar_metadata']
|
||||
for info in metadata['items']:
|
||||
if not info['url']:
|
||||
try:
|
||||
result = self['xep_0084'].retrieve_avatar(msg['from'], info['id'])
|
||||
except XMPPError:
|
||||
print("Error retrieving avatar for %s" % msg['from'])
|
||||
return
|
||||
|
||||
avatar = result['pubsub']['items']['item']['avatar_data']
|
||||
|
||||
filetype = FILE_TYPES.get(metadata['type'], 'png')
|
||||
filename = 'avatar_%s_%s.%s' % (msg['from'].bare, info['id'], filetype)
|
||||
with open(filename, 'w+') as img:
|
||||
img.write(avatar['value'])
|
||||
else:
|
||||
# We could retrieve the avatar via HTTP, etc here instead.
|
||||
pass
|
||||
|
||||
def wait_for_presences(self, pres):
|
||||
"""
|
||||
Wait to receive updates from all roster contacts.
|
||||
"""
|
||||
self.received.add(pres['from'].bare)
|
||||
if len(self.received) >= len(self.client_roster.keys()):
|
||||
self.presences_received.set()
|
||||
else:
|
||||
self.presences_received.clear()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# Setup the command line arguments.
|
||||
optp = OptionParser()
|
||||
optp.add_option('-q','--quiet', help='set logging to ERROR',
|
||||
action='store_const',
|
||||
dest='loglevel',
|
||||
const=logging.ERROR,
|
||||
default=logging.ERROR)
|
||||
optp.add_option('-d','--debug', help='set logging to DEBUG',
|
||||
action='store_const',
|
||||
dest='loglevel',
|
||||
const=logging.DEBUG,
|
||||
default=logging.ERROR)
|
||||
optp.add_option('-v','--verbose', help='set logging to COMM',
|
||||
action='store_const',
|
||||
dest='loglevel',
|
||||
const=5,
|
||||
default=logging.ERROR)
|
||||
|
||||
# JID and password options.
|
||||
optp.add_option("-j", "--jid", dest="jid",
|
||||
help="JID to use")
|
||||
optp.add_option("-p", "--password", dest="password",
|
||||
help="password to use")
|
||||
opts,args = optp.parse_args()
|
||||
|
||||
# Setup logging.
|
||||
logging.basicConfig(level=opts.loglevel,
|
||||
format='%(levelname)-8s %(message)s')
|
||||
|
||||
if opts.jid is None:
|
||||
opts.jid = raw_input("Username: ")
|
||||
if opts.password is None:
|
||||
opts.password = getpass.getpass("Password: ")
|
||||
|
||||
xmpp = AvatarDownloader(opts.jid, opts.password)
|
||||
xmpp.register_plugin('xep_0054')
|
||||
xmpp.register_plugin('xep_0153')
|
||||
xmpp.register_plugin('xep_0084')
|
||||
|
||||
# If you are working with an OpenFire server, you may need
|
||||
# to adjust the SSL version used:
|
||||
# xmpp.ssl_version = ssl.PROTOCOL_SSLv3
|
||||
|
||||
# If you want to verify the SSL certificates offered by a server:
|
||||
# xmpp.ca_certs = "path/to/ca/cert"
|
||||
|
||||
# Connect to the XMPP server and start processing XMPP stanzas.
|
||||
if xmpp.connect():
|
||||
# If you do not have the dnspython library installed, you will need
|
||||
# to manually specify the name of the server if it does not match
|
||||
# the one in the JID. For example, to use Google Talk you would
|
||||
# need to use:
|
||||
#
|
||||
# if xmpp.connect(('talk.google.com', 5222)):
|
||||
# ...
|
||||
xmpp.process(block=True)
|
||||
else:
|
||||
print("Unable to connect.")
|
@@ -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
|
||||
|
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):
|
||||
reload(sys)
|
||||
sys.setdefaultencoding('utf8')
|
||||
else:
|
||||
raw_input = input
|
||||
|
||||
|
||||
class AvatarSetter(sleekxmpp.ClientXMPP):
|
||||
|
||||
"""
|
||||
A basic script for downloading the avatars for a user's contacts.
|
||||
"""
|
||||
|
||||
def __init__(self, jid, password, filepath):
|
||||
sleekxmpp.ClientXMPP.__init__(self, jid, password)
|
||||
|
||||
self.add_event_handler("session_start", self.start, threaded=True)
|
||||
|
||||
self.filepath = filepath
|
||||
|
||||
def start(self, event):
|
||||
"""
|
||||
Process the session_start event.
|
||||
|
||||
Typical actions for the session_start event are
|
||||
requesting the roster and broadcasting an initial
|
||||
presence stanza.
|
||||
|
||||
Arguments:
|
||||
event -- An empty dictionary. The session_start
|
||||
event does not provide any additional
|
||||
data.
|
||||
"""
|
||||
self.send_presence()
|
||||
self.get_roster()
|
||||
|
||||
avatar_file = None
|
||||
try:
|
||||
avatar_file = open(os.path.expanduser(self.filepath))
|
||||
except IOError:
|
||||
print('Could not find file: %s' % self.filepath)
|
||||
return self.disconnect()
|
||||
|
||||
avatar = avatar_file.read()
|
||||
|
||||
avatar_type = 'image/%s' % imghdr.what('', avatar)
|
||||
avatar_id = self['xep_0084'].generate_id(avatar)
|
||||
avatar_bytes = len(avatar)
|
||||
|
||||
avatar_file.close()
|
||||
|
||||
used_xep84 = False
|
||||
try:
|
||||
print('Publish XEP-0084 avatar data')
|
||||
self['xep_0084'].publish_avatar(avatar)
|
||||
used_xep84 = True
|
||||
except XMPPError:
|
||||
print('Could not publish XEP-0084 avatar')
|
||||
|
||||
try:
|
||||
print('Update vCard with avatar')
|
||||
self['xep_0153'].set_avatar(avatar=avatar, mtype=avatar_type)
|
||||
except XMPPError:
|
||||
print('Could not set vCard avatar')
|
||||
|
||||
if used_xep84:
|
||||
try:
|
||||
print('Advertise XEP-0084 avatar metadata')
|
||||
self['xep_0084'].publish_avatar_metadata([
|
||||
{'id': avatar_id,
|
||||
'type': avatar_type,
|
||||
'bytes': avatar_bytes}
|
||||
# We could advertise multiple avatars to provide
|
||||
# options in image type, source (HTTP vs pubsub),
|
||||
# size, etc.
|
||||
# {'id': ....}
|
||||
])
|
||||
except XMPPError:
|
||||
print('Could not publish XEP-0084 metadata')
|
||||
|
||||
print('Wait for presence updates to propagate...')
|
||||
self.schedule('end', 5, self.disconnect, kwargs={'wait': True})
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# Setup the command line arguments.
|
||||
optp = OptionParser()
|
||||
optp.add_option('-q','--quiet', help='set logging to ERROR',
|
||||
action='store_const',
|
||||
dest='loglevel',
|
||||
const=logging.ERROR,
|
||||
default=logging.ERROR)
|
||||
optp.add_option('-d','--debug', help='set logging to DEBUG',
|
||||
action='store_const',
|
||||
dest='loglevel',
|
||||
const=logging.DEBUG,
|
||||
default=logging.ERROR)
|
||||
optp.add_option('-v','--verbose', help='set logging to COMM',
|
||||
action='store_const',
|
||||
dest='loglevel',
|
||||
const=5,
|
||||
default=logging.ERROR)
|
||||
|
||||
# JID and password options.
|
||||
optp.add_option("-j", "--jid", dest="jid",
|
||||
help="JID to use")
|
||||
optp.add_option("-p", "--password", dest="password",
|
||||
help="password to use")
|
||||
optp.add_option("-f", "--file", dest="filepath",
|
||||
help="path to the avatar file")
|
||||
opts,args = optp.parse_args()
|
||||
|
||||
# Setup logging.
|
||||
logging.basicConfig(level=opts.loglevel,
|
||||
format='%(levelname)-8s %(message)s')
|
||||
|
||||
if opts.jid is None:
|
||||
opts.jid = raw_input("Username: ")
|
||||
if opts.password is None:
|
||||
opts.password = getpass.getpass("Password: ")
|
||||
if opts.filepath is None:
|
||||
opts.filepath = raw_input("Avatar file location: ")
|
||||
|
||||
xmpp = AvatarSetter(opts.jid, opts.password, opts.filepath)
|
||||
xmpp.register_plugin('xep_0054')
|
||||
xmpp.register_plugin('xep_0153')
|
||||
xmpp.register_plugin('xep_0084')
|
||||
|
||||
# If you are working with an OpenFire server, you may need
|
||||
# to adjust the SSL version used:
|
||||
# xmpp.ssl_version = ssl.PROTOCOL_SSLv3
|
||||
|
||||
# If you want to verify the SSL certificates offered by a server:
|
||||
# xmpp.ca_certs = "path/to/ca/cert"
|
||||
|
||||
# Connect to the XMPP server and start processing XMPP stanzas.
|
||||
if xmpp.connect():
|
||||
# If you do not have the dnspython library installed, you will need
|
||||
# to manually specify the name of the server if it does not match
|
||||
# the one in the JID. For example, to use Google Talk you would
|
||||
# need to use:
|
||||
#
|
||||
# if xmpp.connect(('talk.google.com', 5222)):
|
||||
# ...
|
||||
xmpp.process(block=True)
|
||||
else:
|
||||
print("Unable to connect.")
|
3
setup.py
3
setup.py
@@ -49,6 +49,7 @@ packages = [ 'sleekxmpp',
|
||||
'sleekxmpp/stanza',
|
||||
'sleekxmpp/test',
|
||||
'sleekxmpp/roster',
|
||||
'sleekxmpp/util',
|
||||
'sleekxmpp/xmlstream',
|
||||
'sleekxmpp/xmlstream/matcher',
|
||||
'sleekxmpp/xmlstream/handler',
|
||||
@@ -81,10 +82,12 @@ packages = [ 'sleekxmpp',
|
||||
'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',
|
||||
|
@@ -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
|
||||
@@ -69,7 +70,15 @@ class BaseXMPP(XMLStream):
|
||||
|
||||
#: The JabberID (JID) used by this connection.
|
||||
self.boundjid = JID(jid)
|
||||
|
||||
self._expected_server_name = self.boundjid.host
|
||||
self._redirect_attempts = 0
|
||||
|
||||
#: The maximum number of consecutive see-other-host
|
||||
#: redirections that will be followed before quitting.
|
||||
self.max_redirects = 5
|
||||
|
||||
self.session_bind_event = threading.Event()
|
||||
|
||||
#: A dictionary mapping plugin names to plugins.
|
||||
self.plugin = PluginManager(self)
|
||||
@@ -87,13 +96,13 @@ 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
|
||||
@@ -134,11 +143,14 @@ class BaseXMPP(XMLStream):
|
||||
Callback('Presence',
|
||||
MatchXPath("{%s}presence" % self.default_ns),
|
||||
self._handle_presence))
|
||||
|
||||
self.register_handler(
|
||||
Callback('Stream Error',
|
||||
MatchXPath("{%s}error" % self.stream_ns),
|
||||
self._handle_stream_error))
|
||||
|
||||
self.add_event_handler('session_start',
|
||||
self._handle_session_start)
|
||||
self.add_event_handler('disconnected',
|
||||
self._handle_disconnected)
|
||||
self.add_event_handler('presence_available',
|
||||
@@ -586,7 +598,7 @@ class BaseXMPP(XMLStream):
|
||||
|
||||
@resource.setter
|
||||
def resource(self, value):
|
||||
log.warning("fulljid property deprecated. Use boundjid.full")
|
||||
log.warning("fulljid property deprecated. Use boundjid.resource")
|
||||
self.boundjid.resource = value
|
||||
|
||||
@property
|
||||
@@ -651,30 +663,61 @@ class BaseXMPP(XMLStream):
|
||||
def getjidbare(self, fulljid):
|
||||
return fulljid.split('/', 1)[0]
|
||||
|
||||
def _handle_session_start(self, event):
|
||||
"""Reset redirection attempt count."""
|
||||
self._redirect_attempts = 0
|
||||
|
||||
def _handle_disconnected(self, event):
|
||||
"""When disconnected, reset the roster"""
|
||||
self.roster.reset()
|
||||
self.session_bind_event.clear()
|
||||
|
||||
def _handle_stream_error(self, error):
|
||||
self.event('stream_error', error)
|
||||
|
||||
if error['condition'] == 'see-other-host':
|
||||
other_host = error['see_other_host']
|
||||
if not other_host:
|
||||
log.warning("No other host specified.")
|
||||
return
|
||||
|
||||
if self._redirect_attempts > self.max_redirects:
|
||||
log.error("Exceeded maximum number of redirection attempts.")
|
||||
return
|
||||
|
||||
self._redirect_attempts += 1
|
||||
|
||||
host = other_host
|
||||
port = 5222
|
||||
|
||||
if '[' in other_host and ']' in other_host:
|
||||
host = other_host.split(']')[0][1:]
|
||||
elif ':' in other_host:
|
||||
host = other_host.split(':')[0]
|
||||
|
||||
port_sec = other_host.split(']')[-1]
|
||||
if ':' in port_sec:
|
||||
port = int(port_sec.split(':')[1])
|
||||
|
||||
self.address = (host, port)
|
||||
self.default_domain = host
|
||||
self.dns_records = None
|
||||
self.reconnect_delay = None
|
||||
self.reconnect()
|
||||
|
||||
def _handle_message(self, msg):
|
||||
"""Process incoming message stanzas."""
|
||||
if not self.is_component and not msg['to'].bare:
|
||||
msg['to'] = self.boundjid
|
||||
self.event('message', msg)
|
||||
|
||||
def _handle_available(self, presence):
|
||||
pto = presence['to'].bare
|
||||
pfrom = presence['from'].bare
|
||||
self.roster[pto][pfrom].handle_available(presence)
|
||||
def _handle_available(self, pres):
|
||||
self.roster[pres['to']][pres['from']].handle_available(pres)
|
||||
|
||||
def _handle_unavailable(self, presence):
|
||||
pto = presence['to'].bare
|
||||
pfrom = presence['from'].bare
|
||||
self.roster[pto][pfrom].handle_unavailable(presence)
|
||||
def _handle_unavailable(self, pres):
|
||||
self.roster[pres['to']][pres['from']].handle_unavailable(pres)
|
||||
|
||||
def _handle_new_subscription(self, stanza):
|
||||
def _handle_new_subscription(self, pres):
|
||||
"""Attempt to automatically handle subscription requests.
|
||||
|
||||
Subscriptions will be approved if the request is from
|
||||
@@ -686,8 +729,8 @@ class BaseXMPP(XMLStream):
|
||||
If a subscription is accepted, a request for a mutual
|
||||
subscription will be sent if :attr:`auto_subscribe` is ``True``.
|
||||
"""
|
||||
roster = self.roster[stanza['to'].bare]
|
||||
item = self.roster[stanza['to'].bare][stanza['from'].bare]
|
||||
roster = self.roster[pres['to']]
|
||||
item = self.roster[pres['to']][pres['from']]
|
||||
if item['whitelisted']:
|
||||
item.authorize()
|
||||
elif roster.auto_authorize:
|
||||
@@ -697,30 +740,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.
|
||||
|
@@ -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)
|
||||
|
||||
@@ -270,8 +285,9 @@ 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]
|
||||
items = iq['roster']['items']
|
||||
for jid in items:
|
||||
item = items[jid]
|
||||
roster[jid]['name'] = item['name']
|
||||
roster[jid]['groups'] = item['groups']
|
||||
roster[jid]['from'] = item['subscription'] in ['from', 'both']
|
||||
|
@@ -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)
|
||||
|
@@ -51,6 +51,7 @@ class FeatureBind(BasePlugin):
|
||||
self.xmpp.set_jid(response['bind']['jid'])
|
||||
self.xmpp.bound = True
|
||||
self.xmpp.event('session_bind', self.xmpp.boundjid, direct=True)
|
||||
self.xmpp.session_bind_event.set()
|
||||
|
||||
self.xmpp.features.add('bind')
|
||||
|
||||
|
@@ -29,10 +29,13 @@ class FeatureMechanisms(BasePlugin):
|
||||
description = 'RFC 6120: Stream Feature: SASL'
|
||||
dependencies = set()
|
||||
stanza = stanza
|
||||
default_config = {
|
||||
'use_mech': None,
|
||||
'sasl_callback': None,
|
||||
'order': 100
|
||||
}
|
||||
|
||||
def plugin_init(self):
|
||||
self.use_mech = self.config.get('use_mech', None)
|
||||
|
||||
if not self.use_mech and not self.xmpp.boundjid.user:
|
||||
self.use_mech = 'ANONYMOUS'
|
||||
|
||||
@@ -53,15 +56,14 @@ class FeatureMechanisms(BasePlugin):
|
||||
values[value] = creds[value]
|
||||
mech.fulfill(values)
|
||||
|
||||
sasl_callback = self.config.get('sasl_callback', None)
|
||||
if sasl_callback is None:
|
||||
sasl_callback = basic_callback
|
||||
if self.sasl_callback is None:
|
||||
self.sasl_callback = basic_callback
|
||||
|
||||
self.mech = None
|
||||
self.sasl = suelta.SASL(self.xmpp.boundjid.domain, 'xmpp',
|
||||
username=self.xmpp.boundjid.user,
|
||||
sec_query=suelta.sec_query_allow,
|
||||
request_values=sasl_callback,
|
||||
request_values=self.sasl_callback,
|
||||
tls_active=tls_active,
|
||||
mech=self.use_mech)
|
||||
|
||||
@@ -95,7 +97,7 @@ class FeatureMechanisms(BasePlugin):
|
||||
self.xmpp.register_feature('mechanisms',
|
||||
self._handle_sasl_auth,
|
||||
restart=True,
|
||||
order=self.config.get('order', 100))
|
||||
order=self.order)
|
||||
|
||||
def _handle_sasl_auth(self, features):
|
||||
"""
|
||||
|
543
sleekxmpp/jid.py
Normal file
543
sleekxmpp/jid.py
Normal file
@@ -0,0 +1,543 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
sleekxmpp.jid
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
This module allows for working with Jabber IDs (JIDs).
|
||||
|
||||
Part of SleekXMPP: The Sleek XMPP Library
|
||||
|
||||
:copyright: (c) 2011 Nathanael C. Fritz
|
||||
:license: MIT, see LICENSE for more details
|
||||
"""
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import re
|
||||
import socket
|
||||
import stringprep
|
||||
import encodings.idna
|
||||
|
||||
from sleekxmpp.util import stringprep_profiles
|
||||
|
||||
#: These characters are not allowed to appear in a JID.
|
||||
ILLEGAL_CHARS = '\x00\x01\x02\x03\x04\x05\x06\x07\x08\t\n\x0b\x0c\r' + \
|
||||
'\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19' + \
|
||||
'\x1a\x1b\x1c\x1d\x1e\x1f' + \
|
||||
' !"#$%&\'()*+,./:;<=>?@[\\]^_`{|}~\x7f'
|
||||
|
||||
#: The basic regex pattern that a JID must match in order to determine
|
||||
#: the local, domain, and resource parts. This regex does NOT do any
|
||||
#: validation, which requires application of nodeprep, resourceprep, etc.
|
||||
JID_PATTERN = re.compile(
|
||||
"^(?:([^\"&'/:<>@]{1,1023})@)?([^/@]{1,1023})(?:/(.{1,1023}))?$"
|
||||
)
|
||||
|
||||
#: The set of escape sequences for the characters not allowed by nodeprep.
|
||||
JID_ESCAPE_SEQUENCES = set(['\\20', '\\22', '\\26', '\\27', '\\2f',
|
||||
'\\3a', '\\3c', '\\3e', '\\40', '\\5c'])
|
||||
|
||||
#: A mapping of unallowed characters to their escape sequences. An escape
|
||||
#: sequence for '\' is also included since it must also be escaped in
|
||||
#: certain situations.
|
||||
JID_ESCAPE_TRANSFORMATIONS = {' ': '\\20',
|
||||
'"': '\\22',
|
||||
'&': '\\26',
|
||||
"'": '\\27',
|
||||
'/': '\\2f',
|
||||
':': '\\3a',
|
||||
'<': '\\3c',
|
||||
'>': '\\3e',
|
||||
'@': '\\40',
|
||||
'\\': '\\5c'}
|
||||
|
||||
#: The reverse mapping of escape sequences to their original forms.
|
||||
JID_UNESCAPE_TRANSFORMATIONS = {'\\20': ' ',
|
||||
'\\22': '"',
|
||||
'\\26': '&',
|
||||
'\\27': "'",
|
||||
'\\2f': '/',
|
||||
'\\3a': ':',
|
||||
'\\3c': '<',
|
||||
'\\3e': '>',
|
||||
'\\40': '@',
|
||||
'\\5c': '\\'}
|
||||
|
||||
|
||||
# pylint: disable=c0103
|
||||
#: The nodeprep profile of stringprep used to validate the local,
|
||||
#: or username, portion of a JID.
|
||||
nodeprep = stringprep_profiles.create(
|
||||
nfkc=True,
|
||||
bidi=True,
|
||||
mappings=[
|
||||
stringprep_profiles.b1_mapping,
|
||||
stringprep_profiles.c12_mapping],
|
||||
prohibited=[
|
||||
stringprep.in_table_c11,
|
||||
stringprep.in_table_c12,
|
||||
stringprep.in_table_c21,
|
||||
stringprep.in_table_c22,
|
||||
stringprep.in_table_c3,
|
||||
stringprep.in_table_c4,
|
||||
stringprep.in_table_c5,
|
||||
stringprep.in_table_c6,
|
||||
stringprep.in_table_c7,
|
||||
stringprep.in_table_c8,
|
||||
stringprep.in_table_c9,
|
||||
lambda c: c in ' \'"&/:<>@'],
|
||||
unassigned=[stringprep.in_table_a1])
|
||||
|
||||
# pylint: disable=c0103
|
||||
#: The resourceprep profile of stringprep, which is used to validate
|
||||
#: the resource portion of a JID.
|
||||
resourceprep = stringprep_profiles.create(
|
||||
nfkc=True,
|
||||
bidi=True,
|
||||
mappings=[stringprep_profiles.b1_mapping],
|
||||
prohibited=[
|
||||
stringprep.in_table_c12,
|
||||
stringprep.in_table_c21,
|
||||
stringprep.in_table_c22,
|
||||
stringprep.in_table_c3,
|
||||
stringprep.in_table_c4,
|
||||
stringprep.in_table_c5,
|
||||
stringprep.in_table_c6,
|
||||
stringprep.in_table_c7,
|
||||
stringprep.in_table_c8,
|
||||
stringprep.in_table_c9],
|
||||
unassigned=[stringprep.in_table_a1])
|
||||
|
||||
|
||||
def _parse_jid(data):
|
||||
"""
|
||||
Parse string data into the node, domain, and resource
|
||||
components of a JID, if possible.
|
||||
|
||||
:param string data: A string that is potentially a JID.
|
||||
|
||||
:raises InvalidJID:
|
||||
|
||||
:returns: tuple of the validated local, domain, and resource strings
|
||||
"""
|
||||
match = JID_PATTERN.match(data)
|
||||
if not match:
|
||||
raise InvalidJID('JID could not be parsed')
|
||||
|
||||
(node, domain, resource) = match.groups()
|
||||
|
||||
node = _validate_node(node)
|
||||
domain = _validate_domain(domain)
|
||||
resource = _validate_resource(resource)
|
||||
|
||||
return node, domain, resource
|
||||
|
||||
|
||||
def _validate_node(node):
|
||||
"""Validate the local, or username, portion of a JID.
|
||||
|
||||
:raises InvalidJID:
|
||||
|
||||
:returns: The local portion of a JID, as validated by nodeprep.
|
||||
"""
|
||||
try:
|
||||
if node is not None:
|
||||
node = nodeprep(node)
|
||||
|
||||
if not node:
|
||||
raise InvalidJID('Localpart must not be 0 bytes')
|
||||
if len(node) > 1023:
|
||||
raise InvalidJID('Localpart must be less than 1024 bytes')
|
||||
return node
|
||||
except stringprep_profiles.StringPrepError:
|
||||
raise InvalidJID('Invalid local part')
|
||||
|
||||
|
||||
def _validate_domain(domain):
|
||||
"""Validate the domain portion of a JID.
|
||||
|
||||
IP literal addresses are left as-is, if valid. Domain names
|
||||
are stripped of any trailing label separators (`.`), and are
|
||||
checked with the nameprep profile of stringprep. If the given
|
||||
domain is actually a punyencoded version of a domain name, it
|
||||
is converted back into its original Unicode form. Domains must
|
||||
also not start or end with a dash (`-`).
|
||||
|
||||
:raises InvalidJID:
|
||||
|
||||
:returns: The validated domain name
|
||||
"""
|
||||
ip_addr = False
|
||||
|
||||
# First, check if this is an IPv4 address
|
||||
try:
|
||||
socket.inet_aton(domain)
|
||||
ip_addr = True
|
||||
except socket.error:
|
||||
pass
|
||||
|
||||
# Check if this is an IPv6 address
|
||||
if not ip_addr and hasattr(socket, 'inet_pton'):
|
||||
try:
|
||||
socket.inet_pton(socket.AF_INET6, domain.strip('[]'))
|
||||
domain = '[%s]' % domain.strip('[]')
|
||||
ip_addr = True
|
||||
except socket.error:
|
||||
pass
|
||||
|
||||
if not ip_addr:
|
||||
# This is a domain name, which must be checked further
|
||||
|
||||
if domain and domain[-1] == '.':
|
||||
domain = domain[:-1]
|
||||
|
||||
domain_parts = []
|
||||
for label in domain.split('.'):
|
||||
try:
|
||||
label = encodings.idna.nameprep(label)
|
||||
encodings.idna.ToASCII(label)
|
||||
pass_nameprep = True
|
||||
except UnicodeError:
|
||||
pass_nameprep = False
|
||||
|
||||
if not pass_nameprep:
|
||||
raise InvalidJID('Could not encode domain as ASCII')
|
||||
|
||||
if label.startswith('xn--'):
|
||||
label = encodings.idna.ToUnicode(label)
|
||||
|
||||
for char in label:
|
||||
if char in ILLEGAL_CHARS:
|
||||
raise InvalidJID('Domain contains illegar characters')
|
||||
|
||||
if '-' in (label[0], label[-1]):
|
||||
raise InvalidJID('Domain started or ended with -')
|
||||
|
||||
domain_parts.append(label)
|
||||
domain = '.'.join(domain_parts)
|
||||
|
||||
if not domain:
|
||||
raise InvalidJID('Domain must not be 0 bytes')
|
||||
if len(domain) > 1023:
|
||||
raise InvalidJID('Domain must be less than 1024 bytes')
|
||||
|
||||
return domain
|
||||
|
||||
|
||||
def _validate_resource(resource):
|
||||
"""Validate the resource portion of a JID.
|
||||
|
||||
:raises InvalidJID:
|
||||
|
||||
:returns: The local portion of a JID, as validated by resourceprep.
|
||||
"""
|
||||
try:
|
||||
if resource is not None:
|
||||
resource = resourceprep(resource)
|
||||
|
||||
if not resource:
|
||||
raise InvalidJID('Resource must not be 0 bytes')
|
||||
if len(resource) > 1023:
|
||||
raise InvalidJID('Resource must be less than 1024 bytes')
|
||||
return resource
|
||||
except stringprep_profiles.StringPrepError:
|
||||
raise InvalidJID('Invalid resource')
|
||||
|
||||
|
||||
def _escape_node(node):
|
||||
"""Escape the local portion of a JID."""
|
||||
result = []
|
||||
|
||||
for i, char in enumerate(node):
|
||||
if char == '\\':
|
||||
if ''.join((node[i:i+3])) in JID_ESCAPE_SEQUENCES:
|
||||
result.append('\\5c')
|
||||
continue
|
||||
result.append(char)
|
||||
|
||||
for i, char in enumerate(result):
|
||||
if char != '\\':
|
||||
result[i] = JID_ESCAPE_TRANSFORMATIONS.get(char, char)
|
||||
|
||||
escaped = ''.join(result)
|
||||
|
||||
if escaped.startswith('\\20') or escaped.endswith('\\20'):
|
||||
raise InvalidJID('Escaped local part starts or ends with "\\20"')
|
||||
|
||||
_validate_node(escaped)
|
||||
|
||||
return escaped
|
||||
|
||||
|
||||
def _unescape_node(node):
|
||||
"""Unescape a local portion of a JID.
|
||||
|
||||
.. note::
|
||||
The unescaped local portion is meant ONLY for presentation,
|
||||
and should not be used for other purposes.
|
||||
"""
|
||||
unescaped = []
|
||||
seq = ''
|
||||
for i, char in enumerate(node):
|
||||
if char == '\\':
|
||||
seq = node[i:i+3]
|
||||
if seq not in JID_ESCAPE_SEQUENCES:
|
||||
seq = ''
|
||||
if seq:
|
||||
if len(seq) == 3:
|
||||
unescaped.append(JID_UNESCAPE_TRANSFORMATIONS.get(seq, char))
|
||||
|
||||
# Pop character off the escape sequence, and ignore it
|
||||
seq = seq[1:]
|
||||
else:
|
||||
unescaped.append(char)
|
||||
unescaped = ''.join(unescaped)
|
||||
|
||||
return unescaped
|
||||
|
||||
|
||||
def _format_jid(local=None, domain=None, resource=None):
|
||||
"""Format the given JID components into a full or bare JID.
|
||||
|
||||
:param string local: Optional. The local portion of the JID.
|
||||
:param string domain: Required. The domain name portion of the JID.
|
||||
:param strin resource: Optional. The resource portion of the JID.
|
||||
|
||||
:return: A full or bare JID string.
|
||||
"""
|
||||
result = []
|
||||
if local:
|
||||
result.append(local)
|
||||
result.append('@')
|
||||
if domain:
|
||||
result.append(domain)
|
||||
if resource:
|
||||
result.append('/')
|
||||
result.append(resource)
|
||||
return ''.join(result)
|
||||
|
||||
|
||||
class InvalidJID(ValueError):
|
||||
"""
|
||||
Raised when attempting to create a JID that does not pass validation.
|
||||
|
||||
It can also be raised if modifying an existing JID in such a way as
|
||||
to make it invalid, such trying to remove the domain from an existing
|
||||
full JID while the local and resource portions still exist.
|
||||
"""
|
||||
|
||||
# pylint: disable=R0903
|
||||
class UnescapedJID(object):
|
||||
|
||||
"""
|
||||
.. versionadded:: 1.1.10
|
||||
"""
|
||||
|
||||
def __init__(self, local, domain, resource):
|
||||
self._jid = (local, domain, resource)
|
||||
|
||||
# pylint: disable=R0911
|
||||
def __getattr__(self, name):
|
||||
"""Retrieve the given JID component.
|
||||
|
||||
:param name: one of: user, server, domain, resource,
|
||||
full, or bare.
|
||||
"""
|
||||
if name == 'resource':
|
||||
return self._jid[2] or ''
|
||||
elif name in ('user', 'username', 'local', 'node'):
|
||||
return self._jid[0] or ''
|
||||
elif name in ('server', 'domain', 'host'):
|
||||
return self._jid[1] or ''
|
||||
elif name in ('full', 'jid'):
|
||||
return _format_jid(*self._jid)
|
||||
elif name == 'bare':
|
||||
return _format_jid(self._jid[0], self._jid[1])
|
||||
elif name == '_jid':
|
||||
return getattr(super(JID, self), '_jid')
|
||||
else:
|
||||
return None
|
||||
|
||||
def __str__(self):
|
||||
"""Use the full JID as the string value."""
|
||||
return _format_jid(*self._jid)
|
||||
|
||||
def __repr__(self):
|
||||
"""Use the full JID as the representation."""
|
||||
return self.__str__()
|
||||
|
||||
|
||||
class JID(object):
|
||||
|
||||
"""
|
||||
A representation of a Jabber ID, or JID.
|
||||
|
||||
Each JID may have three components: a user, a domain, and an optional
|
||||
resource. For example: user@domain/resource
|
||||
|
||||
When a resource is not used, the JID is called a bare JID.
|
||||
The JID is a full JID otherwise.
|
||||
|
||||
**JID Properties:**
|
||||
:jid: Alias for ``full``.
|
||||
:full: The string value of the full JID.
|
||||
:bare: The string value of the bare JID.
|
||||
:user: The username portion of the JID.
|
||||
:username: Alias for ``user``.
|
||||
:local: Alias for ``user``.
|
||||
:node: Alias for ``user``.
|
||||
:domain: The domain name portion of the JID.
|
||||
:server: Alias for ``domain``.
|
||||
:host: Alias for ``domain``.
|
||||
:resource: The resource portion of the JID.
|
||||
|
||||
:param string jid:
|
||||
A string of the form ``'[user@]domain[/resource]'``.
|
||||
:param string local:
|
||||
Optional. Specify the local, or username, portion
|
||||
of the JID. If provided, it will override the local
|
||||
value provided by the `jid` parameter. The given
|
||||
local value will also be escaped if necessary.
|
||||
:param string domain:
|
||||
Optional. Specify the domain of the JID. If
|
||||
provided, it will override the domain given by
|
||||
the `jid` parameter.
|
||||
:param string resource:
|
||||
Optional. Specify the resource value of the JID.
|
||||
If provided, it will override the domain given
|
||||
by the `jid` parameter.
|
||||
|
||||
:raises InvalidJID:
|
||||
"""
|
||||
|
||||
# pylint: disable=W0212
|
||||
def __init__(self, jid=None, **kwargs):
|
||||
self._jid = (None, None, None)
|
||||
|
||||
if jid is None or jid == '':
|
||||
jid = (None, None, None)
|
||||
elif not isinstance(jid, JID):
|
||||
jid = _parse_jid(jid)
|
||||
else:
|
||||
jid = jid._jid
|
||||
|
||||
local, domain, resource = jid
|
||||
|
||||
local = kwargs.get('local', local)
|
||||
domain = kwargs.get('domain', domain)
|
||||
resource = kwargs.get('resource', resource)
|
||||
|
||||
if 'local' in kwargs:
|
||||
local = _escape_node(local)
|
||||
if 'domain' in kwargs:
|
||||
domain = _validate_domain(domain)
|
||||
if 'resource' in kwargs:
|
||||
resource = _validate_resource(resource)
|
||||
|
||||
self._jid = (local, domain, resource)
|
||||
|
||||
def unescape(self):
|
||||
"""Return an unescaped JID object.
|
||||
|
||||
Using an unescaped JID is preferred for displaying JIDs
|
||||
to humans, and they should NOT be used for any other
|
||||
purposes than for presentation.
|
||||
|
||||
:return: :class:`UnescapedJID`
|
||||
|
||||
.. versionadded:: 1.1.10
|
||||
"""
|
||||
return UnescapedJID(_unescape_node(self._jid[0]),
|
||||
self._jid[1],
|
||||
self._jid[2])
|
||||
|
||||
def regenerate(self):
|
||||
"""No-op
|
||||
|
||||
.. deprecated:: 1.1.10
|
||||
"""
|
||||
pass
|
||||
|
||||
def reset(self, data):
|
||||
"""Start fresh from a new JID string.
|
||||
|
||||
:param string data: A string of the form ``'[user@]domain[/resource]'``.
|
||||
|
||||
.. deprecated:: 1.1.10
|
||||
"""
|
||||
self._jid = JID(data)._jid
|
||||
|
||||
# pylint: disable=R0911
|
||||
def __getattr__(self, name):
|
||||
"""Retrieve the given JID component.
|
||||
|
||||
:param name: one of: user, server, domain, resource,
|
||||
full, or bare.
|
||||
"""
|
||||
if name == 'resource':
|
||||
return self._jid[2] or ''
|
||||
elif name in ('user', 'username', 'local', 'node'):
|
||||
return self._jid[0] or ''
|
||||
elif name in ('server', 'domain', 'host'):
|
||||
return self._jid[1] or ''
|
||||
elif name in ('full', 'jid'):
|
||||
return _format_jid(*self._jid)
|
||||
elif name == 'bare':
|
||||
return _format_jid(self._jid[0], self._jid[1])
|
||||
elif name == '_jid':
|
||||
return getattr(super(JID, self), '_jid')
|
||||
else:
|
||||
return None
|
||||
|
||||
# pylint: disable=W0212
|
||||
def __setattr__(self, name, value):
|
||||
"""Update the given JID component.
|
||||
|
||||
:param name: one of: ``user``, ``username``, ``local``,
|
||||
``node``, ``server``, ``domain``, ``host``,
|
||||
``resource``, ``full``, ``jid``, or ``bare``.
|
||||
:param value: The new string value of the JID component.
|
||||
"""
|
||||
if name == 'resource':
|
||||
self._jid = JID(self, resource=value)._jid
|
||||
elif name in ('user', 'username', 'local', 'node'):
|
||||
self._jid = JID(self, local=value)._jid
|
||||
elif name in ('server', 'domain', 'host'):
|
||||
self._jid = JID(self, domain=value)._jid
|
||||
elif name in ('full', 'jid'):
|
||||
self._jid = JID(value)._jid
|
||||
elif name == 'bare':
|
||||
parsed = JID(value)._jid
|
||||
self._jid = (parsed[0], parsed[1], self._jid[2])
|
||||
elif name == '_jid':
|
||||
super(JID, self).__setattr__('_jid', value)
|
||||
|
||||
def __str__(self):
|
||||
"""Use the full JID as the string value."""
|
||||
return _format_jid(*self._jid)
|
||||
|
||||
def __repr__(self):
|
||||
"""Use the full JID as the representation."""
|
||||
return self.__str__()
|
||||
|
||||
# pylint: disable=W0212
|
||||
def __eq__(self, other):
|
||||
"""Two JIDs are equal if they have the same full JID value."""
|
||||
if isinstance(other, UnescapedJID):
|
||||
return False
|
||||
|
||||
other = JID(other)
|
||||
return self._jid == other._jid
|
||||
|
||||
# pylint: disable=W0212
|
||||
def __ne__(self, other):
|
||||
"""Two JIDs are considered unequal if they are not equal."""
|
||||
return not self == other
|
||||
|
||||
def __hash__(self):
|
||||
"""Hash a JID based on the string version of its full JID."""
|
||||
return hash(self.__str__())
|
||||
|
||||
def __copy__(self):
|
||||
"""Generate a duplicate JID."""
|
||||
return JID(self)
|
@@ -36,16 +36,20 @@ __all__ = [
|
||||
'xep_0085', # Chat State Notifications
|
||||
'xep_0086', # Legacy Error Codes
|
||||
'xep_0092', # Software Version
|
||||
'xep_0106', # JID Escaping
|
||||
'xep_0107', # User Mood
|
||||
'xep_0108', # User Activity
|
||||
'xep_0115', # Entity Capabilities
|
||||
'xep_0118', # User Tune
|
||||
'xep_0128', # Extended Service Discovery
|
||||
'xep_0131', # Standard Headers and Internet Metadata
|
||||
'xep_0133', # Service Administration
|
||||
'xep_0153', # vCard-Based Avatars
|
||||
'xep_0163', # Personal Eventing Protocol
|
||||
'xep_0172', # User Nickname
|
||||
'xep_0184', # Message Receipts
|
||||
'xep_0186', # Invisible Command
|
||||
'xep_0191', # Blocking Command
|
||||
'xep_0198', # Stream Management
|
||||
'xep_0199', # Ping
|
||||
'xep_0202', # Entity Time
|
||||
@@ -56,5 +60,8 @@ __all__ = [
|
||||
'xep_0224', # Attention
|
||||
'xep_0231', # Bits of Binary
|
||||
'xep_0249', # Direct MUC Invitations
|
||||
'xep_0256', # Last Activity in Presence
|
||||
'xep_0258', # Security Labels in XMPP
|
||||
'xep_0270', # XMPP Compliance Suites 2010
|
||||
'xep_0302', # XMPP Compliance Suites 2012
|
||||
]
|
||||
|
@@ -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=''):
|
||||
|
@@ -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)
|
||||
|
||||
|
@@ -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',
|
||||
@@ -622,11 +623,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 +646,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 +664,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)
|
||||
|
||||
|
@@ -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,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()
|
||||
|
@@ -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())
|
||||
|
||||
|
@@ -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()
|
||||
|
||||
|
@@ -47,6 +47,7 @@ class ResultIterator():
|
||||
self.start = start
|
||||
self.interface = interface
|
||||
self.reverse = reverse
|
||||
self._stop = False
|
||||
|
||||
def __iter__(self):
|
||||
return self
|
||||
@@ -62,6 +63,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)
|
||||
@@ -84,7 +87,7 @@ class ResultIterator():
|
||||
first = int(r[self.interface]['rsm']['first_index'])
|
||||
num_items = len(r[self.interface]['substanzas'])
|
||||
if first + num_items == count:
|
||||
raise StopIteration
|
||||
self._stop = True
|
||||
|
||||
if self.reverse:
|
||||
self.start = r[self.interface]['rsm']['first']
|
||||
@@ -111,10 +114,15 @@ 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 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):
|
||||
"""
|
||||
Create a new result set iterator for a given stanza query.
|
||||
|
@@ -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']
|
||||
|
@@ -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
|
||||
|
@@ -34,16 +34,22 @@ 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)
|
||||
|
||||
register_stanza_plugin(Iq, stanza.IqAuth)
|
||||
register_stanza_plugin(StreamFeatures, stanza.AuthFeature)
|
||||
|
||||
def plugin_end(self):
|
||||
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']:
|
||||
|
@@ -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,
|
||||
@@ -67,12 +74,12 @@ class XEP_0084(BasePlugin):
|
||||
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,
|
||||
|
@@ -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,6 +43,10 @@ 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):
|
||||
|
@@ -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)
|
||||
|
@@ -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):
|
||||
|
@@ -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,7 +8,9 @@
|
||||
|
||||
import hashlib
|
||||
import logging
|
||||
import threading
|
||||
|
||||
from sleekxmpp import JID
|
||||
from sleekxmpp.stanza import Presence
|
||||
from sleekxmpp.xmlstream import register_stanza_plugin
|
||||
from sleekxmpp.xmlstream.matcher import StanzaPath
|
||||
@@ -30,11 +32,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 +49,82 @@ 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)
|
||||
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)
|
||||
if own_jid:
|
||||
self.xmpp.roster[jid].send_last_presence()
|
||||
|
||||
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):
|
||||
|
15
sleekxmpp/plugins/xep_0191/__init__.py
Normal file
15
sleekxmpp/plugins/xep_0191/__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_0191.stanza import Block, Unblock, BlockList
|
||||
from sleekxmpp.plugins.xep_0191.blocking import XEP_0191
|
||||
|
||||
|
||||
register_plugin(XEP_0191)
|
83
sleekxmpp/plugins/xep_0191/blocking.py
Normal file
83
sleekxmpp/plugins/xep_0191/blocking.py
Normal file
@@ -0,0 +1,83 @@
|
||||
"""
|
||||
SleekXMPP: The Sleek XMPP Library
|
||||
Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
|
||||
This file is part of SleekXMPP.
|
||||
|
||||
See the file LICENSE for copying permission.
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from sleekxmpp import Iq
|
||||
from sleekxmpp.plugins import BasePlugin
|
||||
from sleekxmpp.xmlstream.handler import Callback
|
||||
from sleekxmpp.xmlstream.matcher import StanzaPath
|
||||
from sleekxmpp.xmlstream import register_stanza_plugin, JID
|
||||
from sleekxmpp.plugins.xep_0191 import stanza, Block, Unblock, BlockList
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class XEP_0191(BasePlugin):
|
||||
|
||||
name = 'xep_0191'
|
||||
description = 'XEP-0191: Blocking Command'
|
||||
dependencies = set(['xep_0030'])
|
||||
stanza = stanza
|
||||
|
||||
def plugin_init(self):
|
||||
register_stanza_plugin(Iq, BlockList)
|
||||
register_stanza_plugin(Iq, Block)
|
||||
register_stanza_plugin(Iq, Unblock)
|
||||
|
||||
self.xmpp.register_handler(
|
||||
Callback('Blocked Contact',
|
||||
StanzaPath('iq@type=set/block'),
|
||||
self._handle_blocked))
|
||||
|
||||
self.xmpp.register_handler(
|
||||
Callback('Unblocked Contact',
|
||||
StanzaPath('iq@type=set/unblock'),
|
||||
self._handle_unblocked))
|
||||
|
||||
def plugin_end(self):
|
||||
self.xmpp.remove_handler('Blocked Contact')
|
||||
self.xmpp.remove_handler('Unblocked Contact')
|
||||
|
||||
def get_blocked(self, ifrom=None, block=True, timeout=None, callback=None):
|
||||
iq = self.xmpp.Iq()
|
||||
iq['type'] = 'get'
|
||||
iq['from'] = 'ifrom'
|
||||
iq.enable('blocklist')
|
||||
return iq.send(block=block, timeout=timeout, callback=callback)
|
||||
|
||||
def block(self, jids, ifrom=None, block=True, timeout=None, callback=None):
|
||||
iq = self.xmpp.Iq()
|
||||
iq['type'] = 'set'
|
||||
iq['from'] = ifrom
|
||||
|
||||
if not isinstance(jids, (set, list)):
|
||||
jids = [jids]
|
||||
|
||||
iq['block']['items'] = jids
|
||||
return iq.send(block=block, timeout=timeout, callback=callback)
|
||||
|
||||
def unblock(self, jids=None, ifrom=None, block=True, timeout=None, callback=None):
|
||||
iq = self.xmpp.Iq()
|
||||
iq['type'] = 'set'
|
||||
iq['from'] = ifrom
|
||||
|
||||
if jids is None:
|
||||
jids = []
|
||||
if not isinstance(jids, (set, list)):
|
||||
jids = [jids]
|
||||
|
||||
iq['unblock']['items'] = jids
|
||||
return iq.send(block=block, timeout=timeout, callback=callback)
|
||||
|
||||
def _handle_blocked(self, iq):
|
||||
self.xmpp.event('blocked', iq)
|
||||
|
||||
def _handle_unblocked(self, iq):
|
||||
self.xmpp.event('unblocked', iq)
|
50
sleekxmpp/plugins/xep_0191/stanza.py
Normal file
50
sleekxmpp/plugins/xep_0191/stanza.py
Normal file
@@ -0,0 +1,50 @@
|
||||
"""
|
||||
SleekXMPP: The Sleek XMPP Library
|
||||
Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
|
||||
This file is part of SleekXMPP.
|
||||
|
||||
See the file LICENSE for copying permission.
|
||||
"""
|
||||
|
||||
from sleekxmpp.xmlstream import ET, ElementBase, JID
|
||||
|
||||
|
||||
class BlockList(ElementBase):
|
||||
name = 'blocklist'
|
||||
namespace = 'urn:xmpp:blocking'
|
||||
plugin_attrib = 'blocklist'
|
||||
interfaces = set(['items'])
|
||||
|
||||
def get_items(self):
|
||||
result = set()
|
||||
items = self.xml.findall('{%s}item' % self.namespace)
|
||||
if items is not None:
|
||||
for item in items:
|
||||
jid = JID(item.attrib.get('jid', ''))
|
||||
if jid:
|
||||
result.add(jid)
|
||||
return result
|
||||
|
||||
def set_items(self, values):
|
||||
self.del_items()
|
||||
for jid in values:
|
||||
if jid:
|
||||
item = ET.Element('{%s}item' % self.namespace)
|
||||
item.attrib['jid'] = JID(jid).full
|
||||
self.xml.append(item)
|
||||
|
||||
def del_items(self):
|
||||
items = self.xml.findall('{%s}item' % self.namespace)
|
||||
if items is not None:
|
||||
for item in items:
|
||||
self.xml.remove(item)
|
||||
|
||||
|
||||
class Block(BlockList):
|
||||
name = 'block'
|
||||
plugin_attrib = 'block'
|
||||
|
||||
|
||||
class Unblock(BlockList):
|
||||
name = 'unblock'
|
||||
plugin_attrib = 'unblock'
|
@@ -34,6 +34,32 @@ class XEP_0198(BasePlugin):
|
||||
description = 'XEP-0198: Stream Management'
|
||||
dependencies = set()
|
||||
stanza = stanza
|
||||
default_config = {
|
||||
#: The last ack number received from the server.
|
||||
'last_ack': 0,
|
||||
|
||||
#: The number of stanzas to wait between sending ack requests to
|
||||
#: the server. Setting this to ``1`` will send an ack request after
|
||||
#: every sent stanza. Defaults to ``5``.
|
||||
'window': 5,
|
||||
|
||||
#: The stream management ID for the stream. Knowing this value is
|
||||
#: required in order to do stream resumption.
|
||||
'sm_id': None,
|
||||
|
||||
#: A counter of handled incoming stanzas, mod 2^32.
|
||||
'handled': 0,
|
||||
|
||||
#: A counter of unacked outgoing stanzas, mod 2^32.
|
||||
'seq': 0,
|
||||
|
||||
#: Control whether or not the ability to resume the stream will be
|
||||
#: requested when enabling stream management. Defaults to ``True``.
|
||||
'allow_resume': True,
|
||||
|
||||
'order': 10100,
|
||||
'resume_order': 9000
|
||||
}
|
||||
|
||||
def plugin_init(self):
|
||||
"""Start the XEP-0198 plugin."""
|
||||
@@ -43,30 +69,9 @@ class XEP_0198(BasePlugin):
|
||||
if self.xmpp.is_component:
|
||||
return
|
||||
|
||||
#: The stream management ID for the stream. Knowing this value is
|
||||
#: required in order to do stream resumption.
|
||||
self.sm_id = self.config.get('sm_id', None)
|
||||
|
||||
#: A counter of handled incoming stanzas, mod 2^32.
|
||||
self.handled = self.config.get('handled', 0)
|
||||
|
||||
#: A counter of unacked outgoing stanzas, mod 2^32.
|
||||
self.seq = self.config.get('seq', 0)
|
||||
|
||||
#: The last ack number received from the server.
|
||||
self.last_ack = self.config.get('last_ack', 0)
|
||||
|
||||
#: The number of stanzas to wait between sending ack requests to
|
||||
#: the server. Setting this to ``1`` will send an ack request after
|
||||
#: every sent stanza. Defaults to ``5``.
|
||||
self.window = self.config.get('window', 5)
|
||||
self.window_counter = self.window
|
||||
self.window_counter_lock = threading.Lock()
|
||||
|
||||
#: Control whether or not the ability to resume the stream will be
|
||||
#: requested when enabling stream management. Defaults to ``True``.
|
||||
self.allow_resume = self.config.get('allow_resume', True)
|
||||
|
||||
self.enabled = threading.Event()
|
||||
self.unacked_queue = collections.deque()
|
||||
|
||||
@@ -92,11 +97,11 @@ class XEP_0198(BasePlugin):
|
||||
self.xmpp.register_feature('sm',
|
||||
self._handle_sm_feature,
|
||||
restart=True,
|
||||
order=self.config.get('order', 10100))
|
||||
order=self.order)
|
||||
self.xmpp.register_feature('sm',
|
||||
self._handle_sm_feature,
|
||||
restart=True,
|
||||
order=self.config.get('resume_order', 9000))
|
||||
order=self.resume_order)
|
||||
|
||||
self.xmpp.register_handler(
|
||||
Callback('Stream Management Enabled',
|
||||
@@ -133,6 +138,27 @@ class XEP_0198(BasePlugin):
|
||||
|
||||
self.xmpp.add_event_handler('session_end', self.session_end)
|
||||
|
||||
def plugin_end(self):
|
||||
if self.xmpp.is_component:
|
||||
return
|
||||
|
||||
self.xmpp.unregister_feature('sm', self.order)
|
||||
self.xmpp.unregister_feature('sm', self.resume_order)
|
||||
self.xmpp.del_event_handler('session_end', self.session_end)
|
||||
self.xmpp.del_filter('in', self._handle_incoming)
|
||||
self.xmpp.del_filter('out_sync', self._handle_outgoing)
|
||||
self.xmpp.remove_handler('Stream Management Enabled')
|
||||
self.xmpp.remove_handler('Stream Management Resumed')
|
||||
self.xmpp.remove_handler('Stream Management Failed')
|
||||
self.xmpp.remove_handler('Stream Management Ack')
|
||||
self.xmpp.remove_handler('Stream Management Request Ack')
|
||||
self.xmpp.remove_stanza(stanza.Enable)
|
||||
self.xmpp.remove_stanza(stanza.Enabled)
|
||||
self.xmpp.remove_stanza(stanza.Resume)
|
||||
self.xmpp.remove_stanza(stanza.Resumed)
|
||||
self.xmpp.remove_stanza(stanza.Ack)
|
||||
self.xmpp.remove_stanza(stanza.RequestAck)
|
||||
|
||||
def session_end(self, event):
|
||||
"""Reset stream management state."""
|
||||
self.enabled.clear()
|
||||
|
@@ -51,15 +51,16 @@ class XEP_0199(BasePlugin):
|
||||
description = 'XEP-0199: XMPP Ping'
|
||||
dependencies = set(['xep_0030'])
|
||||
stanza = stanza
|
||||
default_config = {
|
||||
'keepalive': False,
|
||||
'frequency': 300,
|
||||
'timeout': 30
|
||||
}
|
||||
|
||||
def plugin_init(self):
|
||||
"""
|
||||
Start the XEP-0199 plugin.
|
||||
"""
|
||||
self.keepalive = self.config.get('keepalive', False)
|
||||
self.frequency = float(self.config.get('frequency', 300))
|
||||
self.timeout = self.config.get('timeout', 30)
|
||||
|
||||
register_stanza_plugin(Iq, Ping)
|
||||
|
||||
self.xmpp.register_handler(
|
||||
@@ -74,6 +75,16 @@ class XEP_0199(BasePlugin):
|
||||
self.xmpp.add_event_handler('session_end',
|
||||
self._handle_session_end)
|
||||
|
||||
def plugin_end(self):
|
||||
self.xmpp['xep_0030'].del_feature(feature=Ping.namespace)
|
||||
self.xmpp.remove_handler('Ping')
|
||||
if self.keepalive:
|
||||
self.xmpp.del_event_handler('session_start',
|
||||
self._handle_keepalive)
|
||||
self.xmpp.del_event_handler('session_end',
|
||||
self._handle_session_end)
|
||||
|
||||
def session_bind(self, jid):
|
||||
self.xmpp['xep_0030'].add_feature(Ping.namespace)
|
||||
|
||||
def _handle_keepalive(self, event):
|
||||
|
@@ -30,21 +30,22 @@ class XEP_0202(BasePlugin):
|
||||
description = 'XEP-0202: Entity Time'
|
||||
dependencies = set(['xep_0030', 'xep_0082'])
|
||||
stanza = stanza
|
||||
default_config = {
|
||||
#: As a default, respond to time requests with the
|
||||
#: local time returned by XEP-0082. However, a
|
||||
#: custom function can be supplied which accepts
|
||||
#: the JID of the entity to query for the time.
|
||||
'local_time': None,
|
||||
'tz_offset': 0
|
||||
}
|
||||
|
||||
def plugin_init(self):
|
||||
"""Start the XEP-0203 plugin."""
|
||||
self.tz_offset = self.config.get('tz_offset', 0)
|
||||
|
||||
# As a default, respond to time requests with the
|
||||
# local time returned by XEP-0082. However, a
|
||||
# custom function can be supplied which accepts
|
||||
# the JID of the entity to query for the time.
|
||||
self.local_time = self.config.get('local_time', None)
|
||||
|
||||
def default_local_time(jid):
|
||||
return xep_0082.datetime(offset=self.tz_offset)
|
||||
|
||||
if not self.local_time:
|
||||
def default_local_time(jid):
|
||||
return xep_0082.datetime(offset=self.tz_offset)
|
||||
|
||||
self.local_time = default_local_time
|
||||
|
||||
self.xmpp.registerHandler(
|
||||
@@ -53,6 +54,11 @@ class XEP_0202(BasePlugin):
|
||||
self._handle_time_request))
|
||||
register_stanza_plugin(Iq, stanza.EntityTime)
|
||||
|
||||
def plugin_end(self):
|
||||
self.xmpp['xep_0030'].del_feature(feature='urn:xmpp:time')
|
||||
self.xmpp.remove_handler('Entity Time')
|
||||
|
||||
def session_bind(self, jid):
|
||||
self.xmpp['xep_0030'].add_feature('urn:xmpp:time')
|
||||
|
||||
def _handle_time_request(self, iq):
|
||||
|
@@ -22,7 +22,7 @@ class XEP_0222(BasePlugin):
|
||||
"""
|
||||
|
||||
name = 'xep_0222'
|
||||
description = 'XEP-0222: Persistent Storage of Private Data via PubSub'
|
||||
description = 'XEP-0222: Persistent Storage of Public Data via PubSub'
|
||||
dependencies = set(['xep_0163', 'xep_0060', 'xep_0004'])
|
||||
|
||||
profile = {'pubsub#persist_items': True,
|
||||
|
@@ -39,6 +39,11 @@ class XEP_0224(BasePlugin):
|
||||
StanzaPath('message/attention'),
|
||||
self._handle_attention))
|
||||
|
||||
def plugin_end(self):
|
||||
self.xmpp['xep_0030'].del_feature(feature=stanza.Attention.namespace)
|
||||
self.xmpp.remove_handler('Attention')
|
||||
|
||||
def session_bind(self, jid):
|
||||
self.xmpp['xep_0030'].add_feature(stanza.Attention.namespace)
|
||||
|
||||
def request_attention(self, to, mfrom=None, mbody=''):
|
||||
|
@@ -35,8 +35,6 @@ class XEP_0231(BasePlugin):
|
||||
def plugin_init(self):
|
||||
self._cids = {}
|
||||
|
||||
self.xmpp['xep_0030'].add_feature('urn:xmpp:bob')
|
||||
|
||||
register_stanza_plugin(Iq, BitsOfBinary)
|
||||
|
||||
self.xmpp.register_handler(
|
||||
@@ -58,6 +56,15 @@ class XEP_0231(BasePlugin):
|
||||
self.api.register(self._set_bob, 'set_bob', default=True)
|
||||
self.api.register(self._del_bob, 'del_bob', default=True)
|
||||
|
||||
def plugin_end(self):
|
||||
self.xmpp['xep_0030'].del_feature(feature='urn:xmpp:bob')
|
||||
self.xmpp.remove_handler('Bits of Binary - Iq')
|
||||
self.xmpp.remove_handler('Bits of Binary - Message')
|
||||
self.xmpp.remove_handler('Bits of Binary - Presence')
|
||||
|
||||
def session_bind(self, jid):
|
||||
self.xmpp['xep_0030'].add_feature('urn:xmpp:bob')
|
||||
|
||||
def set_bob(self, data, mtype, cid=None, max_age=None):
|
||||
if cid is None:
|
||||
cid = 'sha1+%s@bob.xmpp.org' % hashlib.sha1(data).hexdigest()
|
||||
|
@@ -39,6 +39,11 @@ class XEP_0249(BasePlugin):
|
||||
|
||||
register_stanza_plugin(Message, Invite)
|
||||
|
||||
def plugin_end(self):
|
||||
self.xmpp['xep_0030'].del_feature(feature=Invite.namespace)
|
||||
self.xmpp.remove_handler('Direct MUC Invitations')
|
||||
|
||||
def session_bind(self, jid):
|
||||
self.xmpp['xep_0030'].add_feature(Invite.namespace)
|
||||
|
||||
def _handle_invite(self, msg):
|
||||
|
73
sleekxmpp/plugins/xep_0256.py
Normal file
73
sleekxmpp/plugins/xep_0256.py
Normal file
@@ -0,0 +1,73 @@
|
||||
"""
|
||||
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 Presence
|
||||
from sleekxmpp.exceptions import XMPPError
|
||||
from sleekxmpp.plugins import BasePlugin, register_plugin
|
||||
from sleekxmpp.xmlstream import register_stanza_plugin
|
||||
|
||||
from sleekxmpp.plugins.xep_0012 import stanza, LastActivity
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class XEP_0256(BasePlugin):
|
||||
|
||||
name = 'xep_0256'
|
||||
description = 'XEP-0256: Last Activity in Presence'
|
||||
dependencies = set(['xep_0012'])
|
||||
stanza = stanza
|
||||
default_config = {
|
||||
'auto_last_activity': False
|
||||
}
|
||||
|
||||
def plugin_init(self):
|
||||
register_stanza_plugin(Presence, LastActivity)
|
||||
|
||||
self.xmpp.add_filter('out', self._initial_presence_activity)
|
||||
self.xmpp.add_event_handler('connected', self._reset_presence_activity)
|
||||
|
||||
self._initial_presence = set()
|
||||
|
||||
def plugin_end(self):
|
||||
self.xmpp.del_filter('out', self._initial_presence_activity)
|
||||
self.xmpp.del_event_handler('connected', self._reset_presence_activity)
|
||||
|
||||
def _reset_presence_activity(self, e):
|
||||
self._initial_presence = set()
|
||||
|
||||
def _initial_presence_activity(self, stanza):
|
||||
if isinstance(stanza, Presence):
|
||||
use_last_activity = False
|
||||
|
||||
if self.auto_last_activity and stanza['show'] in ('xa', 'away'):
|
||||
use_last_activity = True
|
||||
|
||||
if stanza['from'] not in self._initial_presence:
|
||||
self._initial_presence.add(stanza['from'])
|
||||
use_last_activity = True
|
||||
|
||||
if use_last_activity:
|
||||
plugin = self.xmpp['xep_0012']
|
||||
try:
|
||||
result = plugin.api['get_last_activity'](stanza['from'],
|
||||
None,
|
||||
stanza['to'])
|
||||
seconds = result['last_activity']['seconds']
|
||||
except XMPPError:
|
||||
seconds = None
|
||||
|
||||
if seconds is not None:
|
||||
stanza['last_activity']['seconds'] = seconds
|
||||
return stanza
|
||||
|
||||
|
||||
register_plugin(XEP_0256)
|
@@ -25,11 +25,15 @@ class XEP_0258(BasePlugin):
|
||||
stanza = stanza
|
||||
|
||||
def plugin_init(self):
|
||||
self.xmpp['xep_0030'].add_feature(SecurityLabel.namespace)
|
||||
|
||||
register_stanza_plugin(Message, SecurityLabel)
|
||||
register_stanza_plugin(Iq, Catalog)
|
||||
|
||||
def plugin_end(self):
|
||||
self.xmpp['xep_0030'].del_feature(feature=SecurityLabel.namespace)
|
||||
|
||||
def session_bind(self, jid):
|
||||
self.xmpp['xep_0030'].add_feature(SecurityLabel.namespace)
|
||||
|
||||
def get_catalog(self, jid, ifrom=None, block=True,
|
||||
callback=None, timeout=None):
|
||||
iq = self.xmpp.Iq()
|
||||
|
20
sleekxmpp/plugins/xep_0270.py
Normal file
20
sleekxmpp/plugins/xep_0270.py
Normal file
@@ -0,0 +1,20 @@
|
||||
"""
|
||||
SleekXMPP: The Sleek XMPP Library
|
||||
Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
|
||||
This file is part of SleekXMPP.
|
||||
|
||||
See the file LICENSE for copying permission.
|
||||
"""
|
||||
|
||||
from sleekxmpp.plugins import BasePlugin, register_plugin
|
||||
|
||||
|
||||
class XEP_0270(BasePlugin):
|
||||
|
||||
name = 'xep_0270'
|
||||
description = 'XEP-0270: XMPP Compliance Suites 2010'
|
||||
dependencies = set(['xep_0030', 'xep_0115', 'xep_0054',
|
||||
'xep_0163', 'xep_0045', 'xep_0085'])
|
||||
|
||||
|
||||
register_plugin(XEP_0270)
|
21
sleekxmpp/plugins/xep_0302.py
Normal file
21
sleekxmpp/plugins/xep_0302.py
Normal file
@@ -0,0 +1,21 @@
|
||||
"""
|
||||
SleekXMPP: The Sleek XMPP Library
|
||||
Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
|
||||
This file is part of SleekXMPP.
|
||||
|
||||
See the file LICENSE for copying permission.
|
||||
"""
|
||||
|
||||
from sleekxmpp.plugins import BasePlugin, register_plugin
|
||||
|
||||
|
||||
class XEP_0302(BasePlugin):
|
||||
|
||||
name = 'xep_0302'
|
||||
description = 'XEP-0302: XMPP Compliance Suites 2012'
|
||||
dependencies = set(['xep_0030', 'xep_0115', 'xep_0054',
|
||||
'xep_0163', 'xep_0045', 'xep_0085',
|
||||
'xep_0184', 'xep_0198'])
|
||||
|
||||
|
||||
register_plugin(XEP_0302)
|
@@ -94,10 +94,12 @@ class Roster(object):
|
||||
Arguments:
|
||||
key -- Return the roster for this JID.
|
||||
"""
|
||||
if isinstance(key, JID):
|
||||
key = key.bare
|
||||
if key is None:
|
||||
key = self.xmpp.boundjid.bare
|
||||
key = self.xmpp.boundjid
|
||||
if not isinstance(key, JID):
|
||||
key = JID(key)
|
||||
key = key.bare
|
||||
|
||||
if key not in self._rosters:
|
||||
self.add(key)
|
||||
self._rosters[key].auto_authorize = self.auto_authorize
|
||||
@@ -119,8 +121,10 @@ class Roster(object):
|
||||
Arguments:
|
||||
node -- The JID for the new roster node.
|
||||
"""
|
||||
if isinstance(node, JID):
|
||||
node = node.bare
|
||||
if not isinstance(node, JID):
|
||||
node = JID(node)
|
||||
|
||||
node = node.bare
|
||||
if node not in self._rosters:
|
||||
self._rosters[node] = RosterNode(self.xmpp, node, self.db)
|
||||
|
||||
|
@@ -89,8 +89,11 @@ class RosterNode(object):
|
||||
|
||||
A new item entry will be created if one does not already exist.
|
||||
"""
|
||||
if isinstance(key, JID):
|
||||
key = key.bare
|
||||
if key is None:
|
||||
key = JID('')
|
||||
if not isinstance(key, JID):
|
||||
key = JID(key)
|
||||
key = key.bare
|
||||
if key not in self._jids:
|
||||
self.add(key, save=True)
|
||||
return self._jids[key]
|
||||
@@ -101,8 +104,11 @@ class RosterNode(object):
|
||||
|
||||
To remove an item from the server, use the remove() method.
|
||||
"""
|
||||
if isinstance(key, JID):
|
||||
key = key.bare
|
||||
if key is None:
|
||||
key = JID('')
|
||||
if not isinstance(key, JID):
|
||||
key = JID(key)
|
||||
key = key.bare
|
||||
if key in self._jids:
|
||||
del self._jids[key]
|
||||
|
||||
|
@@ -52,7 +52,7 @@ class Error(ElementBase):
|
||||
name = 'error'
|
||||
plugin_attrib = 'error'
|
||||
interfaces = set(('code', 'condition', 'text', 'type',
|
||||
'gone', 'redirect'))
|
||||
'gone', 'redirect', 'by'))
|
||||
sub_interfaces = set(('text',))
|
||||
plugin_attrib_map = {}
|
||||
plugin_tag_map = {}
|
||||
|
@@ -7,7 +7,7 @@
|
||||
"""
|
||||
|
||||
from sleekxmpp.stanza.rootstanza import RootStanza
|
||||
from sleekxmpp.xmlstream import StanzaBase
|
||||
from sleekxmpp.xmlstream import StanzaBase, ET
|
||||
|
||||
|
||||
class Message(RootStanza):
|
||||
@@ -54,13 +54,14 @@ class Message(RootStanza):
|
||||
del_mucnick -- Dummy method to prevent deletion.
|
||||
"""
|
||||
|
||||
namespace = 'jabber:client'
|
||||
name = 'message'
|
||||
interfaces = set(('type', 'to', 'from', 'id', 'body', 'subject',
|
||||
'mucroom', 'mucnick'))
|
||||
sub_interfaces = set(('body', 'subject'))
|
||||
namespace = 'jabber:client'
|
||||
plugin_attrib = name
|
||||
types = set((None, 'normal', 'chat', 'headline', 'error', 'groupchat'))
|
||||
interfaces = set(['type', 'to', 'from', 'id', 'body', 'subject',
|
||||
'thread', 'parent_thread', 'mucroom', 'mucnick'])
|
||||
sub_interfaces = set(['body', 'subject', 'thread'])
|
||||
lang_interfaces = sub_interfaces
|
||||
types = set(['normal', 'chat', 'headline', 'error', 'groupchat'])
|
||||
|
||||
def get_type(self):
|
||||
"""
|
||||
@@ -72,6 +73,31 @@ class Message(RootStanza):
|
||||
"""
|
||||
return self._get_attr('type', 'normal')
|
||||
|
||||
def get_parent_thread(self):
|
||||
"""Return the message thread's parent thread."""
|
||||
thread = self.xml.find('{%s}thread' % self.namespace)
|
||||
if thread is not None:
|
||||
return thread.attrib.get('parent', '')
|
||||
return ''
|
||||
|
||||
def set_parent_thread(self, value):
|
||||
"""Add or change the message thread's parent thread."""
|
||||
thread = self.xml.find('{%s}thread' % self.namespace)
|
||||
if value:
|
||||
if thread is None:
|
||||
thread = ET.Element('{%s}thread' % self.namespace)
|
||||
self.xml.append(thread)
|
||||
thread.attrib['parent'] = value
|
||||
else:
|
||||
if thread is not None and 'parent' in thread.attrib:
|
||||
del thread.attrib['parent']
|
||||
|
||||
def del_parent_thread(self):
|
||||
"""Delete the message thread's parent reference."""
|
||||
thread = self.xml.find('{%s}thread' % self.namespace)
|
||||
if thread is not None and 'parent' in thread.attrib:
|
||||
del thread.attrib['parent']
|
||||
|
||||
def chat(self):
|
||||
"""Set the message type to 'chat'."""
|
||||
self['type'] = 'chat'
|
||||
@@ -96,10 +122,16 @@ class Message(RootStanza):
|
||||
clear -- Indicates if existing content should be removed
|
||||
before replying. Defaults to True.
|
||||
"""
|
||||
thread = self['thread']
|
||||
parent = self['parent_thread']
|
||||
|
||||
StanzaBase.reply(self, clear)
|
||||
if self['type'] == 'groupchat':
|
||||
self['to'] = self['to'].bare
|
||||
|
||||
self['thread'] = thread
|
||||
self['parent_thread'] = parent
|
||||
|
||||
del self['id']
|
||||
|
||||
if body is not None:
|
||||
|
@@ -60,16 +60,17 @@ class Presence(RootStanza):
|
||||
set_priority -- Set the value of the <priority> element.
|
||||
"""
|
||||
|
||||
namespace = 'jabber:client'
|
||||
name = 'presence'
|
||||
interfaces = set(('type', 'to', 'from', 'id', 'show',
|
||||
'status', 'priority'))
|
||||
sub_interfaces = set(('show', 'status', 'priority'))
|
||||
namespace = 'jabber:client'
|
||||
plugin_attrib = name
|
||||
interfaces = set(['type', 'to', 'from', 'id', 'show',
|
||||
'status', 'priority'])
|
||||
sub_interfaces = set(['show', 'status', 'priority'])
|
||||
lang_interfaces = set(['status'])
|
||||
|
||||
types = set(('available', 'unavailable', 'error', 'probe', 'subscribe',
|
||||
'subscribed', 'unsubscribe', 'unsubscribed'))
|
||||
showtypes = set(('dnd', 'chat', 'xa', 'away'))
|
||||
types = set(['available', 'unavailable', 'error', 'probe', 'subscribe',
|
||||
'subscribed', 'unsubscribe', 'unsubscribed'])
|
||||
showtypes = set(['dnd', 'chat', 'xa', 'away'])
|
||||
|
||||
def exception(self, e):
|
||||
"""
|
||||
|
@@ -8,10 +8,8 @@
|
||||
|
||||
import socket
|
||||
import threading
|
||||
try:
|
||||
import queue
|
||||
except ImportError:
|
||||
import Queue as queue
|
||||
|
||||
from sleekxmpp.util import Queue
|
||||
|
||||
|
||||
class TestLiveSocket(object):
|
||||
@@ -39,8 +37,8 @@ class TestLiveSocket(object):
|
||||
"""
|
||||
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
self.recv_buffer = []
|
||||
self.recv_queue = queue.Queue()
|
||||
self.send_queue = queue.Queue()
|
||||
self.recv_queue = Queue()
|
||||
self.send_queue = Queue()
|
||||
self.send_queue_lock = threading.Lock()
|
||||
self.recv_queue_lock = threading.Lock()
|
||||
self.is_live = True
|
||||
|
@@ -7,10 +7,8 @@
|
||||
"""
|
||||
|
||||
import socket
|
||||
try:
|
||||
import queue
|
||||
except ImportError:
|
||||
import Queue as queue
|
||||
|
||||
from sleekxmpp.util import Queue
|
||||
|
||||
|
||||
class TestSocket(object):
|
||||
@@ -36,8 +34,8 @@ class TestSocket(object):
|
||||
Same as arguments for socket.socket
|
||||
"""
|
||||
self.socket = socket.socket(*args, **kwargs)
|
||||
self.recv_queue = queue.Queue()
|
||||
self.send_queue = queue.Queue()
|
||||
self.recv_queue = Queue()
|
||||
self.send_queue = Queue()
|
||||
self.is_live = False
|
||||
self.disconnected = False
|
||||
|
||||
|
@@ -8,13 +8,10 @@
|
||||
|
||||
import unittest
|
||||
from xml.parsers.expat import ExpatError
|
||||
try:
|
||||
import Queue as queue
|
||||
except:
|
||||
import queue
|
||||
|
||||
import sleekxmpp
|
||||
from sleekxmpp import ClientXMPP, ComponentXMPP
|
||||
from sleekxmpp.util import Queue
|
||||
from sleekxmpp.stanza import Message, Iq, Presence
|
||||
from sleekxmpp.test import TestSocket, TestLiveSocket
|
||||
from sleekxmpp.exceptions import XMPPError, IqTimeout, IqError
|
||||
@@ -338,7 +335,7 @@ class SleekTest(unittest.TestCase):
|
||||
|
||||
# We will use this to wait for the session_start event
|
||||
# for live connections.
|
||||
skip_queue = queue.Queue()
|
||||
skip_queue = Queue()
|
||||
|
||||
if socket == 'mock':
|
||||
self.xmpp.set_socket(TestSocket())
|
||||
|
3
sleekxmpp/thirdparty/suelta/util.py
vendored
3
sleekxmpp/thirdparty/suelta/util.py
vendored
@@ -15,6 +15,9 @@ def bytes(text):
|
||||
:param text: Unicode text to convert to bytes
|
||||
:rtype: bytes (Python3), str (Python2.6+)
|
||||
"""
|
||||
if text is None:
|
||||
return b''
|
||||
|
||||
if sys.version_info < (3, 0):
|
||||
import __builtin__
|
||||
return __builtin__.bytes(text)
|
||||
|
23
sleekxmpp/util/__init__.py
Normal file
23
sleekxmpp/util/__init__.py
Normal file
@@ -0,0 +1,23 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
sleekxmpp.util
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
Part of SleekXMPP: The Sleek XMPP Library
|
||||
|
||||
:copyright: (c) 2012 Nathanael C. Fritz, Lance J.T. Stout
|
||||
:license: MIT, see LICENSE for more details
|
||||
"""
|
||||
|
||||
|
||||
# =====================================================================
|
||||
# Standardize import of Queue class:
|
||||
|
||||
try:
|
||||
import queue
|
||||
except ImportError:
|
||||
import Queue as queue
|
||||
|
||||
|
||||
Queue = queue.Queue
|
||||
QueueEmpty = queue.Empty
|
158
sleekxmpp/util/stringprep_profiles.py
Normal file
158
sleekxmpp/util/stringprep_profiles.py
Normal file
@@ -0,0 +1,158 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
sleekxmpp.util.stringprep_profiles
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
This module makes it easier to define profiles of stringprep,
|
||||
such as nodeprep and resourceprep for JID validation, and
|
||||
SASLprep for SASL.
|
||||
|
||||
Part of SleekXMPP: The Sleek XMPP Library
|
||||
|
||||
:copyright: (c) 2012 Nathanael C. Fritz, Lance J.T. Stout
|
||||
:license: MIT, see LICENSE for more details
|
||||
"""
|
||||
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import sys
|
||||
import stringprep
|
||||
import unicodedata
|
||||
|
||||
|
||||
class StringPrepError(UnicodeError):
|
||||
pass
|
||||
|
||||
|
||||
def to_unicode(data):
|
||||
"""Ensure that a given string is Unicode, regardless of Python version."""
|
||||
if sys.version_info < (3, 0):
|
||||
return unicode(data)
|
||||
else:
|
||||
return str(data)
|
||||
|
||||
|
||||
def b1_mapping(char):
|
||||
"""Map characters that are commonly mapped to nothing."""
|
||||
return '' if stringprep.in_table_b1(char) else None
|
||||
|
||||
|
||||
def c12_mapping(char):
|
||||
"""Map non-ASCII whitespace to spaces."""
|
||||
return ' ' if stringprep.in_table_c12(char) else None
|
||||
|
||||
|
||||
def map_input(data, tables=None):
|
||||
"""
|
||||
Each character in the input stream MUST be checked against
|
||||
a mapping table.
|
||||
"""
|
||||
result = []
|
||||
for char in data:
|
||||
replacement = None
|
||||
|
||||
for mapping in tables:
|
||||
replacement = mapping(char)
|
||||
if replacement is not None:
|
||||
break
|
||||
|
||||
if replacement is None:
|
||||
replacement = char
|
||||
result.append(replacement)
|
||||
return ''.join(result)
|
||||
|
||||
|
||||
def normalize(data, nfkc=True):
|
||||
"""
|
||||
A profile can specify one of two options for Unicode normalization:
|
||||
- no normalization
|
||||
- Unicode normalization with form KC
|
||||
"""
|
||||
if nfkc:
|
||||
data = unicodedata.normalize('NFKC', data)
|
||||
return data
|
||||
|
||||
|
||||
def prohibit_output(data, tables=None):
|
||||
"""
|
||||
Before the text can be emitted, it MUST be checked for prohibited
|
||||
code points.
|
||||
"""
|
||||
for char in data:
|
||||
for check in tables:
|
||||
if check(char):
|
||||
raise StringPrepError("Prohibited code point: %s" % char)
|
||||
|
||||
|
||||
def check_bidi(data):
|
||||
"""
|
||||
1) The characters in section 5.8 MUST be prohibited.
|
||||
|
||||
2) If a string contains any RandALCat character, the string MUST NOT
|
||||
contain any LCat character.
|
||||
|
||||
3) If a string contains any RandALCat character, a RandALCat
|
||||
character MUST be the first character of the string, and a
|
||||
RandALCat character MUST be the last character of the string.
|
||||
"""
|
||||
if not data:
|
||||
return data
|
||||
|
||||
has_lcat = False
|
||||
has_randal = False
|
||||
|
||||
for c in data:
|
||||
if stringprep.in_table_c8(c):
|
||||
raise StringPrepError("BIDI violation: seciton 6 (1)")
|
||||
if stringprep.in_table_d1(c):
|
||||
has_randal = True
|
||||
elif stringprep.in_table_d2(c):
|
||||
has_lcat = True
|
||||
|
||||
if has_randal and has_lcat:
|
||||
raise StringPrepError("BIDI violation: section 6 (2)")
|
||||
|
||||
first_randal = stringprep.in_table_d1(data[0])
|
||||
last_randal = stringprep.in_table_d1(data[-1])
|
||||
if has_randal and not (first_randal and last_randal):
|
||||
raise StringPrepError("BIDI violation: section 6 (3)")
|
||||
|
||||
|
||||
def create(nfkc=True, bidi=True, mappings=None,
|
||||
prohibited=None, unassigned=None):
|
||||
"""Create a profile of stringprep.
|
||||
|
||||
:param bool nfkc:
|
||||
If `True`, perform NFKC Unicode normalization. Defaults to `True`.
|
||||
:param bool bidi:
|
||||
If `True`, perform bidirectional text checks. Defaults to `True`.
|
||||
:param list mappings:
|
||||
Optional list of functions for mapping characters to
|
||||
suitable replacements.
|
||||
:param list prohibited:
|
||||
Optional list of functions which check for the presence of
|
||||
prohibited characters.
|
||||
:param list unassigned:
|
||||
Optional list of functions for detecting the use of unassigned
|
||||
code points.
|
||||
|
||||
:raises: StringPrepError
|
||||
:return: Unicode string of the resulting text passing the
|
||||
profile's requirements.
|
||||
"""
|
||||
def profile(data, query=False):
|
||||
try:
|
||||
data = to_unicode(data)
|
||||
except UnicodeError:
|
||||
raise StringPrepError
|
||||
|
||||
data = map_input(data, mappings)
|
||||
data = normalize(data, nfkc)
|
||||
prohibit_output(data, prohibited)
|
||||
if bidi:
|
||||
check_bidi(data)
|
||||
if query and unassigned:
|
||||
check_unassigned(data, unassigned)
|
||||
return data
|
||||
return profile
|
@@ -9,5 +9,5 @@
|
||||
# We don't want to have to import the entire library
|
||||
# just to get the version info for setup.py
|
||||
|
||||
__version__ = '1.1.7'
|
||||
__version_info__ = (1, 1, 7, '', 0)
|
||||
__version__ = '1.1.10'
|
||||
__version_info__ = (1, 1, 10, '', 0)
|
||||
|
@@ -6,7 +6,7 @@
|
||||
See the file LICENSE for copying permission.
|
||||
"""
|
||||
|
||||
from sleekxmpp.xmlstream.jid import JID
|
||||
from sleekxmpp.jid import JID
|
||||
from sleekxmpp.xmlstream.scheduler import Scheduler
|
||||
from sleekxmpp.xmlstream.stanzabase import StanzaBase, ElementBase, ET
|
||||
from sleekxmpp.xmlstream.stanzabase import register_stanza_plugin
|
||||
|
@@ -10,11 +10,8 @@
|
||||
"""
|
||||
|
||||
import logging
|
||||
try:
|
||||
import queue
|
||||
except ImportError:
|
||||
import Queue as queue
|
||||
|
||||
from sleekxmpp.util import Queue, QueueEmpty
|
||||
from sleekxmpp.xmlstream.handler.base import BaseHandler
|
||||
|
||||
|
||||
@@ -37,7 +34,7 @@ class Waiter(BaseHandler):
|
||||
|
||||
def __init__(self, name, matcher, stream=None):
|
||||
BaseHandler.__init__(self, name, matcher, stream=stream)
|
||||
self._payload = queue.Queue()
|
||||
self._payload = Queue()
|
||||
|
||||
def prerun(self, payload):
|
||||
"""Store the matched stanza when received during processing.
|
||||
@@ -74,7 +71,7 @@ class Waiter(BaseHandler):
|
||||
try:
|
||||
stanza = self._payload.get(True, 1)
|
||||
break
|
||||
except queue.Empty:
|
||||
except QueueEmpty:
|
||||
elapsed_time += 1
|
||||
if elapsed_time >= timeout:
|
||||
log.warning("Timed out waiting for %s", self.name)
|
||||
|
@@ -1,145 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
sleekxmpp.xmlstream.jid
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
import logging
|
||||
|
||||
This module allows for working with Jabber IDs (JIDs) by
|
||||
providing accessors for the various components of a JID.
|
||||
logging.warning('Deprecated: sleekxmpp.xmlstream.jid is moving to sleekxmpp.jid')
|
||||
|
||||
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
|
||||
|
||||
|
||||
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 value of the full JID.
|
||||
:bare: The value of the bare JID.
|
||||
:user: The username portion of the JID.
|
||||
:domain: The domain name portion of the JID.
|
||||
:server: Alias for ``domain``.
|
||||
:resource: The resource portion of the JID.
|
||||
|
||||
:param string jid: A string of the form ``'[user@]domain[/resource]'``.
|
||||
"""
|
||||
|
||||
def __init__(self, jid):
|
||||
"""Initialize a new JID"""
|
||||
self.reset(jid)
|
||||
|
||||
def reset(self, jid):
|
||||
"""Start fresh from a new JID string.
|
||||
|
||||
:param string jid: A string of the form ``'[user@]domain[/resource]'``.
|
||||
"""
|
||||
if isinstance(jid, JID):
|
||||
jid = jid.full
|
||||
self._full = self._jid = jid
|
||||
self._domain = None
|
||||
self._resource = None
|
||||
self._user = None
|
||||
self._bare = None
|
||||
|
||||
def __getattr__(self, name):
|
||||
"""Handle getting the JID values, using cache if available.
|
||||
|
||||
:param name: One of: user, server, domain, resource,
|
||||
full, or bare.
|
||||
"""
|
||||
if name == 'resource':
|
||||
if self._resource is None and '/' in self._jid:
|
||||
self._resource = self._jid.split('/', 1)[-1]
|
||||
return self._resource or ""
|
||||
elif name == 'user':
|
||||
if self._user is None:
|
||||
if '@' in self._jid:
|
||||
self._user = self._jid.split('@', 1)[0]
|
||||
else:
|
||||
self._user = self._user
|
||||
return self._user or ""
|
||||
elif name in ('server', 'domain', 'host'):
|
||||
if self._domain is None:
|
||||
self._domain = self._jid.split('@', 1)[-1].split('/', 1)[0]
|
||||
return self._domain or ""
|
||||
elif name in ('full', 'jid'):
|
||||
return self._jid or ""
|
||||
elif name == 'bare':
|
||||
if self._bare is None:
|
||||
self._bare = self._jid.split('/', 1)[0]
|
||||
return self._bare or ""
|
||||
|
||||
def __setattr__(self, name, value):
|
||||
"""Edit a JID by updating it's individual values, resetting the
|
||||
generated JID in the end.
|
||||
|
||||
Arguments:
|
||||
name -- The name of the JID part. One of: user, domain,
|
||||
server, resource, full, jid, or bare.
|
||||
value -- The new value for the JID part.
|
||||
"""
|
||||
if name in ('resource', 'user', 'domain'):
|
||||
object.__setattr__(self, "_%s" % name, value)
|
||||
self.regenerate()
|
||||
elif name in ('server', 'domain', 'host'):
|
||||
self.domain = value
|
||||
elif name in ('full', 'jid'):
|
||||
self.reset(value)
|
||||
self.regenerate()
|
||||
elif name == 'bare':
|
||||
if '@' in value:
|
||||
u, d = value.split('@', 1)
|
||||
object.__setattr__(self, "_user", u)
|
||||
object.__setattr__(self, "_domain", d)
|
||||
else:
|
||||
object.__setattr__(self, "_user", '')
|
||||
object.__setattr__(self, "_domain", value)
|
||||
self.regenerate()
|
||||
else:
|
||||
object.__setattr__(self, name, value)
|
||||
|
||||
def regenerate(self):
|
||||
"""Generate a new JID based on current values, useful after editing."""
|
||||
jid = ""
|
||||
if self.user:
|
||||
jid = "%s@" % self.user
|
||||
jid += self.domain
|
||||
if self.resource:
|
||||
jid += "/%s" % self.resource
|
||||
self.reset(jid)
|
||||
|
||||
def __str__(self):
|
||||
"""Use the full JID as the string value."""
|
||||
return self.full
|
||||
|
||||
def __repr__(self):
|
||||
return self.full
|
||||
|
||||
def __eq__(self, other):
|
||||
"""
|
||||
Two JIDs are considered equal if they have the same full JID value.
|
||||
"""
|
||||
other = JID(other)
|
||||
return self.full == other.full
|
||||
|
||||
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.full)
|
||||
from sleekxmpp.jid import JID
|
||||
|
@@ -254,6 +254,7 @@ def get_SRV(host, port, service, proto='tcp', resolver=None):
|
||||
by SRV priorities and weights.
|
||||
"""
|
||||
if resolver is None:
|
||||
log.warning("DNS: dnspython not found. Can not use SRV lookup.")
|
||||
return [(host, port)]
|
||||
|
||||
log.debug("DNS: Querying SRV records for %s" % host)
|
||||
|
@@ -15,10 +15,9 @@
|
||||
import time
|
||||
import threading
|
||||
import logging
|
||||
try:
|
||||
import queue
|
||||
except ImportError:
|
||||
import Queue as queue
|
||||
import itertools
|
||||
|
||||
from sleekxmpp.util import Queue, QueueEmpty
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
@@ -102,7 +101,7 @@ class Scheduler(object):
|
||||
|
||||
def __init__(self, parentstop=None):
|
||||
#: A queue for storing tasks
|
||||
self.addq = queue.Queue()
|
||||
self.addq = Queue()
|
||||
|
||||
#: A list of tasks in order of execution time.
|
||||
self.schedule = []
|
||||
@@ -157,26 +156,32 @@ class Scheduler(object):
|
||||
elapsed < wait:
|
||||
newtask = self.addq.get(True, 0.1)
|
||||
elapsed += 0.1
|
||||
except queue.Empty:
|
||||
cleanup = []
|
||||
except QueueEmpty:
|
||||
self.schedule_lock.acquire()
|
||||
for task in self.schedule:
|
||||
if time.time() >= task.next:
|
||||
updated = True
|
||||
if not task.run():
|
||||
cleanup.append(task)
|
||||
# select only those tasks which are to be executed now
|
||||
relevant = itertools.takewhile(
|
||||
lambda task: time.time() >= task.next, self.schedule)
|
||||
# run the tasks and keep the return value in a tuple
|
||||
status = map(lambda task: (task, task.run()), relevant)
|
||||
# remove non-repeating tasks
|
||||
for task, doRepeat in status:
|
||||
if not doRepeat:
|
||||
try:
|
||||
self.schedule.remove(task)
|
||||
except ValueError:
|
||||
pass
|
||||
else:
|
||||
break
|
||||
for task in cleanup:
|
||||
self.schedule.pop(self.schedule.index(task))
|
||||
# only need to resort tasks if a repeated task has
|
||||
# been kept in the list.
|
||||
updated = True
|
||||
else:
|
||||
updated = True
|
||||
self.schedule_lock.acquire()
|
||||
self.schedule.append(newtask)
|
||||
if newtask is not None:
|
||||
self.schedule.append(newtask)
|
||||
finally:
|
||||
if updated:
|
||||
self.schedule = sorted(self.schedule,
|
||||
key=lambda task: task.next)
|
||||
self.schedule.sort(key=lambda task: task.next)
|
||||
self.schedule_lock.release()
|
||||
except KeyboardInterrupt:
|
||||
self.run = False
|
||||
|
@@ -421,12 +421,6 @@ class ElementBase(object):
|
||||
#: ``'{namespace}elementname'``.
|
||||
self.tag = self.tag_name()
|
||||
|
||||
if 'lang' not in self.interfaces:
|
||||
if isinstance(self.interfaces, tuple):
|
||||
self.interfaces += ('lang',)
|
||||
else:
|
||||
self.interfaces.add('lang')
|
||||
|
||||
#: A :class:`weakref.weakref` to the parent stanza, if there is one.
|
||||
#: If not, then :attr:`parent` is ``None``.
|
||||
self.parent = None
|
||||
@@ -520,8 +514,9 @@ class ElementBase(object):
|
||||
:param string attrib: The :attr:`plugin_attrib` value of the
|
||||
plugin to enable.
|
||||
"""
|
||||
if lang is None:
|
||||
lang = self.get_lang()
|
||||
default_lang = self.get_lang()
|
||||
if not lang:
|
||||
lang = default_lang
|
||||
|
||||
plugin_class = self.plugin_attrib_map[attrib]
|
||||
|
||||
@@ -534,7 +529,7 @@ class ElementBase(object):
|
||||
existing_xml = self.xml.find(plugin_class.tag_name())
|
||||
|
||||
if existing_xml is not None:
|
||||
if existing_xml.attrib.get('{%s}lang' % XML_NS, '') != lang:
|
||||
if existing_xml.attrib.get('{%s}lang' % XML_NS, default_lang) != lang:
|
||||
existing_xml = None
|
||||
|
||||
plugin = plugin_class(parent=self, xml=existing_xml)
|
||||
@@ -542,7 +537,8 @@ class ElementBase(object):
|
||||
if plugin.is_extension:
|
||||
self.plugins[(attrib, None)] = plugin
|
||||
else:
|
||||
plugin['lang'] = lang
|
||||
if lang != default_lang:
|
||||
plugin['lang'] = lang
|
||||
self.plugins[(attrib, lang)] = plugin
|
||||
|
||||
if plugin_class in self.plugin_iterables:
|
||||
@@ -574,6 +570,7 @@ class ElementBase(object):
|
||||
.. versionadded:: 1.0-Beta1
|
||||
"""
|
||||
values = {}
|
||||
values['lang'] = self['lang']
|
||||
for interface in self.interfaces:
|
||||
values[interface] = self[interface]
|
||||
if interface in self.lang_interfaces:
|
||||
@@ -581,7 +578,7 @@ class ElementBase(object):
|
||||
for plugin, stanza in self.plugins.items():
|
||||
lang = stanza['lang']
|
||||
if lang:
|
||||
values['%s|%s' % (plugin, lang)] = stanza.values
|
||||
values['%s|%s' % (plugin[0], lang)] = stanza.values
|
||||
else:
|
||||
values[plugin[0]] = stanza.values
|
||||
if self.iterables:
|
||||
@@ -629,6 +626,8 @@ class ElementBase(object):
|
||||
sub.values = subdict
|
||||
self.iterables.append(sub)
|
||||
break
|
||||
elif interface == 'lang':
|
||||
self[interface] = value
|
||||
elif interface in self.interfaces:
|
||||
self[full_interface] = value
|
||||
elif interface in self.plugin_attrib_map:
|
||||
@@ -678,7 +677,7 @@ class ElementBase(object):
|
||||
|
||||
if attrib == 'substanzas':
|
||||
return self.iterables
|
||||
elif attrib in self.interfaces:
|
||||
elif attrib in self.interfaces or attrib == 'lang':
|
||||
get_method = "get_%s" % attrib.lower()
|
||||
get_method2 = "get%s" % attrib.title()
|
||||
|
||||
@@ -752,7 +751,7 @@ class ElementBase(object):
|
||||
if lang and attrib in self.lang_interfaces:
|
||||
kwargs['lang'] = lang
|
||||
|
||||
if attrib in self.interfaces:
|
||||
if attrib in self.interfaces or attrib == 'lang':
|
||||
if value is not None:
|
||||
set_method = "set_%s" % attrib.lower()
|
||||
set_method2 = "set%s" % attrib.title()
|
||||
@@ -838,7 +837,7 @@ class ElementBase(object):
|
||||
if lang and attrib in self.lang_interfaces:
|
||||
kwargs['lang'] = lang
|
||||
|
||||
if attrib in self.interfaces:
|
||||
if attrib in self.interfaces or attrib == 'lang':
|
||||
del_method = "del_%s" % attrib.lower()
|
||||
del_method2 = "del%s" % attrib.title()
|
||||
|
||||
@@ -973,10 +972,6 @@ class ElementBase(object):
|
||||
:param keep: Indicates if the element should be kept if its text is
|
||||
removed. Defaults to False.
|
||||
"""
|
||||
path = self._fix_ns(name, split=True)
|
||||
element = self.xml.find(name)
|
||||
parent = self.xml
|
||||
|
||||
default_lang = self.get_lang()
|
||||
if lang is None:
|
||||
lang = default_lang
|
||||
@@ -984,32 +979,51 @@ class ElementBase(object):
|
||||
if not text and not keep:
|
||||
return self._del_sub(name, lang=lang)
|
||||
|
||||
if element is None:
|
||||
# We need to add the element. If the provided name was
|
||||
# an XPath expression, some of the intermediate elements
|
||||
# may already exist. If so, we want to use those instead
|
||||
# of generating new elements.
|
||||
last_xml = self.xml
|
||||
walked = []
|
||||
for ename in path:
|
||||
walked.append(ename)
|
||||
element = self.xml.find("/".join(walked))
|
||||
if element is None:
|
||||
element = ET.Element(ename)
|
||||
if lang:
|
||||
element.attrib['{%s}lang' % XML_NS] = lang
|
||||
last_xml.append(element)
|
||||
parent = last_xml
|
||||
last_xml = element
|
||||
element = last_xml
|
||||
path = self._fix_ns(name, split=True)
|
||||
name = path[-1]
|
||||
parent = self.xml
|
||||
|
||||
if lang:
|
||||
if element.attrib.get('{%s}lang' % XML_NS, default_lang) != lang:
|
||||
element = ET.Element(ename)
|
||||
element.attrib['{%s}lang' % XML_NS] = lang
|
||||
parent.append(element)
|
||||
# The first goal is to find the parent of the subelement, or, if
|
||||
# we can't find that, the closest grandparent element.
|
||||
missing_path = []
|
||||
search_order = path[:-1]
|
||||
while search_order:
|
||||
parent = self.xml.find('/'.join(search_order))
|
||||
ename = search_order.pop()
|
||||
if parent is not None:
|
||||
break
|
||||
else:
|
||||
missing_path.append(ename)
|
||||
missing_path.reverse()
|
||||
|
||||
# Find all existing elements that match the desired
|
||||
# element path (there may be multiples due to different
|
||||
# languages values).
|
||||
if parent is not None:
|
||||
elements = self.xml.findall('/'.join(path))
|
||||
else:
|
||||
parent = self.xml
|
||||
elements = []
|
||||
|
||||
# Insert the remaining grandparent elements that don't exist yet.
|
||||
for ename in missing_path:
|
||||
element = ET.Element(ename)
|
||||
parent.append(element)
|
||||
parent = element
|
||||
|
||||
# Re-use an existing element with the proper language, if one exists.
|
||||
for element in elements:
|
||||
elang = element.attrib.get('{%s}lang' % XML_NS, default_lang)
|
||||
if not lang and elang == default_lang or lang and lang == elang:
|
||||
element.text = text
|
||||
return element
|
||||
|
||||
# No useable element exists, so create a new one.
|
||||
element = ET.Element(name)
|
||||
element.text = text
|
||||
if lang and lang != default_lang:
|
||||
element.attrib['{%s}lang' % XML_NS] = lang
|
||||
parent.append(element)
|
||||
return element
|
||||
|
||||
def _set_all_sub_text(self, name, values, keep=False, lang=None):
|
||||
@@ -1184,6 +1198,7 @@ class ElementBase(object):
|
||||
out = []
|
||||
out += [x for x in self.interfaces]
|
||||
out += [x for x in self.loaded_plugins]
|
||||
out.append('lang')
|
||||
if self.iterables:
|
||||
out.append('substanzas')
|
||||
return out
|
||||
@@ -1263,7 +1278,7 @@ class ElementBase(object):
|
||||
"""
|
||||
return "{%s}%s" % (cls.namespace, cls.name)
|
||||
|
||||
def get_lang(self):
|
||||
def get_lang(self, lang=None):
|
||||
result = self.xml.attrib.get('{%s}lang' % XML_NS, '')
|
||||
if not result and self.parent and self.parent():
|
||||
return self.parent()['lang']
|
||||
|
@@ -63,9 +63,11 @@ def tostring(xml=None, xmlns='', stanza_ns='', stream=None,
|
||||
|
||||
default_ns = ''
|
||||
stream_ns = ''
|
||||
use_cdata = False
|
||||
if stream:
|
||||
default_ns = stream.default_ns
|
||||
stream_ns = stream.stream_ns
|
||||
use_cdata = stream.use_cdata
|
||||
|
||||
# Output the tag name and derived namespace of the element.
|
||||
namespace = ''
|
||||
@@ -81,7 +83,7 @@ def tostring(xml=None, xmlns='', stanza_ns='', stream=None,
|
||||
|
||||
# Output escaped attribute values.
|
||||
for attrib, value in xml.attrib.items():
|
||||
value = xml_escape(value)
|
||||
value = escape(value, use_cdata)
|
||||
if '}' not in attrib:
|
||||
output.append(' %s="%s"' % (attrib, value))
|
||||
else:
|
||||
@@ -105,24 +107,24 @@ def tostring(xml=None, xmlns='', stanza_ns='', stream=None,
|
||||
# If there are additional child elements to serialize.
|
||||
output.append(">")
|
||||
if xml.text:
|
||||
output.append(xml_escape(xml.text))
|
||||
output.append(escape(xml.text, use_cdata))
|
||||
if len(xml):
|
||||
for child in xml:
|
||||
output.append(tostring(child, tag_xmlns, stanza_ns, stream))
|
||||
output.append("</%s>" % tag_name)
|
||||
elif xml.text:
|
||||
# If we only have text content.
|
||||
output.append(">%s</%s>" % (xml_escape(xml.text), tag_name))
|
||||
output.append(">%s</%s>" % (escape(xml.text, use_cdata), tag_name))
|
||||
else:
|
||||
# Empty element.
|
||||
output.append(" />")
|
||||
if xml.tail:
|
||||
# If there is additional text after the element.
|
||||
output.append(xml_escape(xml.tail))
|
||||
output.append(escape(xml.tail, use_cdata))
|
||||
return ''.join(output)
|
||||
|
||||
|
||||
def xml_escape(text):
|
||||
def escape(text, use_cdata=False):
|
||||
"""Convert special characters in XML to escape sequences.
|
||||
|
||||
:param string text: The XML text to convert.
|
||||
@@ -132,12 +134,24 @@ def xml_escape(text):
|
||||
if type(text) != types.UnicodeType:
|
||||
text = unicode(text, 'utf-8', 'ignore')
|
||||
|
||||
text = list(text)
|
||||
escapes = {'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
"'": ''',
|
||||
'"': '"'}
|
||||
for i, c in enumerate(text):
|
||||
text[i] = escapes.get(c, c)
|
||||
return ''.join(text)
|
||||
|
||||
if not use_cdata:
|
||||
text = list(text)
|
||||
for i, c in enumerate(text):
|
||||
text[i] = escapes.get(c, c)
|
||||
return ''.join(text)
|
||||
else:
|
||||
escape_needed = False
|
||||
for c in text:
|
||||
if c in escapes:
|
||||
escape_needed = True
|
||||
break
|
||||
if escape_needed:
|
||||
escaped = map(lambda x : "<![CDATA[%s]]>" % x, text.split("]]>"))
|
||||
return "<![CDATA[]]]><![CDATA[]>]]>".join(escaped)
|
||||
return text
|
||||
|
@@ -26,14 +26,11 @@ import time
|
||||
import random
|
||||
import weakref
|
||||
import uuid
|
||||
try:
|
||||
import queue
|
||||
except ImportError:
|
||||
import Queue as queue
|
||||
|
||||
from xml.parsers.expat import ExpatError
|
||||
|
||||
import sleekxmpp
|
||||
from sleekxmpp.util import Queue, QueueEmpty
|
||||
from sleekxmpp.thirdparty.statemachine import StateMachine
|
||||
from sleekxmpp.xmlstream import Scheduler, tostring, cert
|
||||
from sleekxmpp.xmlstream.stanzabase import StanzaBase, ET, ElementBase
|
||||
@@ -215,6 +212,10 @@ class XMLStream(object):
|
||||
#: If set to ``True``, attempt to use IPv6.
|
||||
self.use_ipv6 = True
|
||||
|
||||
#: Use CDATA for escaping instead of XML entities. Defaults
|
||||
#: to ``False``.
|
||||
self.use_cdata = False
|
||||
|
||||
#: An optional dictionary of proxy settings. It may provide:
|
||||
#: :host: The host offering proxy services.
|
||||
#: :port: The port for the proxy service.
|
||||
@@ -270,10 +271,10 @@ class XMLStream(object):
|
||||
self.end_session_on_disconnect = True
|
||||
|
||||
#: A queue of stream, custom, and scheduled events to be processed.
|
||||
self.event_queue = queue.Queue()
|
||||
self.event_queue = Queue()
|
||||
|
||||
#: A queue of string data to be sent over the stream.
|
||||
self.send_queue = queue.Queue()
|
||||
self.send_queue = Queue()
|
||||
self.send_queue_lock = threading.Lock()
|
||||
self.send_lock = threading.RLock()
|
||||
|
||||
@@ -467,7 +468,7 @@ class XMLStream(object):
|
||||
log.debug("No remaining DNS records to try.")
|
||||
self.dns_answers = None
|
||||
if reattempt:
|
||||
self.reconnect_delay = delay
|
||||
self.reconnect_delay = None
|
||||
return False
|
||||
|
||||
af = Socket.AF_INET
|
||||
@@ -537,8 +538,8 @@ class XMLStream(object):
|
||||
try:
|
||||
cert.verify(self._expected_server_name, self._der_cert)
|
||||
except cert.CertificateError as err:
|
||||
log.error(err.message)
|
||||
if not self.event_handled('ssl_invalid_cert'):
|
||||
log.error(err.message)
|
||||
self.disconnect(send_close=False)
|
||||
else:
|
||||
self.event('ssl_invalid_cert',
|
||||
@@ -828,8 +829,8 @@ class XMLStream(object):
|
||||
try:
|
||||
cert.verify(self._expected_server_name, self._der_cert)
|
||||
except cert.CertificateError as err:
|
||||
log.error(err.message)
|
||||
if not self.event_handled('ssl_invalid_cert'):
|
||||
log.error(err.message)
|
||||
self.disconnect(self.auto_reconnect, send_close=False)
|
||||
else:
|
||||
self.event('ssl_invalid_cert', pem_cert, direct=True)
|
||||
@@ -954,6 +955,10 @@ class XMLStream(object):
|
||||
else:
|
||||
self.__filters[mode].append(handler)
|
||||
|
||||
def del_filter(self, mode, handler):
|
||||
"""Remove an incoming or outgoing filter."""
|
||||
self.__filters[mode].remove(handler)
|
||||
|
||||
def add_handler(self, mask, pointer, name=None, disposable=False,
|
||||
threaded=False, filter=False, instream=False):
|
||||
"""A shortcut method for registering a handler using XML masks.
|
||||
@@ -1582,7 +1587,7 @@ class XMLStream(object):
|
||||
try:
|
||||
wait = self.wait_timeout
|
||||
event = self.event_queue.get(True, timeout=wait)
|
||||
except queue.Empty:
|
||||
except QueueEmpty:
|
||||
event = None
|
||||
if event is None:
|
||||
continue
|
||||
@@ -1651,7 +1656,7 @@ class XMLStream(object):
|
||||
else:
|
||||
try:
|
||||
data = self.send_queue.get(True, 1)
|
||||
except queue.Empty:
|
||||
except QueueEmpty:
|
||||
continue
|
||||
log.debug("SEND: %s", data)
|
||||
enc_data = data.encode('utf-8')
|
||||
|
@@ -1,5 +1,5 @@
|
||||
from sleekxmpp.test import *
|
||||
from sleekxmpp.xmlstream.jid import JID
|
||||
from sleekxmpp import JID, InvalidJID
|
||||
|
||||
|
||||
class TestJIDClass(SleekTest):
|
||||
@@ -137,5 +137,146 @@ class TestJIDClass(SleekTest):
|
||||
self.assertFalse(jid1 == jid2, "Same JIDs are not considered equal")
|
||||
self.assertTrue(jid1 != jid2, "Same JIDs are considered not equal")
|
||||
|
||||
def testZeroLengthDomain(self):
|
||||
self.assertRaises(InvalidJID, JID, domain='')
|
||||
self.assertRaises(InvalidJID, JID, 'user@/resource')
|
||||
|
||||
def testZeroLengthLocalPart(self):
|
||||
self.assertRaises(InvalidJID, JID, local='', domain='test.com')
|
||||
self.assertRaises(InvalidJID, JID, '@/test.com')
|
||||
|
||||
def testZeroLengthResource(self):
|
||||
self.assertRaises(InvalidJID, JID, domain='test.com', resource='')
|
||||
self.assertRaises(InvalidJID, JID, 'test.com/')
|
||||
|
||||
def test1023LengthDomain(self):
|
||||
domain = ('a.' * 509) + 'a.com'
|
||||
jid1 = JID(domain=domain)
|
||||
jid2 = JID('user@%s/resource' % domain)
|
||||
|
||||
def test1023LengthLocalPart(self):
|
||||
local = 'a' * 1023
|
||||
jid1 = JID(local=local, domain='test.com')
|
||||
jid2 = JID('%s@test.com' % local)
|
||||
|
||||
def test1023LengthResource(self):
|
||||
resource = 'r' * 1023
|
||||
jid1 = JID(domain='test.com', resource=resource)
|
||||
jid2 = JID('test.com/%s' % resource)
|
||||
|
||||
def test1024LengthDomain(self):
|
||||
domain = ('a.' * 509) + 'aa.com'
|
||||
self.assertRaises(InvalidJID, JID, domain=domain)
|
||||
self.assertRaises(InvalidJID, JID, 'user@%s/resource' % domain)
|
||||
|
||||
def test1024LengthLocalPart(self):
|
||||
local = 'a' * 1024
|
||||
self.assertRaises(InvalidJID, JID, local=local, domain='test.com')
|
||||
self.assertRaises(InvalidJID, JID, '%s@/test.com' % local)
|
||||
|
||||
def test1024LengthResource(self):
|
||||
resource = 'r' * 1024
|
||||
self.assertRaises(InvalidJID, JID, domain='test.com', resource=resource)
|
||||
self.assertRaises(InvalidJID, JID, 'test.com/%s' % resource)
|
||||
|
||||
def testTooLongDomainLabel(self):
|
||||
domain = ('a' * 64) + '.com'
|
||||
self.assertRaises(InvalidJID, JID, domain=domain)
|
||||
self.assertRaises(InvalidJID, JID, 'user@%s/resource' % domain)
|
||||
|
||||
def testDomainEmptyLabel(self):
|
||||
domain = 'aaa..bbb.com'
|
||||
self.assertRaises(InvalidJID, JID, domain=domain)
|
||||
self.assertRaises(InvalidJID, JID, 'user@%s/resource' % domain)
|
||||
|
||||
def testDomainIPv4(self):
|
||||
domain = '127.0.0.1'
|
||||
jid1 = JID(domain=domain)
|
||||
jid2 = JID('user@%s/resource' % domain)
|
||||
|
||||
def testDomainIPv6(self):
|
||||
domain = '[::1]'
|
||||
jid1 = JID(domain=domain)
|
||||
jid2 = JID('user@%s/resource' % domain)
|
||||
|
||||
def testDomainInvalidIPv6NoBrackets(self):
|
||||
domain = '::1'
|
||||
jid1 = JID(domain=domain)
|
||||
jid2 = JID('user@%s/resource' % domain)
|
||||
|
||||
self.assertEqual(jid1.domain, '[::1]')
|
||||
self.assertEqual(jid2.domain, '[::1]')
|
||||
|
||||
def testDomainInvalidIPv6MissingBracket(self):
|
||||
domain = '[::1'
|
||||
jid1 = JID(domain=domain)
|
||||
jid2 = JID('user@%s/resource' % domain)
|
||||
|
||||
self.assertEqual(jid1.domain, '[::1]')
|
||||
self.assertEqual(jid2.domain, '[::1]')
|
||||
|
||||
def testDomainWithPort(self):
|
||||
domain = 'example.com:5555'
|
||||
self.assertRaises(InvalidJID, JID, domain=domain)
|
||||
self.assertRaises(InvalidJID, JID, 'user@%s/resource' % domain)
|
||||
|
||||
def testDomainWithTrailingDot(self):
|
||||
domain = 'example.com.'
|
||||
jid1 = JID(domain=domain)
|
||||
jid2 = JID('user@%s/resource' % domain)
|
||||
|
||||
self.assertEqual(jid1.domain, 'example.com')
|
||||
self.assertEqual(jid2.domain, 'example.com')
|
||||
|
||||
def testDomainWithDashes(self):
|
||||
domain = 'example.com-'
|
||||
self.assertRaises(InvalidJID, JID, domain=domain)
|
||||
self.assertRaises(InvalidJID, JID, 'user@%s/resource' % domain)
|
||||
|
||||
domain = '-example.com'
|
||||
self.assertRaises(InvalidJID, JID, domain=domain)
|
||||
self.assertRaises(InvalidJID, JID, 'user@%s/resource' % domain)
|
||||
|
||||
def testACEDomain(self):
|
||||
domain = 'xn--bcher-kva.ch'
|
||||
jid1 = JID(domain=domain)
|
||||
jid2 = JID('user@%s/resource' % domain)
|
||||
|
||||
self.assertEqual(jid1.domain.encode('utf-8'), b'b\xc3\xbccher.ch')
|
||||
self.assertEqual(jid2.domain.encode('utf-8'), b'b\xc3\xbccher.ch')
|
||||
|
||||
def testJIDEscapeExistingSequences(self):
|
||||
jid = JID(local='blah\\foo\\20bar', domain='example.com')
|
||||
self.assertEqual(jid.local, 'blah\\foo\\5c20bar')
|
||||
|
||||
def testJIDEscape(self):
|
||||
jid = JID(local='here\'s_a_wild_&_/cr%zy/_address_for:<wv>("IMPS")',
|
||||
domain='example.com')
|
||||
self.assertEqual(jid.local, r'here\27s_a_wild_\26_\2fcr%zy\2f_address_for\3a\3cwv\3e(\22IMPS\22)')
|
||||
|
||||
def testJIDUnescape(self):
|
||||
jid = JID(local='here\'s_a_wild_&_/cr%zy/_address_for:<wv>("IMPS")',
|
||||
domain='example.com')
|
||||
ujid = jid.unescape()
|
||||
self.assertEqual(ujid.local, 'here\'s_a_wild_&_/cr%zy/_address_for:<wv>("IMPS")')
|
||||
|
||||
jid = JID(local='blah\\foo\\20bar', domain='example.com')
|
||||
ujid = jid.unescape()
|
||||
self.assertEqual(ujid.local, 'blah\\foo\\20bar')
|
||||
|
||||
def testStartOrEndWithEscapedSpaces(self):
|
||||
local = ' foo'
|
||||
self.assertRaises(InvalidJID, JID, local=local, domain='example.com')
|
||||
self.assertRaises(InvalidJID, JID, '%s@example.com' % local)
|
||||
|
||||
local = 'bar '
|
||||
self.assertRaises(InvalidJID, JID, local=local, domain='example.com')
|
||||
self.assertRaises(InvalidJID, JID, '%s@example.com' % local)
|
||||
|
||||
# Need more input for these cases. A JID starting with \20 *is* valid
|
||||
# according to RFC 6122, but is not according to XEP-0106.
|
||||
#self.assertRaises(InvalidJID, JID, '%s@example.com' % '\\20foo2')
|
||||
#self.assertRaises(InvalidJID, JID, '%s@example.com' % 'bar2\\20')
|
||||
|
||||
|
||||
suite = unittest.TestLoader().loadTestsFromTestCase(TestJIDClass)
|
||||
|
@@ -1,5 +1,6 @@
|
||||
from sleekxmpp.test import *
|
||||
from sleekxmpp.xmlstream.stanzabase import ElementBase
|
||||
from sleekxmpp.thirdparty import OrderedDict
|
||||
|
||||
|
||||
class TestElementBase(SleekTest):
|
||||
@@ -760,7 +761,7 @@ class TestElementBase(SleekTest):
|
||||
<foo xmlns="foo" />
|
||||
""")
|
||||
|
||||
self.assertFalse(stanza['bar'],
|
||||
self.assertFalse(stanza['bar'],
|
||||
"Returned True for missing bool interface element.")
|
||||
|
||||
stanza['bar'] = True
|
||||
@@ -797,7 +798,7 @@ class TestElementBase(SleekTest):
|
||||
namespace = 'baz'
|
||||
plugin_attrib = name
|
||||
plugin_multi_attrib = 'bazs'
|
||||
|
||||
|
||||
register_stanza_plugin(TestStanza, TestMultiStanza1, iterable=True)
|
||||
register_stanza_plugin(TestStanza, TestMultiStanza2, iterable=True)
|
||||
|
||||
@@ -829,9 +830,9 @@ class TestElementBase(SleekTest):
|
||||
<baz xmlns="baz" />
|
||||
""")
|
||||
|
||||
self.assertEqual(len(bars), 2,
|
||||
self.assertEqual(len(bars), 2,
|
||||
"Wrong number of <bar /> stanzas: %s" % len(bars))
|
||||
self.assertEqual(len(bazs), 2,
|
||||
self.assertEqual(len(bazs), 2,
|
||||
"Wrong number of <baz /> stanzas: %s" % len(bazs))
|
||||
|
||||
def testSetMultiAttrib(self):
|
||||
@@ -853,7 +854,7 @@ class TestElementBase(SleekTest):
|
||||
namespace = 'baz'
|
||||
plugin_attrib = name
|
||||
plugin_multi_attrib = 'bazs'
|
||||
|
||||
|
||||
register_stanza_plugin(TestStanza, TestMultiStanza1, iterable=True)
|
||||
register_stanza_plugin(TestStanza, TestMultiStanza2, iterable=True)
|
||||
|
||||
@@ -906,7 +907,7 @@ class TestElementBase(SleekTest):
|
||||
namespace = 'baz'
|
||||
plugin_attrib = name
|
||||
plugin_multi_attrib = 'bazs'
|
||||
|
||||
|
||||
register_stanza_plugin(TestStanza, TestMultiStanza1, iterable=True)
|
||||
register_stanza_plugin(TestStanza, TestMultiStanza2, iterable=True)
|
||||
|
||||
@@ -938,5 +939,313 @@ class TestElementBase(SleekTest):
|
||||
self.assertEqual(len(stanza['substanzas']), 2,
|
||||
"Wrong number of substanzas: %s" % len(stanza['substanzas']))
|
||||
|
||||
def testDefaultLang(self):
|
||||
"""Test setting a normal subinterface when a default language is set"""
|
||||
|
||||
class TestStanza(ElementBase):
|
||||
name = 'foo'
|
||||
namespace = 'test'
|
||||
interfaces = set(['test'])
|
||||
sub_interfaces = interfaces
|
||||
lang_interfaces = interfaces
|
||||
|
||||
stanza = TestStanza()
|
||||
stanza['lang'] = 'sv'
|
||||
stanza['test'] = 'hej'
|
||||
|
||||
self.check(stanza, """
|
||||
<foo xmlns="test" xml:lang="sv">
|
||||
<test>hej</test>
|
||||
</foo>
|
||||
""")
|
||||
|
||||
self.assertEqual(stanza['test'], 'hej',
|
||||
"Incorrect subinterface value: %s" % stanza['test'])
|
||||
|
||||
self.assertEqual(stanza['test|sv'], 'hej',
|
||||
"Incorrect subinterface value: %s" % stanza['test|sv'])
|
||||
|
||||
def testSpecifyLangWithDefault(self):
|
||||
"""Test specifying various languages."""
|
||||
|
||||
class TestStanza(ElementBase):
|
||||
name = 'foo'
|
||||
namespace = 'test'
|
||||
interfaces = set(['test'])
|
||||
sub_interfaces = interfaces
|
||||
lang_interfaces = interfaces
|
||||
|
||||
stanza = TestStanza()
|
||||
stanza['lang'] = 'sv'
|
||||
stanza['test'] = 'hej'
|
||||
stanza['test|en'] = 'hi'
|
||||
stanza['test|es'] = 'hola'
|
||||
|
||||
self.check(stanza, """
|
||||
<foo xmlns="test" xml:lang="sv">
|
||||
<test>hej</test>
|
||||
<test xml:lang="en">hi</test>
|
||||
<test xml:lang="es">hola</test>
|
||||
</foo>
|
||||
""")
|
||||
|
||||
self.assertEqual(stanza['test'], 'hej',
|
||||
"Incorrect subinterface value: %s" % stanza['test'])
|
||||
|
||||
self.assertEqual(stanza['test|sv'], 'hej',
|
||||
"Incorrect subinterface value: %s" % stanza['test|sv'])
|
||||
|
||||
self.assertEqual(stanza['test|en'], 'hi',
|
||||
"Incorrect subinterface value: %s" % stanza['test|en'])
|
||||
|
||||
self.assertEqual(stanza['test|es'], 'hola',
|
||||
"Incorrect subinterface value: %s" % stanza['test|es'])
|
||||
|
||||
def testSpecifyLangWithNoDefault(self):
|
||||
"""Test specifying various languages."""
|
||||
|
||||
class TestStanza(ElementBase):
|
||||
name = 'foo'
|
||||
namespace = 'test'
|
||||
interfaces = set(['test'])
|
||||
sub_interfaces = interfaces
|
||||
lang_interfaces = interfaces
|
||||
|
||||
stanza = TestStanza()
|
||||
stanza['test'] = 'hej'
|
||||
stanza['test|en'] = 'hi'
|
||||
stanza['test|es'] = 'hola'
|
||||
|
||||
self.check(stanza, """
|
||||
<foo xmlns="test">
|
||||
<test>hej</test>
|
||||
<test xml:lang="en">hi</test>
|
||||
<test xml:lang="es">hola</test>
|
||||
</foo>
|
||||
""")
|
||||
|
||||
self.assertEqual(stanza['test'], 'hej',
|
||||
"Incorrect subinterface value: %s" % stanza['test'])
|
||||
|
||||
self.assertEqual(stanza['test|en'], 'hi',
|
||||
"Incorrect subinterface value: %s" % stanza['test|en'])
|
||||
|
||||
self.assertEqual(stanza['test|es'], 'hola',
|
||||
"Incorrect subinterface value: %s" % stanza['test|es'])
|
||||
|
||||
def testModifyLangInterfaceWithDefault(self):
|
||||
"""Test resetting an interface when a default lang is used."""
|
||||
|
||||
class TestStanza(ElementBase):
|
||||
name = 'foo'
|
||||
namespace = 'test'
|
||||
interfaces = set(['test'])
|
||||
sub_interfaces = interfaces
|
||||
lang_interfaces = interfaces
|
||||
|
||||
stanza = TestStanza()
|
||||
stanza['lang'] = 'es'
|
||||
stanza['test'] = 'hola'
|
||||
stanza['test|en'] = 'hi'
|
||||
|
||||
self.check(stanza, """
|
||||
<foo xmlns="test" xml:lang="es">
|
||||
<test>hola</test>
|
||||
<test xml:lang="en">hi</test>
|
||||
</foo>
|
||||
""")
|
||||
|
||||
stanza['test'] = 'adios'
|
||||
stanza['test|en'] = 'bye'
|
||||
|
||||
self.check(stanza, """
|
||||
<foo xmlns="test" xml:lang="es">
|
||||
<test>adios</test>
|
||||
<test xml:lang="en">bye</test>
|
||||
</foo>
|
||||
""")
|
||||
|
||||
self.assertEqual(stanza['test'], 'adios',
|
||||
"Incorrect subinterface value: %s" % stanza['test'])
|
||||
|
||||
self.assertEqual(stanza['test|es'], 'adios',
|
||||
"Incorrect subinterface value: %s" % stanza['test|es'])
|
||||
|
||||
self.assertEqual(stanza['test|en'], 'bye',
|
||||
"Incorrect subinterface value: %s" % stanza['test|en'])
|
||||
|
||||
stanza['test|es'] = 'hola'
|
||||
|
||||
self.check(stanza, """
|
||||
<foo xmlns="test" xml:lang="es">
|
||||
<test>hola</test>
|
||||
<test xml:lang="en">bye</test>
|
||||
</foo>
|
||||
""")
|
||||
|
||||
self.assertEqual(stanza['test'], 'hola',
|
||||
"Incorrect subinterface value: %s" % stanza['test'])
|
||||
|
||||
self.assertEqual(stanza['test|es'], 'hola',
|
||||
"Incorrect subinterface value: %s" % stanza['test|es'])
|
||||
|
||||
def testModifyLangInterfaceWithNoDefault(self):
|
||||
"""Test resetting an interface when no default lang is used."""
|
||||
|
||||
class TestStanza(ElementBase):
|
||||
name = 'foo'
|
||||
namespace = 'test'
|
||||
interfaces = set(['test'])
|
||||
sub_interfaces = interfaces
|
||||
lang_interfaces = interfaces
|
||||
|
||||
stanza = TestStanza()
|
||||
stanza['test'] = 'hola'
|
||||
stanza['test|en'] = 'hi'
|
||||
|
||||
self.check(stanza, """
|
||||
<foo xmlns="test">
|
||||
<test>hola</test>
|
||||
<test xml:lang="en">hi</test>
|
||||
</foo>
|
||||
""")
|
||||
|
||||
stanza['test'] = 'adios'
|
||||
stanza['test|en'] = 'bye'
|
||||
|
||||
self.check(stanza, """
|
||||
<foo xmlns="test">
|
||||
<test>adios</test>
|
||||
<test xml:lang="en">bye</test>
|
||||
</foo>
|
||||
""")
|
||||
|
||||
self.assertEqual(stanza['test'], 'adios',
|
||||
"Incorrect subinterface value: %s" % stanza['test'])
|
||||
|
||||
self.assertEqual(stanza['test'], 'adios',
|
||||
"Incorrect subinterface value: %s" % stanza['test|es'])
|
||||
|
||||
self.assertEqual(stanza['test|en'], 'bye',
|
||||
"Incorrect subinterface value: %s" % stanza['test|en'])
|
||||
|
||||
def testDelInterfacesWithDefaultLang(self):
|
||||
"""Test deleting interfaces with a default lang set."""
|
||||
|
||||
class TestStanza(ElementBase):
|
||||
name = 'foo'
|
||||
namespace = 'test'
|
||||
interfaces = set(['test'])
|
||||
sub_interfaces = interfaces
|
||||
lang_interfaces = interfaces
|
||||
|
||||
stanza = TestStanza()
|
||||
stanza['lang'] = 'en'
|
||||
stanza['test'] = 'hi'
|
||||
stanza['test|no'] = 'hej'
|
||||
stanza['test|fr'] = 'bonjour'
|
||||
|
||||
self.check(stanza, """
|
||||
<foo xmlns="test" xml:lang="en">
|
||||
<test>hi</test>
|
||||
<test xml:lang="no">hej</test>
|
||||
<test xml:lang="fr">bonjour</test>
|
||||
</foo>
|
||||
""")
|
||||
|
||||
del stanza['test']
|
||||
|
||||
self.check(stanza, """
|
||||
<foo xmlns="test" xml:lang="en">
|
||||
<test xml:lang="no">hej</test>
|
||||
<test xml:lang="fr">bonjour</test>
|
||||
</foo>
|
||||
""")
|
||||
|
||||
del stanza['test|no']
|
||||
|
||||
self.check(stanza, """
|
||||
<foo xmlns="test" xml:lang="en">
|
||||
<test xml:lang="fr">bonjour</test>
|
||||
</foo>
|
||||
""")
|
||||
|
||||
def testDelInterfacesWithNoDefaultLang(self):
|
||||
"""Test deleting interfaces with no default lang set."""
|
||||
|
||||
class TestStanza(ElementBase):
|
||||
name = 'foo'
|
||||
namespace = 'test'
|
||||
interfaces = set(['test'])
|
||||
sub_interfaces = interfaces
|
||||
lang_interfaces = interfaces
|
||||
|
||||
stanza = TestStanza()
|
||||
stanza['test'] = 'hi'
|
||||
stanza['test|no'] = 'hej'
|
||||
stanza['test|fr'] = 'bonjour'
|
||||
|
||||
self.check(stanza, """
|
||||
<foo xmlns="test">
|
||||
<test>hi</test>
|
||||
<test xml:lang="no">hej</test>
|
||||
<test xml:lang="fr">bonjour</test>
|
||||
</foo>
|
||||
""")
|
||||
|
||||
del stanza['test']
|
||||
|
||||
self.check(stanza, """
|
||||
<foo xmlns="test">
|
||||
<test xml:lang="no">hej</test>
|
||||
<test xml:lang="fr">bonjour</test>
|
||||
</foo>
|
||||
""")
|
||||
|
||||
del stanza['test|no']
|
||||
|
||||
self.check(stanza, """
|
||||
<foo xmlns="test">
|
||||
<test xml:lang="fr">bonjour</test>
|
||||
</foo>
|
||||
""")
|
||||
|
||||
def testStarLang(self):
|
||||
"""Test using interface|*."""
|
||||
|
||||
class TestStanza(ElementBase):
|
||||
name = 'foo'
|
||||
namespace = 'test'
|
||||
interfaces = set(['test'])
|
||||
sub_interfaces = interfaces
|
||||
lang_interfaces = interfaces
|
||||
|
||||
data = OrderedDict()
|
||||
data['en'] = 'hi'
|
||||
data['fr'] = 'bonjour'
|
||||
data['no'] = 'hej'
|
||||
|
||||
stanza = TestStanza()
|
||||
stanza['test|*'] = data
|
||||
|
||||
self.check(stanza, """
|
||||
<foo xmlns="test">
|
||||
<test xml:lang="en">hi</test>
|
||||
<test xml:lang="fr">bonjour</test>
|
||||
<test xml:lang="no">hej</test>
|
||||
</foo>
|
||||
""")
|
||||
|
||||
data2 = stanza['test|*']
|
||||
|
||||
self.assertEqual(data, data2,
|
||||
"Did not extract expected language data: %s" % data2)
|
||||
|
||||
del stanza['test|*']
|
||||
|
||||
self.check(stanza, """
|
||||
<foo xmlns="test" />
|
||||
""")
|
||||
|
||||
|
||||
suite = unittest.TestLoader().loadTestsFromTestCase(TestElementBase)
|
||||
|
@@ -1,7 +1,7 @@
|
||||
from sleekxmpp.test import *
|
||||
from sleekxmpp.stanza import Message
|
||||
from sleekxmpp.xmlstream.stanzabase import ET, ElementBase
|
||||
from sleekxmpp.xmlstream.tostring import tostring, xml_escape
|
||||
from sleekxmpp.xmlstream.tostring import tostring, escape
|
||||
|
||||
|
||||
class TestToString(SleekTest):
|
||||
@@ -30,7 +30,7 @@ class TestToString(SleekTest):
|
||||
def testXMLEscape(self):
|
||||
"""Test escaping XML special characters."""
|
||||
original = """<foo bar="baz">'Hi & welcome!'</foo>"""
|
||||
escaped = xml_escape(original)
|
||||
escaped = escape(original)
|
||||
desired = """<foo bar="baz">'Hi"""
|
||||
desired += """ & welcome!'</foo>"""
|
||||
|
||||
|
Reference in New Issue
Block a user