Merge branch 'develop' into roster

Conflicts:
	setup.py
	sleekxmpp/clientxmpp.py
This commit is contained in:
Lance Stout 2011-08-12 16:47:58 -07:00
commit 484efff156
46 changed files with 1747 additions and 1111 deletions

2
.gitignore vendored
View file

@ -1,2 +1,4 @@
*.pyc *.pyc
build/ build/
dist/
MANIFEST

121
LICENSE
View file

@ -17,3 +17,124 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE. THE SOFTWARE.
Licences of Bundled Third Pary Code
-----------------------------------
dateutil - Extensions to the standard python 2.3+ datetime module.
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Copyright (c) 2003-2011 - Gustavo Niemeyer <gustavo@niemeyer.net>
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
fixed_datetime
~~~~~~~~~~~~~~
Copyright (c) 2008, Red Innovation Ltd., Finland
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
* Neither the name of Red Innovation nor the names of its contributors
may be used to endorse or promote products derived from this software
without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY RED INNOVATION ``AS IS'' AND ANY
EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL RED INNOVATION BE LIABLE FOR ANY
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
OrderedDict - A port of the Python 2.7+ OrderedDict to Python 2.6
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Copyright (c) 2009 Raymond Hettinger
Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation files
(the "Software"), to deal in the Software without restriction,
including without limitation the rights to use, copy, modify, merge,
publish, distribute, sublicense, and/or sell copies of the Software,
and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
SUELTA A PURE-PYTHON SASL CLIENT LIBRARY
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
This software is subject to "The MIT License"
Copyright 2007-2010 David Alan Cridland
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

149
examples/send_client.py Executable file
View file

@ -0,0 +1,149 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
SleekXMPP: The Sleek XMPP Library
Copyright (C) 2010 Nathanael C. Fritz
This file is part of SleekXMPP.
See the file LICENSE for copying permission.
"""
import sys
import logging
import time
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')
class SendMsgBot(sleekxmpp.ClientXMPP):
"""
A simple SleekXMPP bot that will echo messages it
receives, along with a short thank you message.
"""
def __init__(self, jid, password):
sleekxmpp.ClientXMPP.__init__(self, jid, password)
# The session_start event will be triggered when
# the bot establishes its connection with the server
# and the XML streams are ready for use. We want to
# listen for this event so that we we can intialize
# our roster.
self.add_event_handler("session_start", self.start)
# The message event is triggered whenever a message
# stanza is received. Be aware that that includes
# MUC messages and error messages.
self.add_event_handler("message", self.message)
def start(self, event):
"""
Process the session_start event.
Typical actions for the session_start event are
requesting the roster and broadcasting an intial
presence stanza.
Arguments:
event -- An empty dictionary. The session_start
event does not provide any additional
data.
"""
self.send_presence()
self.get_roster()
msg = self.Message()
msg['to'] = 'user@example.com'
msg['type'] = 'chat'
msg['body'] = "Hello there!"
msg.send()
self.disconnect()
def message(self, msg):
"""
Process incoming message stanzas. Be aware that this also
includes MUC messages and error messages. It is usually
a good idea to check the messages's type before processing
or sending replies.
Arguments:
msg -- The received message stanza. See the documentation
for stanza objects and the Message stanza to see
how it may be used.
"""
#msg.reply("Thanks for sending\n%(body)s" % msg).send()
print "Msg rceived from %(body)s: %(jid)s" % msg
if __name__ == '__main__':
# Setup the command line arguments.
optp = OptionParser()
# Output verbosity options.
optp.add_option('-q', '--quiet', help='set logging to ERROR',
action='store_const', dest='loglevel',
const=logging.ERROR, default=logging.INFO)
optp.add_option('-d', '--debug', help='set logging to DEBUG',
action='store_const', dest='loglevel',
const=logging.DEBUG, default=logging.INFO)
optp.add_option('-v', '--verbose', help='set logging to COMM',
action='store_const', dest='loglevel',
const=5, default=logging.INFO)
# JID and password options.
optp.add_option("-j", "--jid", dest="jid",
help="JID to use")
optp.add_option("-p", "--password", dest="password",
help="password to use")
opts, args = optp.parse_args()
# Setup logging.
logging.basicConfig(level=opts.loglevel,
format='%(levelname)-8s %(message)s')
if opts.jid is None:
opts.jid = raw_input("Username: ")
if opts.password is None:
opts.password = getpass.getpass("Password: ")
# Setup the EchoBot and register plugins. Note that while plugins may
# have interdependencies, the order in which you register them does
# not matter.
xmpp = SendMsgBot(opts.jid, opts.password)
xmpp.register_plugin('xep_0030') # Service Discovery
xmpp.register_plugin('xep_0004') # Data Forms
xmpp.register_plugin('xep_0060') # PubSub
xmpp.register_plugin('xep_0199') # XMPP Ping
# If you are working with an OpenFire server, you may need
# to adjust the SSL version used:
# xmpp.ssl_version = ssl.PROTOCOL_SSLv3
# If you want to verify the SSL certificates offered by a server:
# xmpp.ca_certs = "path/to/ca/cert"
# Connect to the XMPP server and start processing XMPP stanzas.
if xmpp.connect():
# If you do not have the pydns library installed, you will need
# to manually specify the name of the server if it does not match
# the one in the JID. For example, to use Google Talk you would
# need to use:
#
# if xmpp.connect(('talk.google.com', 5222)):
# ...
xmpp.process(threaded=False)
print("Done")
else:
print("Unable to connect.")

View file

@ -1,7 +1,7 @@
#!/usr/bin/env python #!/usr/bin/env python
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
# Copyright (C) 2007-2008 Nathanael C. Fritz # Copyright (C) 2007-2011 Nathanael C. Fritz
# All Rights Reserved # All Rights Reserved
# #
# This software is licensed as described in the README file, # This software is licensed as described in the README file,
@ -29,24 +29,28 @@ import sleekxmpp
VERSION = sleekxmpp.__version__ VERSION = sleekxmpp.__version__
DESCRIPTION = 'SleekXMPP is an elegant Python library for XMPP (aka Jabber, Google Talk, etc).' DESCRIPTION = 'SleekXMPP is an elegant Python library for XMPP (aka Jabber, Google Talk, etc).'
LONG_DESCRIPTION = """ with open('README') as readme:
SleekXMPP is an elegant Python library for XMPP (aka Jabber, Google Talk, etc). LONG_DESCRIPTION = '\n'.join(readme)
"""
CLASSIFIERS = [ 'Intended Audience :: Developers', CLASSIFIERS = [ 'Intended Audience :: Developers',
'License :: OSI Approved :: MIT', 'License :: OSI Approved :: MIT License',
'Programming Language :: Python', 'Programming Language :: Python',
'Programming Language :: Python 2.6',
'Programming Language :: Python 2.7',
'Programming Language :: Python 3.1',
'Programming Language :: Python 3.2',
'Topic :: Software Development :: Libraries :: Python Modules', 'Topic :: Software Development :: Libraries :: Python Modules',
] ]
packages = [ 'sleekxmpp', packages = [ 'sleekxmpp',
'sleekxmpp/stanza', 'sleekxmpp/stanza',
'sleekxmpp/test', 'sleekxmpp/test',
'sleekxmpp/roster'
'sleekxmpp/xmlstream', 'sleekxmpp/xmlstream',
'sleekxmpp/xmlstream/matcher', 'sleekxmpp/xmlstream/matcher',
'sleekxmpp/xmlstream/handler', 'sleekxmpp/xmlstream/handler',
'sleekxmpp/plugins', 'sleekxmpp/plugins',
'sleekxmpp/plugins/xep_0009', 'sleekxmpp/plugins/xep_0009'
'sleekxmpp/plugins/xep_0009/stanza', 'sleekxmpp/plugins/xep_0009/stanza',
'sleekxmpp/plugins/xep_0030', 'sleekxmpp/plugins/xep_0030',
'sleekxmpp/plugins/xep_0030/stanza', 'sleekxmpp/plugins/xep_0030/stanza',
@ -54,11 +58,17 @@ packages = [ 'sleekxmpp',
'sleekxmpp/plugins/xep_0059', 'sleekxmpp/plugins/xep_0059',
'sleekxmpp/plugins/xep_0060', 'sleekxmpp/plugins/xep_0060',
'sleekxmpp/plugins/xep_0060/stanza', 'sleekxmpp/plugins/xep_0060/stanza',
'sleekxmpp/plugins/xep_0066',
'sleekxmpp/plugins/xep_0078',
'sleekxmpp/plugins/xep_0085', 'sleekxmpp/plugins/xep_0085',
'sleekxmpp/plugins/xep_0086', 'sleekxmpp/plugins/xep_0086',
'sleekxmpp/plugins/xep_0092', 'sleekxmpp/plugins/xep_0092',
'sleekxmpp/plugins/xep_0128', 'sleekxmpp/plugins/xep_0128',
'sleekxmpp/plugins/xep_0199', 'sleekxmpp/plugins/xep_0199',
'sleekxmpp/plugins/xep_0202',
'sleekxmpp/plugins/xep_0203',
'sleekxmpp/plugins/xep_0224',
'sleekxmpp/plugins/xep_0249',
'sleekxmpp/features', 'sleekxmpp/features',
'sleekxmpp/features/feature_mechanisms', 'sleekxmpp/features/feature_mechanisms',
'sleekxmpp/features/feature_mechanisms/stanza', 'sleekxmpp/features/feature_mechanisms/stanza',
@ -70,11 +80,6 @@ packages = [ 'sleekxmpp',
'sleekxmpp/thirdparty/suelta/mechanisms', 'sleekxmpp/thirdparty/suelta/mechanisms',
] ]
if sys.version_info < (3, 0):
py_modules = ['sleekxmpp.xmlstream.tostring.tostring26']
else:
py_modules = ['sleekxmpp.xmlstream.tostring.tostring']
setup( setup(
name = "sleekxmpp", name = "sleekxmpp",
version = VERSION, version = VERSION,
@ -82,10 +87,9 @@ setup(
long_description = LONG_DESCRIPTION, long_description = LONG_DESCRIPTION,
author = 'Nathanael Fritz', author = 'Nathanael Fritz',
author_email = 'fritzy [at] netflint.net', author_email = 'fritzy [at] netflint.net',
url = 'http://code.google.com/p/sleekxmpp', url = 'http://github.com/fritzy/SleekXMPP',
license = 'MIT', license = 'MIT',
platforms = [ 'any' ], platforms = [ 'any' ],
packages = packages, packages = packages,
py_modules = py_modules,
requires = [ 'tlslite', 'pythondns' ], requires = [ 'tlslite', 'pythondns' ],
) )

View file

@ -15,5 +15,5 @@ from sleekxmpp.xmlstream import XMLStream, RestartStream
from sleekxmpp.xmlstream.matcher import * from sleekxmpp.xmlstream.matcher import *
from sleekxmpp.xmlstream.stanzabase import StanzaBase, ET from sleekxmpp.xmlstream.stanzabase import StanzaBase, ET
__version__ = '1.0beta6' __version__ = '1.0rc1'
__version_info__ = (1, 0, 0, 'beta6', 0) __version_info__ = (1, 0, 0, 'rc1', 0)

View file

@ -163,12 +163,41 @@ class BaseXMPP(XMLStream):
register_stanza_plugin(Message, Nick) register_stanza_plugin(Message, Nick)
register_stanza_plugin(Message, HTMLIM) register_stanza_plugin(Message, HTMLIM)
def start_stream_handler(self, xml):
"""
Save the stream ID once the streams have been established.
Overrides XMLStream.start_stream_handler.
Arguments:
xml -- The incoming stream's root element.
"""
self.stream_id = xml.get('id', '')
def process(self, *args, **kwargs): def process(self, *args, **kwargs):
""" """
Ensure that plugin inter-dependencies are handled before starting
event processing.
Overrides XMLStream.process. Overrides XMLStream.process.
Initialize the XML streams and begin processing events.
The number of threads used for processing stream events is determined
by HANDLER_THREADS.
Arguments:
block -- If block=False then event dispatcher will run
in a separate thread, allowing for the stream to be
used in the background for another application.
Otherwise, process(block=True) blocks the current thread.
Defaults to False.
**threaded is deprecated and included for API compatibility**
threaded -- If threaded=True then event dispatcher will run
in a separate thread, allowing for the stream to be
used in the background for another application.
Defaults to True.
Event handlers and the send queue will be threaded
regardless of these parameters.
""" """
for name in self.plugin: for name in self.plugin:
if not self.plugin[name].post_inited: if not self.plugin[name].post_inited:
@ -192,17 +221,23 @@ class BaseXMPP(XMLStream):
if not module: if not module:
try: try:
module = sleekxmpp.plugins module = sleekxmpp.plugins
module = __import__(str("%s.%s" % (module.__name__, plugin)), module = __import__(
str("%s.%s" % (module.__name__, plugin)),
globals(), locals(), [str(plugin)]) globals(), locals(), [str(plugin)])
except ImportError: except ImportError:
module = sleekxmpp.features module = sleekxmpp.features
module = __import__(str("%s.%s" % (module.__name__, plugin)), module = __import__(
str("%s.%s" % (module.__name__, plugin)),
globals(), locals(), [str(plugin)]) globals(), locals(), [str(plugin)])
if isinstance(module, str): if isinstance(module, str):
# We probably want to load a module from outside # We probably want to load a module from outside
# the sleekxmpp package, so leave out the globals(). # the sleekxmpp package, so leave out the globals().
module = __import__(module, fromlist=[plugin]) module = __import__(module, fromlist=[plugin])
# Use the global plugin config cache, if applicable
if not pconfig:
pconfig = self.plugin_config.get(plugin, {})
# Load the plugin class from the module. # Load the plugin class from the module.
self.plugin[plugin] = getattr(module, plugin)(self, pconfig) self.plugin[plugin] = getattr(module, plugin)(self, pconfig)

View file

@ -40,9 +40,12 @@ log = logging.getLogger(__name__)
class ClientXMPP(BaseXMPP): class ClientXMPP(BaseXMPP):
""" """
SleekXMPP's client class. SleekXMPP's client class. ( Use only for good, not for evil.)
Use only for good, not for evil. Typical Use:
xmpp = ClientXMPP('user@server.tld/resource', 'password')
xmpp.process(block=False) // when block is True, it blocks the current
// thread. False by default.
Attributes: Attributes:
@ -251,12 +254,7 @@ class ClientXMPP(BaseXMPP):
iq.enable('roster') iq.enable('roster')
response = iq.send(block, timeout, callback) response = iq.send(block, timeout, callback)
if response == False: if callback is None:
self.event('roster_timeout')
if response in [False, None] or not isinstance(response, Iq):
return response
else:
return self._handle_roster(response, request=True) return self._handle_roster(response, request=True)
def _handle_connected(self, event=None): def _handle_connected(self, event=None):

View file

@ -117,11 +117,13 @@ class ComponentXMPP(BaseXMPP):
Once the streams are established, attempt to handshake Once the streams are established, attempt to handshake
with the server to be accepted as a component. with the server to be accepted as a component.
Overrides XMLStream.start_stream_handler. Overrides BaseXMPP.start_stream_handler.
Arguments: Arguments:
xml -- The incoming stream's root element. xml -- The incoming stream's root element.
""" """
BaseXMPP.start_stream_handler(self, xml)
# Construct a hash of the stream ID and the component secret. # Construct a hash of the stream ID and the component secret.
sid = xml.get('id', '') sid = xml.get('id', '')
pre_hash = '%s%s' % (sid, self.secret) pre_hash = '%s%s' % (sid, self.secret)

View file

@ -52,3 +52,24 @@ class XMPPError(Exception):
self.extension = extension self.extension = extension
self.extension_ns = extension_ns self.extension_ns = extension_ns
self.extension_args = extension_args self.extension_args = extension_args
class IqTimeout(Exception):
"""
An exception which indicates that an IQ request response has not been
received within the alloted time window.
"""
def __init__(self, iq):
self.iq = iq
class IqError(Exception):
"""
An exception raised when an Iq stanza of type 'error' is received
after making a blocking send call.
"""
def __init__(self, iq):
self.iq = iq

View file

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

View file

@ -29,6 +29,7 @@ class feature_mechanisms(base_plugin):
self.description = "SASL Stream Feature" self.description = "SASL Stream Feature"
self.stanza = stanza self.stanza = stanza
self.use_mech = self.config.get('use_mech', None)
def tls_active(): def tls_active():
return 'starttls' in self.xmpp.features return 'starttls' in self.xmpp.features
@ -49,7 +50,8 @@ class feature_mechanisms(base_plugin):
username=self.xmpp.boundjid.user, username=self.xmpp.boundjid.user,
sec_query=suelta.sec_query_allow, sec_query=suelta.sec_query_allow,
request_values=sasl_callback, request_values=sasl_callback,
tls_active=tls_active) tls_active=tls_active,
mech=self.use_mech)
register_stanza_plugin(StreamFeatures, stanza.Mechanisms) register_stanza_plugin(StreamFeatures, stanza.Mechanisms)

View file

@ -8,4 +8,6 @@
__all__ = ['xep_0004', 'xep_0009', 'xep_0012', 'xep_0030', 'xep_0033', __all__ = ['xep_0004', 'xep_0009', 'xep_0012', 'xep_0030', 'xep_0033',
'xep_0045', 'xep_0050', 'xep_0060', 'xep_0066', 'xep_0082', 'xep_0045', 'xep_0050', 'xep_0060', 'xep_0066', 'xep_0082',
'xep_0085', 'xep_0086', 'xep_0092', 'xep_0128', 'xep_0199', 'xep_0085', 'xep_0086', 'xep_0092', 'xep_0128', 'xep_0199',
'xep_0202', 'xep_0203', 'xep_0224', 'xep_0249', 'gmail_notify'] 'xep_0203', 'xep_0224', 'xep_0249', 'gmail_notify']
# Don't automatically load xep_0078

View file

@ -1,395 +0,0 @@
"""
SleekXMPP: The Sleek XMPP Library
Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout
This file is part of SleekXMPP.
See the file LICENSE for copying permission.
"""
import logging
import copy
from . import base
from .. xmlstream.handler.callback import Callback
from .. xmlstream.matcher.xpath import MatchXPath
from .. xmlstream.stanzabase import registerStanzaPlugin, ElementBase, ET, JID
from .. stanza.message import Message
log = logging.getLogger(__name__)
class Form(ElementBase):
namespace = 'jabber:x:data'
name = 'x'
plugin_attrib = 'form'
interfaces = set(('fields', 'instructions', 'items', 'reported', 'title', 'type', 'values'))
sub_interfaces = set(('title',))
form_types = set(('cancel', 'form', 'result', 'submit'))
def __init__(self, *args, **kwargs):
title = None
if 'title' in kwargs:
title = kwargs['title']
del kwargs['title']
ElementBase.__init__(self, *args, **kwargs)
if title is not None:
self['title'] = title
self.field = FieldAccessor(self)
def setup(self, xml=None):
if ElementBase.setup(self, xml): #if we had to generate xml
self['type'] = 'form'
def addField(self, var='', ftype=None, label='', desc='', required=False, value=None, options=None, **kwargs):
kwtype = kwargs.get('type', None)
if kwtype is None:
kwtype = ftype
field = FormField(parent=self)
field['var'] = var
field['type'] = kwtype
field['label'] = label
field['desc'] = desc
field['required'] = required
field['value'] = value
if options is not None:
field['options'] = options
return field
def getXML(self, type='submit'):
self['type'] = type
log.warning("Form.getXML() is deprecated API compatibility with plugins/old_0004.py")
return self.xml
def fromXML(self, xml):
log.warning("Form.fromXML() is deprecated API compatibility with plugins/old_0004.py")
n = Form(xml=xml)
return n
def addItem(self, values):
itemXML = ET.Element('{%s}item' % self.namespace)
self.xml.append(itemXML)
reported_vars = self['reported'].keys()
for var in reported_vars:
fieldXML = ET.Element('{%s}field' % FormField.namespace)
itemXML.append(fieldXML)
field = FormField(xml=fieldXML)
field['var'] = var
field['value'] = values.get(var, None)
def addReported(self, var, ftype=None, label='', desc='', **kwargs):
kwtype = kwargs.get('type', None)
if kwtype is None:
kwtype = ftype
reported = self.xml.find('{%s}reported' % self.namespace)
if reported is None:
reported = ET.Element('{%s}reported' % self.namespace)
self.xml.append(reported)
fieldXML = ET.Element('{%s}field' % FormField.namespace)
reported.append(fieldXML)
field = FormField(xml=fieldXML)
field['var'] = var
field['type'] = kwtype
field['label'] = label
field['desc'] = desc
return field
def cancel(self):
self['type'] = 'cancel'
def delFields(self):
fieldsXML = self.xml.findall('{%s}field' % FormField.namespace)
for fieldXML in fieldsXML:
self.xml.remove(fieldXML)
def delInstructions(self):
instsXML = self.xml.findall('{%s}instructions')
for instXML in instsXML:
self.xml.remove(instXML)
def delItems(self):
itemsXML = self.xml.find('{%s}item' % self.namespace)
for itemXML in itemsXML:
self.xml.remove(itemXML)
def delReported(self):
reportedXML = self.xml.find('{%s}reported' % self.namespace)
if reportedXML is not None:
self.xml.remove(reportedXML)
def getFields(self, use_dict=False):
fields = {} if use_dict else []
fieldsXML = self.xml.findall('{%s}field' % FormField.namespace)
for fieldXML in fieldsXML:
field = FormField(xml=fieldXML)
if use_dict:
fields[field['var']] = field
else:
fields.append((field['var'], field))
return fields
def getInstructions(self):
instructions = ''
instsXML = self.xml.findall('{%s}instructions' % self.namespace)
return "\n".join([instXML.text for instXML in instsXML])
def getItems(self):
items = []
itemsXML = self.xml.findall('{%s}item' % self.namespace)
for itemXML in itemsXML:
item = {}
fieldsXML = itemXML.findall('{%s}field' % FormField.namespace)
for fieldXML in fieldsXML:
field = FormField(xml=fieldXML)
item[field['var']] = field['value']
items.append(item)
return items
def getReported(self):
fields = {}
fieldsXML = self.xml.findall('{%s}reported/{%s}field' % (self.namespace,
FormField.namespace))
for fieldXML in fieldsXML:
field = FormField(xml=fieldXML)
fields[field['var']] = field
return fields
def getValues(self):
values = {}
fields = self.getFields(use_dict=True)
for var in fields:
values[var] = fields[var]['value']
return values
def reply(self):
if self['type'] == 'form':
self['type'] = 'submit'
elif self['type'] == 'submit':
self['type'] = 'result'
def setFields(self, fields, default=None):
del self['fields']
for field_data in fields:
var = field_data[0]
field = field_data[1]
field['var'] = var
self.addField(**field)
def setInstructions(self, instructions):
del self['instructions']
if instructions in [None, '']:
return
instructions = instructions.split('\n')
for instruction in instructions:
inst = ET.Element('{%s}instructions' % self.namespace)
inst.text = instruction
self.xml.append(inst)
def setItems(self, items):
for item in items:
self.addItem(item)
def setReported(self, reported, default=None):
for var in reported:
field = reported[var]
field['var'] = var
self.addReported(var, **field)
def setValues(self, values):
fields = self.getFields(use_dict=True)
for field in values:
fields[field]['value'] = values[field]
def merge(self, other):
new = copy.copy(self)
if type(other) == dict:
new.setValues(other)
return new
nfields = new.getFields(use_dict=True)
ofields = other.getFields(use_dict=True)
nfields.update(ofields)
new.setFields([(x, nfields[x]) for x in nfields])
return new
class FieldAccessor(object):
def __init__(self, form):
self.form = form
def __getitem__(self, key):
return self.form.getFields(use_dict=True)[key]
def __contains__(self, key):
return key in self.form.getFields(use_dict=True)
def has_key(self, key):
return key in self.form.getFields(use_dict=True)
class FormField(ElementBase):
namespace = 'jabber:x:data'
name = 'field'
plugin_attrib = 'field'
interfaces = set(('answer', 'desc', 'required', 'value', 'options', 'label', 'type', 'var'))
sub_interfaces = set(('desc',))
field_types = set(('boolean', 'fixed', 'hidden', 'jid-multi', 'jid-single', 'list-multi',
'list-single', 'text-multi', 'text-private', 'text-single'))
multi_value_types = set(('hidden', 'jid-multi', 'list-multi', 'text-multi'))
multi_line_types = set(('hidden', 'text-multi'))
option_types = set(('list-multi', 'list-single'))
true_values = set((True, '1', 'true'))
def addOption(self, label='', value=''):
if self['type'] in self.option_types:
opt = FieldOption(parent=self)
opt['label'] = label
opt['value'] = value
else:
raise ValueError("Cannot add options to a %s field." % self['type'])
def delOptions(self):
optsXML = self.xml.findall('{%s}option' % self.namespace)
for optXML in optsXML:
self.xml.remove(optXML)
def delRequired(self):
reqXML = self.xml.find('{%s}required' % self.namespace)
if reqXML is not None:
self.xml.remove(reqXML)
def delValue(self):
valsXML = self.xml.findall('{%s}value' % self.namespace)
for valXML in valsXML:
self.xml.remove(valXML)
def getAnswer(self):
return self.getValue()
def getOptions(self):
options = []
optsXML = self.xml.findall('{%s}option' % self.namespace)
for optXML in optsXML:
opt = FieldOption(xml=optXML)
options.append({'label': opt['label'], 'value':opt['value']})
return options
def getRequired(self):
reqXML = self.xml.find('{%s}required' % self.namespace)
return reqXML is not None
def getValue(self):
valsXML = self.xml.findall('{%s}value' % self.namespace)
if len(valsXML) == 0:
return None
elif self['type'] == 'boolean':
return valsXML[0].text in self.true_values
elif self['type'] in self.multi_value_types:
values = []
for valXML in valsXML:
if valXML.text is None:
valXML.text = ''
values.append(valXML.text)
if self['type'] == 'text-multi':
values = "\n".join(values)
return values
else:
return valsXML[0].text
def setAnswer(self, answer):
self.setValue(answer)
def setFalse(self):
self.setValue(False)
def setOptions(self, options):
for value in options:
if isinstance(value, dict):
self.addOption(**value)
else:
self.addOption(value=value)
def setRequired(self, required):
exists = self.getRequired()
if not exists and required:
self.xml.append(ET.Element('{%s}required' % self.namespace))
elif exists and not required:
self.delRequired()
def setTrue(self):
self.setValue(True)
def setValue(self, value):
self.delValue()
valXMLName = '{%s}value' % self.namespace
if self['type'] == 'boolean':
if value in self.true_values:
valXML = ET.Element(valXMLName)
valXML.text = '1'
self.xml.append(valXML)
else:
valXML = ET.Element(valXMLName)
valXML.text = '0'
self.xml.append(valXML)
elif self['type'] in self.multi_value_types or self['type'] in ['', None]:
if self['type'] in self.multi_line_types and isinstance(value, str):
value = value.split('\n')
if not isinstance(value, list):
value = [value]
for val in value:
if self['type'] in ['', None] and val in self.true_values:
val = '1'
valXML = ET.Element(valXMLName)
valXML.text = val
self.xml.append(valXML)
else:
if isinstance(value, list):
raise ValueError("Cannot add multiple values to a %s field." % self['type'])
valXML = ET.Element(valXMLName)
valXML.text = value
self.xml.append(valXML)
class FieldOption(ElementBase):
namespace = 'jabber:x:data'
name = 'option'
plugin_attrib = 'option'
interfaces = set(('label', 'value'))
sub_interfaces = set(('value',))
class xep_0004(base.base_plugin):
"""
XEP-0004: Data Forms
"""
def plugin_init(self):
self.xep = '0004'
self.description = 'Data Forms'
self.xmpp.registerHandler(
Callback('Data Form',
MatchXPath('{%s}message/{%s}x' % (self.xmpp.default_ns,
Form.namespace)),
self.handle_form))
registerStanzaPlugin(FormField, FieldOption)
registerStanzaPlugin(Form, FormField)
registerStanzaPlugin(Message, Form)
def makeForm(self, ftype='form', title='', instructions=''):
f = Form()
f['type'] = ftype
f['title'] = title
f['instructions'] = instructions
return f
def post_init(self):
base.base_plugin.post_init(self)
self.xmpp.plugin['xep_0030'].add_feature('jabber:x:data')
def handle_form(self, message):
self.xmpp.event("message_xform", message)
def buildForm(self, xml):
return Form(xml=xml)

View file

@ -0,0 +1,11 @@
"""
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.plugins.xep_0004.stanza import Form
from sleekxmpp.plugins.xep_0004.stanza import FormField, FieldOption
from sleekxmpp.plugins.xep_0004.dataforms import xep_0004

View file

@ -0,0 +1,60 @@
"""
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.
"""
import copy
from sleekxmpp.thirdparty import OrderedDict
from sleekxmpp import Message
from sleekxmpp.xmlstream import register_stanza_plugin, ElementBase, ET
from sleekxmpp.xmlstream.handler import Callback
from sleekxmpp.xmlstream.matcher import StanzaPath
from sleekxmpp.plugins.base import base_plugin
from sleekxmpp.plugins.xep_0004 import stanza
from sleekxmpp.plugins.xep_0004.stanza import Form, FormField, FieldOption
class xep_0004(base_plugin):
"""
XEP-0004: Data Forms
"""
def plugin_init(self):
self.xep = '0004'
self.description = 'Data Forms'
self.stanza = stanza
self.xmpp.registerHandler(
Callback('Data Form',
StanzaPath('message/form'),
self.handle_form))
register_stanza_plugin(FormField, FieldOption, iterable=True)
register_stanza_plugin(Form, FormField, iterable=True)
register_stanza_plugin(Message, Form)
def make_form(self, ftype='form', title='', instructions=''):
f = Form()
f['type'] = ftype
f['title'] = title
f['instructions'] = instructions
return f
def post_init(self):
base_plugin.post_init(self)
self.xmpp.plugin['xep_0030'].add_feature('jabber:x:data')
def handle_form(self, message):
self.xmpp.event("message_xform", message)
def build_form(self, xml):
return Form(xml=xml)
xep_0004.makeForm = xep_0004.make_form
xep_0004.buildForm = xep_0004.build_form

View file

@ -0,0 +1,10 @@
"""
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.plugins.xep_0004.stanza.field import FormField, FieldOption
from sleekxmpp.plugins.xep_0004.stanza.form import Form

View file

@ -0,0 +1,167 @@
"""
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.xmlstream import ElementBase, ET
class FormField(ElementBase):
namespace = 'jabber:x:data'
name = 'field'
plugin_attrib = 'field'
interfaces = set(('answer', 'desc', 'required', 'value',
'options', 'label', 'type', 'var'))
sub_interfaces = set(('desc',))
plugin_tag_map = {}
plugin_attrib_map = {}
field_types = set(('boolean', 'fixed', 'hidden', 'jid-multi',
'jid-single', 'list-multi', 'list-single',
'text-multi', 'text-private', 'text-single'))
true_values = set((True, '1', 'true'))
option_types = set(('list-multi', 'list-single'))
multi_line_types = set(('hidden', 'text-multi'))
multi_value_types = set(('hidden', 'jid-multi',
'list-multi', 'text-multi'))
def add_option(self, label='', value=''):
if self['type'] in self.option_types:
opt = FieldOption(parent=self)
opt['label'] = label
opt['value'] = value
else:
raise ValueError("Cannot add options to " + \
"a %s field." % self['type'])
def del_options(self):
optsXML = self.xml.findall('{%s}option' % self.namespace)
for optXML in optsXML:
self.xml.remove(optXML)
def del_required(self):
reqXML = self.xml.find('{%s}required' % self.namespace)
if reqXML is not None:
self.xml.remove(reqXML)
def del_value(self):
valsXML = self.xml.findall('{%s}value' % self.namespace)
for valXML in valsXML:
self.xml.remove(valXML)
def get_answer(self):
return self['value']
def get_options(self):
options = []
optsXML = self.xml.findall('{%s}option' % self.namespace)
for optXML in optsXML:
opt = FieldOption(xml=optXML)
options.append({'label': opt['label'], 'value': opt['value']})
return options
def get_required(self):
reqXML = self.xml.find('{%s}required' % self.namespace)
return reqXML is not None
def get_value(self):
valsXML = self.xml.findall('{%s}value' % self.namespace)
if len(valsXML) == 0:
return None
elif self['type'] == 'boolean':
return valsXML[0].text in self.true_values
elif self['type'] in self.multi_value_types:
values = []
for valXML in valsXML:
if valXML.text is None:
valXML.text = ''
values.append(valXML.text)
if self['type'] == 'text-multi':
values = "\n".join(values)
return values
else:
return valsXML[0].text
def set_answer(self, answer):
self['value'] = answer
def set_false(self):
self['value'] = False
def set_options(self, options):
for value in options:
if isinstance(value, dict):
self.add_option(**value)
else:
self.add_option(value=value)
def set_required(self, required):
exists = self['required']
if not exists and required:
self.xml.append(ET.Element('{%s}required' % self.namespace))
elif exists and not required:
del self['required']
def set_true(self):
self['value'] = True
def set_value(self, value):
del self['value']
valXMLName = '{%s}value' % self.namespace
if self['type'] == 'boolean':
if value in self.true_values:
valXML = ET.Element(valXMLName)
valXML.text = '1'
self.xml.append(valXML)
else:
valXML = ET.Element(valXMLName)
valXML.text = '0'
self.xml.append(valXML)
elif self['type'] in self.multi_value_types or not self['type']:
if not isinstance(value, list):
if self['type'] in self.multi_line_types:
value = value.split('\n')
else:
value = [value]
for val in value:
if self['type'] in ['', None] and val in self.true_values:
val = '1'
valXML = ET.Element(valXMLName)
valXML.text = val
self.xml.append(valXML)
else:
if isinstance(value, list):
raise ValueError("Cannot add multiple values " + \
"to a %s field." % self['type'])
valXML = ET.Element(valXMLName)
valXML.text = value
self.xml.append(valXML)
class FieldOption(ElementBase):
namespace = 'jabber:x:data'
name = 'option'
plugin_attrib = 'option'
interfaces = set(('label', 'value'))
sub_interfaces = set(('value',))
FormField.addOption = FormField.add_option
FormField.delOptions = FormField.del_options
FormField.delRequired = FormField.del_required
FormField.delValue = FormField.del_value
FormField.getAnswer = FormField.get_answer
FormField.getOptions = FormField.get_options
FormField.getRequired = FormField.get_required
FormField.getValue = FormField.get_value
FormField.setAnswer = FormField.set_answer
FormField.setFalse = FormField.set_false
FormField.setOptions = FormField.set_options
FormField.setRequired = FormField.set_required
FormField.setTrue = FormField.set_true
FormField.setValue = FormField.set_value

View file

@ -0,0 +1,250 @@
"""
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.
"""
import copy
import logging
from sleekxmpp.thirdparty import OrderedDict
from sleekxmpp.xmlstream import ElementBase, ET
from sleekxmpp.plugins.xep_0004.stanza import FormField
log = logging.getLogger(__name__)
class Form(ElementBase):
namespace = 'jabber:x:data'
name = 'x'
plugin_attrib = 'form'
interfaces = set(('fields', 'instructions', 'items',
'reported', 'title', 'type', 'values'))
sub_interfaces = set(('title',))
form_types = set(('cancel', 'form', 'result', 'submit'))
def __init__(self, *args, **kwargs):
title = None
if 'title' in kwargs:
title = kwargs['title']
del kwargs['title']
ElementBase.__init__(self, *args, **kwargs)
if title is not None:
self['title'] = title
def setup(self, xml=None):
if ElementBase.setup(self, xml):
# If we had to generate xml
self['type'] = 'form'
def set_type(self, ftype):
self._set_attr('type', ftype)
if ftype == 'submit':
fields = self['fields']
for var in fields:
field = fields[var]
del field['type']
del field['label']
del field['desc']
del field['required']
del field['options']
elif ftype == 'cancel':
del self['fields']
def add_field(self, var='', ftype=None, label='', desc='',
required=False, value=None, options=None, **kwargs):
kwtype = kwargs.get('type', None)
if kwtype is None:
kwtype = ftype
field = FormField(parent=self)
field['var'] = var
field['type'] = kwtype
field['value'] = value
if self['type'] in ('form', 'result'):
field['label'] = label
field['desc'] = desc
field['required'] = required
if options is not None:
field['options'] = options
else:
del field['type']
return field
def getXML(self, type='submit'):
self['type'] = type
log.warning("Form.getXML() is deprecated API compatibility " + \
"with plugins/old_0004.py")
return self.xml
def fromXML(self, xml):
log.warning("Form.fromXML() is deprecated API compatibility " + \
"with plugins/old_0004.py")
n = Form(xml=xml)
return n
def add_item(self, values):
itemXML = ET.Element('{%s}item' % self.namespace)
self.xml.append(itemXML)
reported_vars = self['reported'].keys()
for var in reported_vars:
fieldXML = ET.Element('{%s}field' % FormField.namespace)
itemXML.append(fieldXML)
field = FormField(xml=fieldXML)
field['var'] = var
field['value'] = values.get(var, None)
def add_reported(self, var, ftype=None, label='', desc='', **kwargs):
kwtype = kwargs.get('type', None)
if kwtype is None:
kwtype = ftype
reported = self.xml.find('{%s}reported' % self.namespace)
if reported is None:
reported = ET.Element('{%s}reported' % self.namespace)
self.xml.append(reported)
fieldXML = ET.Element('{%s}field' % FormField.namespace)
reported.append(fieldXML)
field = FormField(xml=fieldXML)
field['var'] = var
field['type'] = kwtype
field['label'] = label
field['desc'] = desc
return field
def cancel(self):
self['type'] = 'cancel'
def del_fields(self):
fieldsXML = self.xml.findall('{%s}field' % FormField.namespace)
for fieldXML in fieldsXML:
self.xml.remove(fieldXML)
def del_instructions(self):
instsXML = self.xml.findall('{%s}instructions')
for instXML in instsXML:
self.xml.remove(instXML)
def del_items(self):
itemsXML = self.xml.find('{%s}item' % self.namespace)
for itemXML in itemsXML:
self.xml.remove(itemXML)
def del_reported(self):
reportedXML = self.xml.find('{%s}reported' % self.namespace)
if reportedXML is not None:
self.xml.remove(reportedXML)
def get_fields(self, use_dict=False):
fields = OrderedDict()
fieldsXML = self.xml.findall('{%s}field' % FormField.namespace)
for fieldXML in fieldsXML:
field = FormField(xml=fieldXML)
fields[field['var']] = field
return fields
def get_instructions(self):
instructions = ''
instsXML = self.xml.findall('{%s}instructions' % self.namespace)
return "\n".join([instXML.text for instXML in instsXML])
def get_items(self):
items = []
itemsXML = self.xml.findall('{%s}item' % self.namespace)
for itemXML in itemsXML:
item = {}
fieldsXML = itemXML.findall('{%s}field' % FormField.namespace)
for fieldXML in fieldsXML:
field = FormField(xml=fieldXML)
item[field['var']] = field['value']
items.append(item)
return items
def get_reported(self):
fields = {}
xml = self.xml.findall('{%s}reported/{%s}field' % (self.namespace,
FormField.namespace))
for field in xml:
field = FormField(xml=field)
fields[field['var']] = field
return fields
def get_values(self):
values = {}
fields = self['fields']
for var in fields:
values[var] = fields[var]['value']
return values
def reply(self):
if self['type'] == 'form':
self['type'] = 'submit'
elif self['type'] == 'submit':
self['type'] = 'result'
def set_fields(self, fields):
del self['fields']
if not isinstance(fields, list):
fields = fields.items()
for var, field in fields:
field['var'] = var
self.add_field(**field)
def set_instructions(self, instructions):
del self['instructions']
if instructions in [None, '']:
return
instructions = instructions.split('\n')
for instruction in instructions:
inst = ET.Element('{%s}instructions' % self.namespace)
inst.text = instruction
self.xml.append(inst)
def set_items(self, items):
for item in items:
self.add_item(item)
def set_reported(self, reported):
for var in reported:
field = reported[var]
field['var'] = var
self.add_reported(var, **field)
def set_values(self, values):
fields = self['fields']
for field in values:
fields[field]['value'] = values[field]
def merge(self, other):
new = copy.copy(self)
if type(other) == dict:
new['values'] = other
return new
nfields = new['fields']
ofields = other['fields']
nfields.update(ofields)
new['fields'] = nfields
return new
Form.setType = Form.set_type
Form.addField = Form.add_field
Form.addItem = Form.add_item
Form.addReported = Form.add_reported
Form.delFields = Form.del_fields
Form.delInstructions = Form.del_instructions
Form.delItems = Form.del_items
Form.delReported = Form.del_reported
Form.getFields = Form.get_fields
Form.getInstructions = Form.get_instructions
Form.getItems = Form.get_items
Form.getReported = Form.get_reported
Form.getValues = Form.get_values
Form.setFields = Form.set_fields
Form.setInstructions = Form.set_instructions
Form.setItems = Form.set_items
Form.setReported = Form.set_reported
Form.setValues = Form.set_values

View file

@ -463,7 +463,7 @@ class RemoteSession(object):
key = "%s.%s" % (endpoint, name) key = "%s.%s" % (endpoint, name)
log.debug("Registering call handler for %s (%s)." % (key, method)) log.debug("Registering call handler for %s (%s)." % (key, method))
with self._lock: with self._lock:
if self._entries.has_key(key): if key in self._entries:
raise KeyError("A handler for %s has already been regisered!" % endpoint) raise KeyError("A handler for %s has already been regisered!" % endpoint)
self._entries[key] = JabberRPCEntry(endpoint, method) self._entries[key] = JabberRPCEntry(endpoint, method)
return key return key

View file

@ -19,295 +19,110 @@ class xep_0060(base.base_plugin):
self.xep = '0060' self.xep = '0060'
self.description = 'Publish-Subscribe' self.description = 'Publish-Subscribe'
def create_node(self, jid, node, config=None, collection=False, ntype=None): def create_node(self, jid, node, config=None, ntype=None):
pubsub = ET.Element('{http://jabber.org/protocol/pubsub}pubsub') iq = IQ(sto=jid, stype='set', sfrom=self.xmpp.jid)
create = ET.Element('create') iq['pubsub']['create']['node'] = node
create.set('node', node) if ntype is None:
pubsub.append(create) ntype = 'leaf'
configure = ET.Element('configure')
if collection:
ntype = 'collection'
#if config is None:
# submitform = self.xmpp.plugin['xep_0004'].makeForm('submit')
#else:
if config is not None: if config is not None:
submitform = config
if 'FORM_TYPE' in submitform.field: if 'FORM_TYPE' in submitform.field:
submitform.field['FORM_TYPE'].setValue('http://jabber.org/protocol/pubsub#node_config') config.field['FORM_TYPE'].setValue('http://jabber.org/protocol/pubsub#node_config')
else: else:
submitform.addField('FORM_TYPE', 'hidden', value='http://jabber.org/protocol/pubsub#node_config') config.addField('FORM_TYPE', 'hidden', value='http://jabber.org/protocol/pubsub#node_config')
if ntype:
if 'pubsub#node_type' in submitform.field: if 'pubsub#node_type' in submitform.field:
submitform.field['pubsub#node_type'].setValue(ntype) config.field['pubsub#node_type'].setValue(ntype)
else: else:
submitform.addField('pubsub#node_type', value=ntype) config.addField('pubsub#node_type', value=ntype)
else: iq['pubsub']['configure']['form'] = config
if 'pubsub#node_type' in submitform.field: return iq.send()
submitform.field['pubsub#node_type'].setValue('leaf')
else:
submitform.addField('pubsub#node_type', value='leaf')
submitform['type'] = 'submit'
configure.append(submitform.xml)
pubsub.append(configure)
iq = self.xmpp.makeIqSet(pubsub)
iq.attrib['to'] = jid
iq.attrib['from'] = self.xmpp.boundjid.full
id = iq['id']
result = iq.send()
if result is False or result is None or result['type'] == 'error': return False
return True
def subscribe(self, jid, node, bare=True, subscribee=None): def subscribe(self, jid, node, bare=True, subscribee=None):
pubsub = ET.Element('{http://jabber.org/protocol/pubsub}pubsub') iq = IQ(sto=jid, sfrom=self.xmpp.jid, stype='set')
subscribe = ET.Element('subscribe') iq['pubsub']['subscribe']['node'] = node
subscribe.attrib['node'] = node
if subscribee is None: if subscribee is None:
if bare: if bare:
subscribe.attrib['jid'] = self.xmpp.boundjid.bare iq['pubsub']['subscribe']['jid'] = self.xmpp.jid.bare
else: else:
subscribe.attrib['jid'] = self.xmpp.boundjid.full iq['pubsub']['subscribe']['jid'] = self.xmpp.jid.full
else: else:
subscribe.attrib['jid'] = subscribee iq['pubsub']['subscribe']['jid'] = subscribee
pubsub.append(subscribe) return iq.send()
iq = self.xmpp.makeIqSet(pubsub)
iq.attrib['to'] = jid
iq.attrib['from'] = self.xmpp.boundjid.full
id = iq['id']
result = iq.send()
if result is False or result is None or result['type'] == 'error': return False
return True
def unsubscribe(self, jid, node, bare=True, subscribee=None): def unsubscribe(self, jid, node, subid=None, bare=True, subscribee=None):
pubsub = ET.Element('{http://jabber.org/protocol/pubsub}pubsub') iq = IQ(sto=jid, sfrom=self.xmpp.jid, stype='set')
unsubscribe = ET.Element('unsubscribe') iq['pubsub']['unsubscribe']['node'] = node
unsubscribe.attrib['node'] = node
if subscribee is None: if subscribee is None:
if bare: if bare:
unsubscribe.attrib['jid'] = self.xmpp.boundjid.bare iq['pubsub']['unsubscribe']['jid'] = self.xmpp.jid.bare
else: else:
unsubscribe.attrib['jid'] = self.xmpp.boundjid.full iq['pubsub']['unsubscribe']['jid'] = self.xmpp.jid.full
else: else:
unsubscribe.attrib['jid'] = subscribee iq['pubsub']['unsubscribe']['jid'] = subscribee
pubsub.append(unsubscribe) if subid is not None:
iq = self.xmpp.makeIqSet(pubsub) iq['pubsub']['unsubscribe']['subid'] = subid
iq.attrib['to'] = jid return iq.send()
iq.attrib['from'] = self.xmpp.boundjid.full
id = iq['id']
result = iq.send()
if result is False or result is None or result['type'] == 'error': return False
return True
def getNodeConfig(self, jid, node=None): # if no node, then grab default def get_node_config(self, jid, node=None): # if no node, then grab default
pubsub = ET.Element('{http://jabber.org/protocol/pubsub#owner}pubsub') iq = IQ(sto=jid, sfrom=self.xmpp.jid, stype='get')
if node is not None: if node is None:
configure = ET.Element('configure') iq['pubsub_owner']['default']
configure.attrib['node'] = node
else: else:
configure = ET.Element('default') iq['pubsub_owner']['configure']['node'] = node
pubsub.append(configure) return iq.send()
#TODO: Add configure support.
iq = self.xmpp.makeIqGet()
iq.append(pubsub)
iq.attrib['to'] = jid
iq.attrib['from'] = self.xmpp.boundjid.full
id = iq['id']
#self.xmpp.add_handler("<iq id='%s'/>" % id, self.handlerCreateNodeResponse)
result = iq.send()
if result is None or result == False or result['type'] == 'error':
log.warning("got error instead of config")
return False
if node is not None:
form = result.find('{http://jabber.org/protocol/pubsub#owner}pubsub/{http://jabber.org/protocol/pubsub#owner}configure/{jabber:x:data}x')
else:
form = result.find('{http://jabber.org/protocol/pubsub#owner}pubsub/{http://jabber.org/protocol/pubsub#owner}default/{jabber:x:data}x')
if not form or form is None:
log.error("No form found.")
return False
return Form(xml=form)
def getNodeSubscriptions(self, jid, node): def get_node_subscriptions(self, jid, node):
pubsub = ET.Element('{http://jabber.org/protocol/pubsub#owner}pubsub') iq = IQ(sto=jid, sfrom=self.xmpp.jid, stype='get')
subscriptions = ET.Element('subscriptions') iq['pubsub_owner']['subscriptions']['node'] = node
subscriptions.attrib['node'] = node return iq.send()
pubsub.append(subscriptions)
iq = self.xmpp.makeIqGet()
iq.append(pubsub)
iq.attrib['to'] = jid
iq.attrib['from'] = self.xmpp.boundjid.full
id = iq['id']
result = iq.send()
if result is None or result == False or result['type'] == 'error':
log.warning("got error instead of config")
return False
else:
results = result.findall('{http://jabber.org/protocol/pubsub#owner}pubsub/{http://jabber.org/protocol/pubsub#owner}subscriptions/{http://jabber.org/protocol/pubsub#owner}subscription')
if results is None:
return False
subs = {}
for sub in results:
subs[sub.get('jid')] = sub.get('subscription')
return subs
def getNodeAffiliations(self, jid, node): def get_node_affiliations(self, jid, node):
pubsub = ET.Element('{http://jabber.org/protocol/pubsub#owner}pubsub') iq = IQ(sto=jid, sfrom=self.xmpp.jid, stype='get')
affiliations = ET.Element('affiliations') iq['pubsub_owner']['affiliations']['node'] = node
affiliations.attrib['node'] = node return iq.send()
pubsub.append(affiliations)
iq = self.xmpp.makeIqGet()
iq.append(pubsub)
iq.attrib['to'] = jid
iq.attrib['from'] = self.xmpp.boundjid.full
id = iq['id']
result = iq.send()
if result is None or result == False or result['type'] == 'error':
log.warning("got error instead of config")
return False
else:
results = result.findall('{http://jabber.org/protocol/pubsub#owner}pubsub/{http://jabber.org/protocol/pubsub#owner}affiliations/{http://jabber.org/protocol/pubsub#owner}affiliation')
if results is None:
return False
subs = {}
for sub in results:
subs[sub.get('jid')] = sub.get('affiliation')
return subs
def deleteNode(self, jid, node): def delete_node(self, jid, node):
pubsub = ET.Element('{http://jabber.org/protocol/pubsub#owner}pubsub') iq = IQ(sto=jid, sfrom=self.xmpp.jid, stype='get')
iq = self.xmpp.makeIqSet() iq['pubsub_owner']['delete']['node'] = node
delete = ET.Element('delete') return iq.send()
delete.attrib['node'] = node
pubsub.append(delete)
iq.append(pubsub)
iq.attrib['to'] = jid
iq.attrib['from'] = self.xmpp.boundjid.full
result = iq.send()
if result is not None and result is not False and result['type'] != 'error':
return True
else:
return False
def set_node_config(self, jid, node, config):
iq = IQ(sto=jid, sfrom=self.xmpp.jid, stype='set')
iq['pubsub_owner']['configure']['node'] = node
iq['pubsub_owner']['configure']['config'] = config
return iq.send()
def setNodeConfig(self, jid, node, config): def publish(self, jid, node, items=[]):
pubsub = ET.Element('{http://jabber.org/protocol/pubsub#owner}pubsub') iq = IQ(sto=jid, sfrom=self.xmpp.jid, stype='set')
configure = ET.Element('configure') iq['pubsub']['publish']['node'] = node
configure.attrib['node'] = node for id, payload in items:
config = config.getXML('submit') item = stanza.pubsub.Item()
configure.append(config)
pubsub.append(configure)
iq = self.xmpp.makeIqSet(pubsub)
iq.attrib['to'] = jid
iq.attrib['from'] = self.xmpp.boundjid.full
id = iq['id']
result = iq.send()
if result is None or result['type'] == 'error':
return False
return True
def setItem(self, jid, node, items=[]):
pubsub = ET.Element('{http://jabber.org/protocol/pubsub}pubsub')
publish = ET.Element('publish')
publish.attrib['node'] = node
for pub_item in items:
id, payload = pub_item
item = ET.Element('item')
if id is not None: if id is not None:
item.attrib['id'] = id item['id'] = id
item.append(payload) item['payload'] = payload
publish.append(item) iq['pubsub']['publish'].append(item)
pubsub.append(publish) return iq.send()
iq = self.xmpp.makeIqSet(pubsub)
iq.attrib['to'] = jid
iq.attrib['from'] = self.xmpp.boundjid.full
id = iq['id']
result = iq.send()
if result is None or result is False or result['type'] == 'error': return False
return True
def addItem(self, jid, node, items=[]): def retract(self, jid, node, item):
return self.setItem(jid, node, items) iq = IQ(sto=jid, sfrom=self.xmpp.jid, stype='set')
iq['pubsub']['retract']['node'] = node
item = stanza.pubsub.Item()
item['id'] = item
iq['pubsub']['retract'].append(item)
return iq.send()
def deleteItem(self, jid, node, item): def get_nodes(self, jid):
pubsub = ET.Element('{http://jabber.org/protocol/pubsub}pubsub') return self.xmpp.plugin['xep_0030'].get_items(jid)
retract = ET.Element('retract')
retract.attrib['node'] = node
itemn = ET.Element('item')
itemn.attrib['id'] = item
retract.append(itemn)
pubsub.append(retract)
iq = self.xmpp.makeIqSet(pubsub)
iq.attrib['to'] = jid
iq.attrib['from'] = self.xmpp.boundjid.full
id = iq['id']
result = iq.send()
if result is None or result is False or result['type'] == 'error': return False
return True
def getNodes(self, jid):
response = self.xmpp.plugin['xep_0030'].getItems(jid)
items = response.findall('{http://jabber.org/protocol/disco#items}query/{http://jabber.org/protocol/disco#items}item')
nodes = {}
if items is not None and items is not False:
for item in items:
nodes[item.get('node')] = item.get('name')
return nodes
def getItems(self, jid, node): def getItems(self, jid, node):
response = self.xmpp.plugin['xep_0030'].getItems(jid, node) return self.xmpp.plugin['xep_0030'].get_items(jid, node)
items = response.findall('{http://jabber.org/protocol/disco#items}query/{http://jabber.org/protocol/disco#items}item')
nodeitems = []
if items is not None and items is not False:
for item in items:
nodeitems.append(item.get('node'))
return nodeitems
def addNodeToCollection(self, jid, child, parent=''):
config = self.getNodeConfig(jid, child)
if not config or config is None:
self.lasterror = "Config Error"
return False
try:
config.field['pubsub#collection'].setValue(parent)
except KeyError:
log.warning("pubsub#collection doesn't exist in config, trying to add it")
config.addField('pubsub#collection', value=parent)
if not self.setNodeConfig(jid, child, config):
return False
return True
def modifyAffiliation(self, ps_jid, node, user_jid, affiliation):
if affiliation not in ('owner', 'publisher', 'member', 'none', 'outcast'):
raise TypeError
pubsub = ET.Element('{http://jabber.org/protocol/pubsub#owner}pubsub')
affs = ET.Element('affiliations')
affs.attrib['node'] = node
aff = ET.Element('affiliation')
aff.attrib['jid'] = user_jid
aff.attrib['affiliation'] = affiliation
affs.append(aff)
pubsub.append(affs)
iq = self.xmpp.makeIqSet(pubsub)
iq.attrib['to'] = ps_jid
iq.attrib['from'] = self.xmpp.boundjid.full
id = iq['id']
result = iq.send()
if result is None or result is False or result['type'] == 'error':
return False
return True
def addNodeToCollection(self, jid, child, parent=''):
config = self.getNodeConfig(jid, child)
if not config or config is None:
self.lasterror = "Config Error"
return False
try:
config.field['pubsub#collection'].setValue(parent)
except KeyError:
log.warning("pubsub#collection doesn't exist in config, trying to add it")
config.addField('pubsub#collection', value=parent)
if not self.setNodeConfig(jid, child, config):
return False
return True
def removeNodeFromCollection(self, jid, child):
self.addNodeToCollection(jid, child, '')
def modify_affiliation(self, jid, node, affiliation, user_jid=None):
iq = IQ(sto=jid, sfrom=self.xmpp.jid, stype='set')
iq['pubsub_owner']['affiliations']
aff = stanza.pubsub.Affiliation()
aff['node'] = node
if user_jid is not None:
aff['jid'] = user_jid
aff['affiliation'] = affiliation
iq['pubsub_owner']['affiliations'].append(aff)
return iq.send()

View file

@ -23,10 +23,16 @@ class Affiliation(ElementBase):
namespace = 'http://jabber.org/protocol/pubsub' namespace = 'http://jabber.org/protocol/pubsub'
name = 'affiliation' name = 'affiliation'
plugin_attrib = name plugin_attrib = name
interfaces = set(('node', 'affiliation')) interfaces = set(('node', 'affiliation', 'jid'))
plugin_attrib_map = {} plugin_attrib_map = {}
plugin_tag_map = {} plugin_tag_map = {}
def setJid(self, value):
self._setAttr('jid', str(value))
def getJid(self):
return JID(self._getAttr('jid'))
class Affiliations(ElementBase): class Affiliations(ElementBase):
namespace = 'http://jabber.org/protocol/pubsub' namespace = 'http://jabber.org/protocol/pubsub'
name = 'affiliations' name = 'affiliations'
@ -36,12 +42,6 @@ class Affiliations(ElementBase):
plugin_tag_map = {} plugin_tag_map = {}
subitem = (Affiliation,) subitem = (Affiliation,)
def append(self, affiliation):
if not isinstance(affiliation, Affiliation):
raise TypeError
self.xml.append(affiliation.xml)
return self.iterables.append(affiliation)
registerStanzaPlugin(Pubsub, Affiliations) registerStanzaPlugin(Pubsub, Affiliations)
@ -164,7 +164,7 @@ class Unsubscribe(ElementBase):
namespace = 'http://jabber.org/protocol/pubsub' namespace = 'http://jabber.org/protocol/pubsub'
name = 'unsubscribe' name = 'unsubscribe'
plugin_attrib = name plugin_attrib = name
interfaces = set(('node', 'jid')) interfaces = set(('node', 'jid', 'subid'))
plugin_attrib_map = {} plugin_attrib_map = {}
plugin_tag_map = {} plugin_tag_map = {}

View file

@ -65,14 +65,8 @@ class OwnerAffiliation(Affiliation):
plugin_tag_map = {} plugin_tag_map = {}
class OwnerConfigure(Configure): class OwnerConfigure(Configure):
namespace = 'http://jabber.org/protocol/pubsub#owner' name = 'configure'
interfaces = set(('node', 'config')) plugin_attrib = 'configure'
plugin_attrib_map = {}
plugin_tag_map = {}
registerStanzaPlugin(PubsubOwner, OwnerConfigure)
class OwnerDefault(OwnerConfigure):
namespace = 'http://jabber.org/protocol/pubsub#owner' namespace = 'http://jabber.org/protocol/pubsub#owner'
interfaces = set(('node', 'config')) interfaces = set(('node', 'config'))
plugin_attrib_map = {} plugin_attrib_map = {}
@ -85,6 +79,15 @@ class OwnerDefault(OwnerConfigure):
self['form'].setStanzaValues(value.getStanzaValues()) self['form'].setStanzaValues(value.getStanzaValues())
return self return self
registerStanzaPlugin(PubsubOwner, OwnerConfigure)
class OwnerDefault(OwnerConfigure):
namespace = 'http://jabber.org/protocol/pubsub#owner'
interfaces = set(('node', 'config'))
plugin_attrib_map = {}
plugin_tag_map = {}
registerStanzaPlugin(PubsubOwner, OwnerDefault) registerStanzaPlugin(PubsubOwner, OwnerDefault)
registerStanzaPlugin(OwnerDefault, xep_0004.Form) registerStanzaPlugin(OwnerDefault, xep_0004.Form)

View file

@ -1,72 +0,0 @@
"""
SleekXMPP: The Sleek XMPP Library
Copyright (C) 2010 Nathanael C. Fritz
This file is part of SleekXMPP.
See the file LICENSE for copying permission.
"""
from __future__ import with_statement
from xml.etree import cElementTree as ET
import logging
import hashlib
from . import base
log = logging.getLogger(__name__)
class xep_0078(base.base_plugin):
"""
XEP-0078 NON-SASL Authentication
"""
def plugin_init(self):
self.description = "Non-SASL Authentication (broken)"
self.xep = "0078"
self.xmpp.add_event_handler("session_start", self.check_stream)
#disabling until I fix conflict with PLAIN
#self.xmpp.registerFeature("<auth xmlns='http://jabber.org/features/iq-auth'/>", self.auth)
self.streamid = ''
def check_stream(self, xml):
self.streamid = xml.attrib['id']
if xml.get('version', '0') != '1.0':
self.auth()
def auth(self, xml=None):
log.debug("Starting jabber:iq:auth Authentication")
auth_request = self.xmpp.makeIqGet()
auth_request_query = ET.Element('{jabber:iq:auth}query')
auth_request.attrib['to'] = self.xmpp.boundjid.host
username = ET.Element('username')
username.text = self.xmpp.username
auth_request_query.append(username)
auth_request.append(auth_request_query)
result = auth_request.send()
rquery = result.find('{jabber:iq:auth}query')
attempt = self.xmpp.makeIqSet()
query = ET.Element('{jabber:iq:auth}query')
resource = ET.Element('resource')
resource.text = self.xmpp.resource
query.append(username)
query.append(resource)
if rquery.find('{jabber:iq:auth}digest') is None:
log.warning("Authenticating via jabber:iq:auth Plain.")
password = ET.Element('password')
password.text = self.xmpp.password
query.append(password)
else:
log.debug("Authenticating via jabber:iq:auth Digest")
digest = ET.Element('digest')
digest.text = hashlib.sha1(b"%s%s" % (self.streamid, self.xmpp.password)).hexdigest()
query.append(digest)
attempt.append(query)
result = attempt.send()
if result.attrib['type'] == 'result':
with self.xmpp.lock:
self.xmpp.authenticated = True
self.xmpp.sessionstarted = True
self.xmpp.event("session_start")
else:
log.info("Authentication failed")
self.xmpp.disconnect()
self.xmpp.event("failed_auth")

View file

@ -0,0 +1,12 @@
"""
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.plugins.xep_0078 import stanza
from sleekxmpp.plugins.xep_0078.stanza import IqAuth, AuthFeature
from sleekxmpp.plugins.xep_0078.legacyauth import xep_0078

View file

@ -0,0 +1,108 @@
"""
SleekXMPP: The Sleek XMPP Library
Copyright (C) 2011 Nathanael C. Fritz
This file is part of SleekXMPP.
See the file LICENSE for copying permission.
"""
import logging
import hashlib
import random
from sleekxmpp.stanza import Iq, StreamFeatures
from sleekxmpp.xmlstream import ElementBase, ET, register_stanza_plugin
from sleekxmpp.plugins.base import base_plugin
from sleekxmpp.plugins.xep_0078 import stanza
log = logging.getLogger(__name__)
class xep_0078(base_plugin):
"""
XEP-0078 NON-SASL Authentication
This XEP is OBSOLETE in favor of using SASL, so DO NOT use this plugin
unless you are forced to use an old XMPP server implementation.
"""
def plugin_init(self):
self.xep = "0078"
self.description = "Non-SASL Authentication"
self.stanza = stanza
self.xmpp.register_feature('auth',
self._handle_auth,
restart=False,
order=self.config.get('order', 15))
register_stanza_plugin(Iq, stanza.IqAuth)
register_stanza_plugin(StreamFeatures, stanza.AuthFeature)
def _handle_auth(self, features):
# If we can or have already authenticated with SASL, do nothing.
if 'mechanisms' in features['features']:
return False
if self.xmpp.authenticated:
return False
log.debug("Starting jabber:iq:auth Authentication")
# Step 1: Request the auth form
iq = self.xmpp.Iq()
iq['type'] = 'get'
iq['to'] = self.xmpp.boundjid.host
iq['auth']['username'] = self.xmpp.boundjid.user
resp = iq.send(now=True)
if resp is None or resp['type'] != 'result':
log.info("Authentication failed: %s" % resp['error']['condition'])
self.xmpp.event('failed_auth', resp, direct=True)
self.xmpp.disconnect()
return True
# Step 2: Fill out auth form for either password or digest auth
iq = self.xmpp.Iq()
iq['type'] = 'set'
iq['auth']['username'] = self.xmpp.boundjid.user
# A resource is required, so create a random one if necessary
if self.xmpp.boundjid.resource:
iq['auth']['resource'] = self.xmpp.boundjid.resource
else:
iq['auth']['resource'] = '%s' % random.random()
if 'digest' in resp['auth']['fields']:
log.debug('Authenticating via jabber:iq:auth Digest')
if sys.version_info < (3, 0):
stream_id = bytes(self.xmpp.stream_id)
password = bytes(self.xmpp.password)
else:
stream_id = bytes(self.xmpp.stream_id, encoding='utf-8')
password = bytes(self.xmpp.password, encoding='utf-8')
digest = hashlib.sha1(b'%s%s' % (stream_id, password)).hexdigest()
iq['auth']['digest'] = digest
else:
log.warning('Authenticating via jabber:iq:auth Plain.')
iq['auth']['password'] = self.xmpp.password
# Step 3: Send credentials
result = iq.send(now=True)
if result is not None and result.attrib['type'] == 'result':
self.xmpp.features.add('auth')
self.xmpp.authenticated = True
log.debug("Established Session")
self.xmpp.sessionstarted = True
self.xmpp.session_started_event.set()
self.xmpp.event('session_start')
else:
log.info("Authentication failed")
self.xmpp.disconnect()
self.xmpp.event("failed_auth")
return True

View file

@ -0,0 +1,43 @@
"""
SleekXMPP: The Sleek XMPP Library
Copyright (C) 2011 Nathanael C. Fritz
This file is part of SleekXMPP.
See the file LICENSE for copying permission.
"""
from sleekxmpp.xmlstream import ElementBase, ET, register_stanza_plugin
class IqAuth(ElementBase):
namespace = 'jabber:iq:auth'
name = 'query'
plugin_attrib = 'auth'
interfaces = set(('fields', 'username', 'password', 'resource', 'digest'))
sub_interfaces = set(('username', 'password', 'resource', 'digest'))
plugin_tag_map = {}
plugin_attrib_map = {}
def get_fields(self):
fields = set()
for field in self.sub_interfaces:
if self.xml.find('{%s}%s' % (self.namespace, field)) is not None:
fields.add(field)
return fields
def set_resource(self, value):
self._set_sub_text('resource', value, keep=True)
def set_password(self, value):
self._set_sub_text('password', value, keep=True)
class AuthFeature(ElementBase):
namespace = 'http://jabber.org/features/iq-auth'
name = 'auth'
plugin_attrib = 'auth'
interfaces = set()
plugin_tag_map = {}
plugin_attrib_map = {}

View file

@ -6,10 +6,11 @@
See the file LICENSE for copying permission. See the file LICENSE for copying permission.
""" """
import logging
import datetime as dt import datetime as dt
from dateutil import parser
from dateutil.tz import tzoffset, tzutc
from sleekxmpp.plugins.base import base_plugin from sleekxmpp.plugins.base import base_plugin
from sleekxmpp.thirdparty import tzutc, tzoffset, parse_iso
# ===================================================================== # =====================================================================
@ -24,7 +25,8 @@ def parse(time_str):
Arguments: Arguments:
time_str -- A formatted timestamp string. time_str -- A formatted timestamp string.
""" """
return parser.parse(time_str) return parse_iso(time_str)
def format_date(time_obj): def format_date(time_obj):
""" """
@ -45,7 +47,7 @@ def format_time(time_obj):
Return a formatted string version of a time object. Return a formatted string version of a time object.
format: format:
hh:mm:ss[.sss][TZD hh:mm:ss[.sss][TZD]
arguments: arguments:
time_obj -- A time or datetime object. time_obj -- A time or datetime object.

View file

@ -108,7 +108,7 @@ class xep_0199(base_plugin):
iq -- The ping request. iq -- The ping request.
""" """
log.debug("Pinged by %s" % iq['from']) log.debug("Pinged by %s" % iq['from'])
iq.reply().enable('ping').send() iq.reply().send()
def send_ping(self, jid, timeout=None, errorfalse=False, def send_ping(self, jid, timeout=None, errorfalse=False,
ifrom=None, block=True, callback=None): ifrom=None, block=True, callback=None):

View file

@ -6,6 +6,7 @@
See the file LICENSE for copying permission. See the file LICENSE for copying permission.
""" """
from sleekxmpp.plugins.xep_0202 import stanza from sleekxmpp.plugins.xep_0202 import stanza
from sleekxmpp.plugins.xep_0202.stanza import EntityTime from sleekxmpp.plugins.xep_0202.stanza import EntityTime
from sleekxmpp.plugins.xep_0202.time import xep_0202 from sleekxmpp.plugins.xep_0202.time import xep_0202

View file

@ -6,11 +6,12 @@
See the file LICENSE for copying permission. See the file LICENSE for copying permission.
""" """
import logging
import datetime as dt import datetime as dt
from dateutil.tz import tzoffset, tzutc
from sleekxmpp.xmlstream import ElementBase from sleekxmpp.xmlstream import ElementBase
from sleekxmpp.plugins import xep_0082 from sleekxmpp.plugins import xep_0082
from sleekxmpp.thirdparty import tzutc, tzoffset
class EntityTime(ElementBase): class EntityTime(ElementBase):

View file

@ -11,6 +11,7 @@ from sleekxmpp.stanza.rootstanza import RootStanza
from sleekxmpp.xmlstream import StanzaBase, ET from sleekxmpp.xmlstream import StanzaBase, ET
from sleekxmpp.xmlstream.handler import Waiter, Callback from sleekxmpp.xmlstream.handler import Waiter, Callback
from sleekxmpp.xmlstream.matcher import MatcherId from sleekxmpp.xmlstream.matcher import MatcherId
from sleekxmpp.exceptions import IqTimeout, IqError
class Iq(RootStanza): class Iq(RootStanza):
@ -197,7 +198,12 @@ class Iq(RootStanza):
waitfor = Waiter('IqWait_%s' % self['id'], MatcherId(self['id'])) waitfor = Waiter('IqWait_%s' % self['id'], MatcherId(self['id']))
self.stream.register_handler(waitfor) self.stream.register_handler(waitfor)
StanzaBase.send(self, now=now) StanzaBase.send(self, now=now)
return waitfor.wait(timeout) result = waitfor.wait(timeout)
if not result:
raise IqTimeout(self)
if result['type'] == 'error':
raise IqError(result)
return result
else: else:
return StanzaBase.send(self, now=now) return StanzaBase.send(self, now=now)

View file

@ -19,6 +19,8 @@ class StreamFeatures(StanzaBase):
namespace = 'http://etherx.jabber.org/streams' namespace = 'http://etherx.jabber.org/streams'
interfaces = set(('features', 'required', 'optional')) interfaces = set(('features', 'required', 'optional'))
sub_interfaces = interfaces sub_interfaces = interfaces
plugin_tag_map = {}
plugin_attrib_map = {}
def setup(self, xml): def setup(self, xml):
StanzaBase.setup(self, xml) StanzaBase.setup(self, xml)

View file

@ -16,6 +16,7 @@ import sleekxmpp
from sleekxmpp import ClientXMPP, ComponentXMPP from sleekxmpp import ClientXMPP, ComponentXMPP
from sleekxmpp.stanza import Message, Iq, Presence from sleekxmpp.stanza import Message, Iq, Presence
from sleekxmpp.test import TestSocket, TestLiveSocket from sleekxmpp.test import TestSocket, TestLiveSocket
from sleekxmpp.exceptions import XMPPError, IqTimeout, IqError
from sleekxmpp.xmlstream import ET, register_stanza_plugin from sleekxmpp.xmlstream import ET, register_stanza_plugin
from sleekxmpp.xmlstream import ElementBase, StanzaBase from sleekxmpp.xmlstream import ElementBase, StanzaBase
from sleekxmpp.xmlstream.tostring import tostring from sleekxmpp.xmlstream.tostring import tostring
@ -344,9 +345,11 @@ class SleekTest(unittest.TestCase):
self.xmpp.socket.recv_data(header) self.xmpp.socket.recv_data(header)
elif socket == 'live': elif socket == 'live':
self.xmpp.socket_class = TestLiveSocket self.xmpp.socket_class = TestLiveSocket
def wait_for_session(x): def wait_for_session(x):
self.xmpp.socket.clear() self.xmpp.socket.clear()
skip_queue.put('started') skip_queue.put('started')
self.xmpp.add_event_handler('session_start', wait_for_session) self.xmpp.add_event_handler('session_start', wait_for_session)
self.xmpp.connect() self.xmpp.connect()
else: else:

View file

@ -4,3 +4,4 @@ except:
from sleekxmpp.thirdparty.ordereddict import OrderedDict from sleekxmpp.thirdparty.ordereddict import OrderedDict
from sleekxmpp.thirdparty import suelta from sleekxmpp.thirdparty import suelta
from sleekxmpp.thirdparty.mini_dateutil import tzutc, tzoffset, parse_iso

267
sleekxmpp/thirdparty/mini_dateutil.py vendored Normal file
View file

@ -0,0 +1,267 @@
# This module is a very stripped down version of the dateutil
# package for when dateutil has not been installed. As a replacement
# for dateutil.parser.parse, the parsing methods from
# http://blog.mfabrik.com/2008/06/30/relativity-of-time-shortcomings-in-python-datetime-and-workaround/
#As such, the following copyrights and licenses applies:
# dateutil - Extensions to the standard python 2.3+ datetime module.
#
# Copyright (c) 2003-2011 - Gustavo Niemeyer <gustavo@niemeyer.net>
#
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
# * Neither the name of the copyright holder nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
# fixed_dateime
#
# Copyright (c) 2008, Red Innovation Ltd., Finland
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# * Neither the name of Red Innovation nor the names of its contributors
# may be used to endorse or promote products derived from this software
# without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY RED INNOVATION ``AS IS'' AND ANY
# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL RED INNOVATION BE LIABLE FOR ANY
# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
import re
import datetime
ZERO = datetime.timedelta(0)
try:
from dateutil.parser import parse as parse_iso
from dateutil.tz import tzoffset, tzutc
except:
# As a stopgap, define the two timezones here based
# on the dateutil code.
class tzutc(datetime.tzinfo):
def utcoffset(self, dt):
return ZERO
def dst(self, dt):
return ZERO
def tzname(self, dt):
return "UTC"
def __eq__(self, other):
return (isinstance(other, tzutc) or
(isinstance(other, tzoffset) and other._offset == ZERO))
def __ne__(self, other):
return not self.__eq__(other)
def __repr__(self):
return "%s()" % self.__class__.__name__
__reduce__ = object.__reduce__
class tzoffset(datetime.tzinfo):
def __init__(self, name, offset):
self._name = name
self._offset = datetime.timedelta(seconds=offset)
def utcoffset(self, dt):
return self._offset
def dst(self, dt):
return ZERO
def tzname(self, dt):
return self._name
def __eq__(self, other):
return (isinstance(other, tzoffset) and
self._offset == other._offset)
def __ne__(self, other):
return not self.__eq__(other)
def __repr__(self):
return "%s(%s, %s)" % (self.__class__.__name__,
repr(self._name),
self._offset.days*86400+self._offset.seconds)
__reduce__ = object.__reduce__
_fixed_offset_tzs = { }
UTC = tzutc()
def _get_fixed_offset_tz(offsetmins):
"""For internal use only: Returns a tzinfo with
the given fixed offset. This creates only one instance
for each offset; the zones are kept in a dictionary"""
if offsetmins == 0:
return UTC
if not offsetmins in _fixed_offset_tzs:
if offsetmins < 0:
sign = '-'
absoff = -offsetmins
else:
sign = '+'
absoff = offsetmins
name = "UTC%s%02d:%02d" % (sign, int(absoff / 60), absoff % 60)
inst = tzoffset(offsetmins, name)
_fixed_offset_tzs[offsetmins] = inst
return _fixed_offset_tzs[offsetmins]
_iso8601_parser = re.compile("""
^
(?P<year> [0-9]{4})?(?P<ymdsep>-?)?
(?P<month>[0-9]{2})?(?P=ymdsep)?
(?P<day> [0-9]{2})?
(?: # time part... optional... at least hour must be specified
(?:T|\s+)?
(?P<hour>[0-9]{2})
(?:
# minutes, separated with :, or none, from hours
(?P<hmssep>[:]?)
(?P<minute>[0-9]{2})
(?:
# same for seconds, separated with :, or none, from hours
(?P=hmssep)
(?P<second>[0-9]{2})
)?
)?
# fractions
(?: [,.] (?P<frac>[0-9]{1,10}))?
# timezone, Z, +-hh or +-hh:?mm. MUST BE, but complain if not there.
(
(?P<tzempty>Z)
|
(?P<tzh>[+-][0-9]{2})
(?: :? # optional separator
(?P<tzm>[0-9]{2})
)?
)?
)?
$
""", re.X) # """
def parse_iso(timestamp):
"""Internal function for parsing a timestamp in
ISO 8601 format"""
timestamp = timestamp.strip()
m = _iso8601_parser.match(timestamp)
if not m:
raise ValueError("Not a proper ISO 8601 timestamp!: %s" % timestamp)
vals = m.groupdict()
def_vals = {'year': 1970, 'month': 1, 'day': 1}
for key in vals:
if vals[key] is None:
vals[key] = def_vals.get(key, 0)
elif key not in ['ymdsep', 'hmssep', 'tzempty']:
vals[key] = int(vals[key])
year = vals['year']
month = vals['month']
day = vals['day']
h, min, s, us = None, None, None, 0
frac = 0
if m.group('tzempty') == None and m.group('tzh') == None:
raise ValueError("Not a proper ISO 8601 timestamp: " +
"missing timezone (Z or +hh[:mm])!")
if m.group('frac'):
frac = m.group('frac')
power = len(frac)
frac = int(frac) / 10.0 ** power
if m.group('hour'):
h = vals['hour']
if m.group('minute'):
min = vals['minute']
if m.group('second'):
s = vals['second']
if frac != None:
# ok, fractions of hour?
if min == None:
frac, min = _math.modf(frac * 60.0)
min = int(min)
# fractions of second?
if s == None:
frac, s = _math.modf(frac * 60.0)
s = int(s)
# and extract microseconds...
us = int(frac * 1000000)
if m.group('tzempty') == 'Z':
offsetmins = 0
else:
# timezone: hour diff with sign
offsetmins = vals['tzh'] * 60
tzm = m.group('tzm')
# add optional minutes
if tzm != None:
tzm = int(tzm)
offsetmins += tzm if offsetmins > 0 else -tzm
tz = _get_fixed_offset_tz(offsetmins)
return datetime.datetime(year, month, day, h, min, s, us, tz)

View file

@ -10,7 +10,7 @@ class ANONYMOUS(Mechanism):
def __init__(self, sasl, name): def __init__(self, sasl, name):
""" """
""" """
super(ANONYMOUS, self).__init__(self, sasl, name, 0) super(ANONYMOUS, self).__init__(sasl, name, 0)
def get_values(self): def get_values(self):
""" """

View file

@ -225,7 +225,7 @@ class SASL(object):
requested_mech = 'ANONYMOUS' requested_mech = 'ANONYMOUS'
else: else:
requested_mech = self.mech requested_mech = self.mech
if requested_mech == '*' and self.user == 'anonymous': if requested_mech == '*' and self.user in ['', 'anonymous', None]:
requested_mech = 'ANONYMOUS' requested_mech = 'ANONYMOUS'
# If a specific mechanism was requested, try it # If a specific mechanism was requested, try it
@ -243,7 +243,7 @@ class SASL(object):
if MECH_SEC_SCORES[name] > best_score: if MECH_SEC_SCORES[name] > best_score:
best_score = MECH_SEC_SCORES[name] best_score = MECH_SEC_SCORES[name]
best_mech = name best_mech = name
if best_mech != None: if best_mech is not None:
best_mech = MECHANISMS[best_mech](self, best_mech) best_mech = MECHANISMS[best_mech](self, best_mech)
return best_mech return best_mech

View file

@ -482,7 +482,8 @@ class ElementBase(object):
if plugin: if plugin:
if plugin not in self.plugins: if plugin not in self.plugins:
self.init_plugin(plugin) self.init_plugin(plugin)
handler = getattr(self.plugins[plugin], set_method, None) handler = getattr(self.plugins[plugin],
set_method, None)
if handler: if handler:
return handler(value) return handler(value)
@ -1066,7 +1067,7 @@ class ElementBase(object):
stanza_ns = '' if top_level_ns else self.namespace stanza_ns = '' if top_level_ns else self.namespace
return tostring(self.xml, xmlns='', return tostring(self.xml, xmlns='',
stanza_ns=stanza_ns, stanza_ns=stanza_ns,
top_level = not top_level_ns) top_level=not top_level_ns)
def __repr__(self): def __repr__(self):
""" """
@ -1285,7 +1286,7 @@ class StanzaBase(ElementBase):
return tostring(self.xml, xmlns='', return tostring(self.xml, xmlns='',
stanza_ns=stanza_ns, stanza_ns=stanza_ns,
stream=self.stream, stream=self.stream,
top_level = not top_level_ns) top_level=not top_level_ns)
# To comply with PEP8, method names now use underscores. # To comply with PEP8, method names now use underscores.

View file

@ -831,7 +831,7 @@ class XMLStream(object):
self.send_queue.put(data) self.send_queue.put(data)
return True return True
def process(self, threaded=True): def process(self, **kwargs):
""" """
Initialize the XML streams and begin processing events. Initialize the XML streams and begin processing events.
@ -839,14 +839,29 @@ class XMLStream(object):
by HANDLER_THREADS. by HANDLER_THREADS.
Arguments: Arguments:
block -- If block=False then event dispatcher will run
in a separate thread, allowing for the stream to be
used in the background for another application.
Otherwise, process(block=True) blocks the current thread.
Defaults to False.
**threaded is deprecated and included for API compatibility**
threaded -- If threaded=True then event dispatcher will run threaded -- If threaded=True then event dispatcher will run
in a separate thread, allowing for the stream to be in a separate thread, allowing for the stream to be
used in the background for another application. used in the background for another application.
Defaults to True. Defaults to True.
Event handlers and the send queue will be threaded Event handlers and the send queue will be threaded
regardless of this parameter's value. regardless of these parameters.
""" """
if 'threaded' in kwargs and 'block' in kwargs:
raise ValueError("process() called with both " + \
"block and threaded arguments")
elif 'block' in kwargs:
threaded = not(kwargs.get('block', False))
else:
threaded = kwargs.get('threaded', True)
self.scheduler.process(threaded=True) self.scheduler.process(threaded=True)
def start_thread(name, target): def start_thread(name, target):

View file

@ -1,4 +1,6 @@
from sleekxmpp.test import * from sleekxmpp.test import *
from sleekxmpp.thirdparty import OrderedDict
import sleekxmpp.plugins.xep_0004 as xep_0004 import sleekxmpp.plugins.xep_0004 as xep_0004
@ -47,21 +49,25 @@ class TestDataForms(SleekTest):
</message> </message>
""") """)
form['fields'] = [('f1', {'type': 'text-single', fields = OrderedDict()
fields['f1'] = {'type': 'text-single',
'label': 'Username', 'label': 'Username',
'required': True}), 'required': True}
('f2', {'type': 'text-private', fields['f2'] = {'type': 'text-private',
'label': 'Password', 'label': 'Password',
'required': True}), 'required': True}
('f3', {'type': 'text-multi', fields['f3'] = {'type': 'text-multi',
'label': 'Message', 'label': 'Message',
'value': 'Enter message.\nA long one even.'}), 'value': 'Enter message.\nA long one even.'}
('f4', {'type': 'list-single', fields['f4'] = {'type': 'list-single',
'label': 'Message Type', 'label': 'Message Type',
'options': [{'label': 'Cool!', 'options': [{'label': 'Cool!',
'value': 'cool'}, 'value': 'cool'},
{'label': 'Urgh!', {'label': 'Urgh!',
'value': 'urgh'}]})] 'value': 'urgh'}]}
form['fields'] = fields
self.check(msg, """ self.check(msg, """
<message> <message>
<x xmlns="jabber:x:data" type="form"> <x xmlns="jabber:x:data" type="form">
@ -92,9 +98,8 @@ class TestDataForms(SleekTest):
msg = self.Message() msg = self.Message()
form = msg['form'] form = msg['form']
form.setFields([ form.add_field(var='foo', ftype='text-single')
('foo', {'type': 'text-single'}), form.add_field(var='bar', ftype='list-multi')
('bar', {'type': 'list-multi'})])
form.setValues({'foo': 'Foo!', form.setValues({'foo': 'Foo!',
'bar': ['a', 'b']}) 'bar': ['a', 'b']})

View file

@ -182,7 +182,7 @@ class TestPubsubStanzas(SleekTest):
<subscribe node="cheese" jid="fritzy@netflint.net/sleekxmpp"> <subscribe node="cheese" jid="fritzy@netflint.net/sleekxmpp">
<options node="cheese" jid="fritzy@netflint.net/sleekxmpp"> <options node="cheese" jid="fritzy@netflint.net/sleekxmpp">
<x xmlns="jabber:x:data" type="submit"> <x xmlns="jabber:x:data" type="submit">
<field var="pubsub#title" type="text-single"> <field var="pubsub#title">
<value>this thing is awesome</value> <value>this thing is awesome</value>
</field> </field>
</x> </x>
@ -306,42 +306,42 @@ class TestPubsubStanzas(SleekTest):
<create node="testnode2" /> <create node="testnode2" />
<configure> <configure>
<x xmlns="jabber:x:data" type="submit"> <x xmlns="jabber:x:data" type="submit">
<field var="FORM_TYPE" type="hidden"> <field var="FORM_TYPE">
<value>http://jabber.org/protocol/pubsub#node_config</value> <value>http://jabber.org/protocol/pubsub#node_config</value>
</field> </field>
<field var="pubsub#node_type" type="list-single" label="Select the node type"> <field var="pubsub#node_type">
<value>leaf</value> <value>leaf</value>
</field> </field>
<field var="pubsub#title" type="text-single" label="A friendly name for the node" /> <field var="pubsub#title" />
<field var="pubsub#deliver_notifications" type="boolean" label="Deliver event notifications"> <field var="pubsub#deliver_notifications">
<value>1</value> <value>1</value>
</field> </field>
<field var="pubsub#deliver_payloads" type="boolean" label="Deliver payloads with event notifications"> <field var="pubsub#deliver_payloads">
<value>1</value> <value>1</value>
</field> </field>
<field var="pubsub#notify_config" type="boolean" label="Notify subscribers when the node configuration changes" /> <field var="pubsub#notify_config" />
<field var="pubsub#notify_delete" type="boolean" label="Notify subscribers when the node is deleted" /> <field var="pubsub#notify_delete" />
<field var="pubsub#notify_retract" type="boolean" label="Notify subscribers when items are removed from the node"> <field var="pubsub#notify_retract">
<value>1</value> <value>1</value>
</field> </field>
<field var="pubsub#notify_sub" type="boolean" label="Notify owners about new subscribers and unsubscribes" /> <field var="pubsub#notify_sub" />
<field var="pubsub#persist_items" type="boolean" label="Persist items in storage" /> <field var="pubsub#persist_items" />
<field var="pubsub#max_items" type="text-single" label="Max # of items to persist"> <field var="pubsub#max_items">
<value>10</value> <value>10</value>
</field> </field>
<field var="pubsub#subscribe" type="boolean" label="Whether to allow subscriptions"> <field var="pubsub#subscribe">
<value>1</value> <value>1</value>
</field> </field>
<field var="pubsub#access_model" type="list-single" label="Specify the subscriber model"> <field var="pubsub#access_model">
<value>open</value> <value>open</value>
</field> </field>
<field var="pubsub#publish_model" type="list-single" label="Specify the publisher model"> <field var="pubsub#publish_model">
<value>publishers</value> <value>publishers</value>
</field> </field>
<field var="pubsub#send_last_published_item" type="list-single" label="Send last published item"> <field var="pubsub#send_last_published_item">
<value>never</value> <value>never</value>
</field> </field>
<field var="pubsub#presence_based_delivery" type="boolean" label="Deliver notification only to available users" /> <field var="pubsub#presence_based_delivery" />
</x> </x>
</configure> </configure>
</pubsub> </pubsub>

View file

@ -90,7 +90,10 @@ class TestHandlers(SleekTest):
iq['id'] = 'test2' iq['id'] = 'test2'
iq['type'] = 'set' iq['type'] = 'set'
iq['query'] = 'test2' iq['query'] = 'test2'
try:
reply = iq.send(block=True, timeout=0) reply = iq.send(block=True, timeout=0)
except IqTimeout:
pass
self.xmpp.add_event_handler('message', waiter_handler, threaded=True) self.xmpp.add_event_handler('message', waiter_handler, threaded=True)

View file

@ -107,19 +107,12 @@ class TestStreamRoster(SleekTest):
def testRosterTimeout(self): def testRosterTimeout(self):
"""Test handling a timed out roster request.""" """Test handling a timed out roster request."""
self.stream_start() self.stream_start()
events = []
def roster_timeout(event): def do_test():
events.append('roster_timeout')
self.xmpp.add_event_handler('roster_timeout', roster_timeout)
self.xmpp.get_roster(timeout=0) self.xmpp.get_roster(timeout=0)
# Give the event queue time to process.
time.sleep(.1) time.sleep(.1)
self.failUnless(events == ['roster_timeout'], self.assertRaises(IqTimeout, do_test)
"Roster timeout event not triggered: %s." % events)
def testRosterCallback(self): def testRosterCallback(self):
"""Test handling a roster request callback.""" """Test handling a roster request callback."""

10
todo1.0
View file

@ -18,8 +18,6 @@ Plugins:
PEP8 PEP8
Documentation Documentation
Stream/Unit tests Stream/Unit tests
0050
Review replacement in github.com/legastero/adhoc
0060 0060
PEP8 PEP8
Documentation Documentation
@ -29,14 +27,6 @@ Plugins:
PEP8 PEP8
Documentation Documentation
Stream/Unit tests Stream/Unit tests
0086
PEP8
Documentation
Consider any simplifications.
0202
PEP8
Documentation
Stream/Unit tests
gmail_notify gmail_notify
PEP8 PEP8
Documentation Documentation