diff options
24 files changed, 894 insertions, 516 deletions
@@ -1,2 +1,4 @@ *.pyc build/ +dist/ +MANIFEST diff --git a/examples/send_client.py b/examples/send_client.py new file mode 100755 index 00000000..fd99e8c9 --- /dev/null +++ b/examples/send_client.py @@ -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.") @@ -1,7 +1,7 @@ #!/usr/bin/env python
# -*- coding: utf-8 -*-
#
-# Copyright (C) 2007-2008 Nathanael C. Fritz
+# Copyright (C) 2007-2011 Nathanael C. Fritz
# All Rights Reserved
#
# This software is licensed as described in the README file,
@@ -29,13 +29,16 @@ import sleekxmpp VERSION = sleekxmpp.__version__
DESCRIPTION = 'SleekXMPP is an elegant Python library for XMPP (aka Jabber, Google Talk, etc).'
-LONG_DESCRIPTION = """
-SleekXMPP is an elegant Python library for XMPP (aka Jabber, Google Talk, etc).
-"""
+with open('README') as readme:
+ LONG_DESCRIPTION = '\n'.join(readme)
CLASSIFIERS = [ 'Intended Audience :: Developers',
- 'License :: OSI Approved :: MIT',
+ 'License :: OSI Approved :: MIT License',
'Programming Language :: Python',
+ 'Programming Language :: Python 2.6',
+ 'Programming Language :: Python 2.7',
+ 'Programming Language :: Python 3.1',
+ 'Programming Language :: Python 3.2',
'Topic :: Software Development :: Libraries :: Python Modules',
]
@@ -55,6 +58,7 @@ packages = [ 'sleekxmpp', 'sleekxmpp/plugins/xep_0060',
'sleekxmpp/plugins/xep_0060/stanza',
'sleekxmpp/plugins/xep_0066',
+ 'sleekxmpp/plugins/xep_0078',
'sleekxmpp/plugins/xep_0085',
'sleekxmpp/plugins/xep_0086',
'sleekxmpp/plugins/xep_0092',
@@ -82,7 +86,7 @@ setup( long_description = LONG_DESCRIPTION,
author = 'Nathanael Fritz',
author_email = 'fritzy [at] netflint.net',
- url = 'http://code.google.com/p/sleekxmpp',
+ url = 'http://github.com/fritzy/SleekXMPP',
license = 'MIT',
platforms = [ 'any' ],
packages = packages,
diff --git a/sleekxmpp/__init__.py b/sleekxmpp/__init__.py index a53cfb0e..d2c014d3 100644 --- a/sleekxmpp/__init__.py +++ b/sleekxmpp/__init__.py @@ -15,5 +15,5 @@ from sleekxmpp.xmlstream import XMLStream, RestartStream from sleekxmpp.xmlstream.matcher import * from sleekxmpp.xmlstream.stanzabase import StanzaBase, ET -__version__ = '1.0beta6' -__version_info__ = (1, 0, 0, 'beta6', 0) +__version__ = '1.0rc1' +__version_info__ = (1, 0, 0, 'rc1', 0) diff --git a/sleekxmpp/basexmpp.py b/sleekxmpp/basexmpp.py index 4d9a8964..7c131250 100644 --- a/sleekxmpp/basexmpp.py +++ b/sleekxmpp/basexmpp.py @@ -138,6 +138,17 @@ class BaseXMPP(XMLStream): register_stanza_plugin(Message, Nick) 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): """ Overrides XMLStream.process. @@ -198,6 +209,10 @@ class BaseXMPP(XMLStream): # the sleekxmpp package, so leave out the globals(). module = __import__(module, fromlist=[plugin]) + # Use the global plugin config cache, if applicable + if not pconfig: + pconfig = self.plugin_config.get(plugin, {}) + # Load the plugin class from the module. self.plugin[plugin] = getattr(module, plugin)(self, pconfig) diff --git a/sleekxmpp/componentxmpp.py b/sleekxmpp/componentxmpp.py index f9e7da4d..ed96016a 100644 --- a/sleekxmpp/componentxmpp.py +++ b/sleekxmpp/componentxmpp.py @@ -115,11 +115,13 @@ class ComponentXMPP(BaseXMPP): Once the streams are established, attempt to handshake with the server to be accepted as a component. - Overrides XMLStream.start_stream_handler. + Overrides BaseXMPP.start_stream_handler. Arguments: xml -- The incoming stream's root element. """ + BaseXMPP.start_stream_handler(self, xml) + # Construct a hash of the stream ID and the component secret. sid = xml.get('id', '') pre_hash = '%s%s' % (sid, self.secret) diff --git a/sleekxmpp/features/feature_mechanisms/mechanisms.py b/sleekxmpp/features/feature_mechanisms/mechanisms.py index 2debf3be..a6cff0a0 100644 --- a/sleekxmpp/features/feature_mechanisms/mechanisms.py +++ b/sleekxmpp/features/feature_mechanisms/mechanisms.py @@ -29,6 +29,8 @@ class feature_mechanisms(base_plugin): self.description = "SASL Stream Feature" self.stanza = stanza + self.use_mech = self.config.get('use_mech', None) + def tls_active(): return 'starttls' in self.xmpp.features @@ -48,7 +50,8 @@ class feature_mechanisms(base_plugin): username=self.xmpp.boundjid.user, sec_query=suelta.sec_query_allow, request_values=sasl_callback, - tls_active=tls_active) + tls_active=tls_active, + mech=self.use_mech) register_stanza_plugin(StreamFeatures, stanza.Mechanisms) diff --git a/sleekxmpp/plugins/__init__.py b/sleekxmpp/plugins/__init__.py index 7fa031ef..c0b1121b 100644 --- a/sleekxmpp/plugins/__init__.py +++ b/sleekxmpp/plugins/__init__.py @@ -9,3 +9,5 @@ __all__ = ['xep_0004', 'xep_0009', 'xep_0012', 'xep_0030', 'xep_0033', 'xep_0045', 'xep_0050', 'xep_0060', 'xep_0066', 'xep_0082', 'xep_0085', 'xep_0086', 'xep_0092', 'xep_0128', 'xep_0199', 'xep_0203', 'xep_0224', 'xep_0249', 'gmail_notify'] + +# Don't automatically load xep_0078 diff --git a/sleekxmpp/plugins/xep_0004.py b/sleekxmpp/plugins/xep_0004.py deleted file mode 100644 index 5a49d70f..00000000 --- a/sleekxmpp/plugins/xep_0004.py +++ /dev/null @@ -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) diff --git a/sleekxmpp/plugins/xep_0004/__init__.py b/sleekxmpp/plugins/xep_0004/__init__.py new file mode 100644 index 00000000..aad4e15f --- /dev/null +++ b/sleekxmpp/plugins/xep_0004/__init__.py @@ -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 diff --git a/sleekxmpp/plugins/xep_0004/dataforms.py b/sleekxmpp/plugins/xep_0004/dataforms.py new file mode 100644 index 00000000..5414be5c --- /dev/null +++ b/sleekxmpp/plugins/xep_0004/dataforms.py @@ -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 diff --git a/sleekxmpp/plugins/xep_0004/stanza/__init__.py b/sleekxmpp/plugins/xep_0004/stanza/__init__.py new file mode 100644 index 00000000..6ad35298 --- /dev/null +++ b/sleekxmpp/plugins/xep_0004/stanza/__init__.py @@ -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 diff --git a/sleekxmpp/plugins/xep_0004/stanza/field.py b/sleekxmpp/plugins/xep_0004/stanza/field.py new file mode 100644 index 00000000..9bb92311 --- /dev/null +++ b/sleekxmpp/plugins/xep_0004/stanza/field.py @@ -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 diff --git a/sleekxmpp/plugins/xep_0004/stanza/form.py b/sleekxmpp/plugins/xep_0004/stanza/form.py new file mode 100644 index 00000000..d85266fc --- /dev/null +++ b/sleekxmpp/plugins/xep_0004/stanza/form.py @@ -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 diff --git a/sleekxmpp/plugins/xep_0078.py b/sleekxmpp/plugins/xep_0078.py deleted file mode 100644 index bb6a4632..00000000 --- a/sleekxmpp/plugins/xep_0078.py +++ /dev/null @@ -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") diff --git a/sleekxmpp/plugins/xep_0078/__init__.py b/sleekxmpp/plugins/xep_0078/__init__.py new file mode 100644 index 00000000..5a2bda77 --- /dev/null +++ b/sleekxmpp/plugins/xep_0078/__init__.py @@ -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 + diff --git a/sleekxmpp/plugins/xep_0078/legacyauth.py b/sleekxmpp/plugins/xep_0078/legacyauth.py new file mode 100644 index 00000000..bdd2df67 --- /dev/null +++ b/sleekxmpp/plugins/xep_0078/legacyauth.py @@ -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 diff --git a/sleekxmpp/plugins/xep_0078/stanza.py b/sleekxmpp/plugins/xep_0078/stanza.py new file mode 100644 index 00000000..86ba09ad --- /dev/null +++ b/sleekxmpp/plugins/xep_0078/stanza.py @@ -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 = {} + + diff --git a/sleekxmpp/plugins/xep_0199/ping.py b/sleekxmpp/plugins/xep_0199/ping.py index d1e08e61..0fa22f8a 100644 --- a/sleekxmpp/plugins/xep_0199/ping.py +++ b/sleekxmpp/plugins/xep_0199/ping.py @@ -108,7 +108,7 @@ class xep_0199(base_plugin): iq -- The ping request. """ log.debug("Pinged by %s" % iq['from']) - iq.reply().enable('ping').send() + iq.reply().send() def send_ping(self, jid, timeout=None, errorfalse=False, ifrom=None, block=True, callback=None): diff --git a/sleekxmpp/stanza/stream_features.py b/sleekxmpp/stanza/stream_features.py index 5be2e55f..b800011f 100644 --- a/sleekxmpp/stanza/stream_features.py +++ b/sleekxmpp/stanza/stream_features.py @@ -19,6 +19,8 @@ class StreamFeatures(StanzaBase): namespace = 'http://etherx.jabber.org/streams' interfaces = set(('features', 'required', 'optional')) sub_interfaces = interfaces + plugin_tag_map = {} + plugin_attrib_map = {} def setup(self, xml): StanzaBase.setup(self, xml) diff --git a/sleekxmpp/thirdparty/suelta/mechanisms/anonymous.py b/sleekxmpp/thirdparty/suelta/mechanisms/anonymous.py index de89eef2..e44e91a2 100644 --- a/sleekxmpp/thirdparty/suelta/mechanisms/anonymous.py +++ b/sleekxmpp/thirdparty/suelta/mechanisms/anonymous.py @@ -10,7 +10,7 @@ class ANONYMOUS(Mechanism): def __init__(self, sasl, name): """ """ - super(ANONYMOUS, self).__init__(self, sasl, name, 0) + super(ANONYMOUS, self).__init__(sasl, name, 0) def get_values(self): """ diff --git a/sleekxmpp/thirdparty/suelta/sasl.py b/sleekxmpp/thirdparty/suelta/sasl.py index ec7afe9d..2ae9ae61 100644 --- a/sleekxmpp/thirdparty/suelta/sasl.py +++ b/sleekxmpp/thirdparty/suelta/sasl.py @@ -225,7 +225,7 @@ class SASL(object): requested_mech = 'ANONYMOUS' else: requested_mech = self.mech - if requested_mech == '*' and self.user == 'anonymous': + if requested_mech == '*' and self.user in ['', 'anonymous', None]: requested_mech = 'ANONYMOUS' # If a specific mechanism was requested, try it @@ -243,7 +243,7 @@ class SASL(object): if MECH_SEC_SCORES[name] > best_score: best_score = MECH_SEC_SCORES[name] best_mech = name - if best_mech != None: + if best_mech is not None: best_mech = MECHANISMS[best_mech](self, best_mech) return best_mech diff --git a/tests/test_stanza_xep_0004.py b/tests/test_stanza_xep_0004.py index bdc4a878..22f8b77d 100644 --- a/tests/test_stanza_xep_0004.py +++ b/tests/test_stanza_xep_0004.py @@ -1,4 +1,6 @@ from sleekxmpp.test import * +from sleekxmpp.thirdparty import OrderedDict + import sleekxmpp.plugins.xep_0004 as xep_0004 @@ -47,21 +49,25 @@ class TestDataForms(SleekTest): </message> """) - form['fields'] = [('f1', {'type': 'text-single', - 'label': 'Username', - 'required': True}), - ('f2', {'type': 'text-private', - 'label': 'Password', - 'required': True}), - ('f3', {'type': 'text-multi', - 'label': 'Message', - 'value': 'Enter message.\nA long one even.'}), - ('f4', {'type': 'list-single', - 'label': 'Message Type', - 'options': [{'label': 'Cool!', - 'value': 'cool'}, - {'label': 'Urgh!', - 'value': 'urgh'}]})] + fields = OrderedDict() + fields['f1'] = {'type': 'text-single', + 'label': 'Username', + 'required': True} + fields['f2'] = {'type': 'text-private', + 'label': 'Password', + 'required': True} + fields['f3'] = {'type': 'text-multi', + 'label': 'Message', + 'value': 'Enter message.\nA long one even.'} + fields['f4'] = {'type': 'list-single', + 'label': 'Message Type', + 'options': [{'label': 'Cool!', + 'value': 'cool'}, + {'label': 'Urgh!', + 'value': 'urgh'}]} + form['fields'] = fields + + self.check(msg, """ <message> <x xmlns="jabber:x:data" type="form"> @@ -92,9 +98,8 @@ class TestDataForms(SleekTest): msg = self.Message() form = msg['form'] - form.setFields([ - ('foo', {'type': 'text-single'}), - ('bar', {'type': 'list-multi'})]) + form.add_field(var='foo', ftype='text-single') + form.add_field(var='bar', ftype='list-multi') form.setValues({'foo': 'Foo!', 'bar': ['a', 'b']}) diff --git a/tests/test_stanza_xep_0060.py b/tests/test_stanza_xep_0060.py index d42c11bd..2427b787 100644 --- a/tests/test_stanza_xep_0060.py +++ b/tests/test_stanza_xep_0060.py @@ -182,7 +182,7 @@ class TestPubsubStanzas(SleekTest): <subscribe node="cheese" jid="fritzy@netflint.net/sleekxmpp"> <options node="cheese" jid="fritzy@netflint.net/sleekxmpp"> <x xmlns="jabber:x:data" type="submit"> - <field var="pubsub#title" type="text-single"> + <field var="pubsub#title"> <value>this thing is awesome</value> </field> </x> @@ -306,42 +306,42 @@ class TestPubsubStanzas(SleekTest): <create node="testnode2" /> <configure> <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> </field> - <field var="pubsub#node_type" type="list-single" label="Select the node type"> + <field var="pubsub#node_type"> <value>leaf</value> </field> - <field var="pubsub#title" type="text-single" label="A friendly name for the node" /> - <field var="pubsub#deliver_notifications" type="boolean" label="Deliver event notifications"> + <field var="pubsub#title" /> + <field var="pubsub#deliver_notifications"> <value>1</value> </field> - <field var="pubsub#deliver_payloads" type="boolean" label="Deliver payloads with event notifications"> + <field var="pubsub#deliver_payloads"> <value>1</value> </field> - <field var="pubsub#notify_config" type="boolean" label="Notify subscribers when the node configuration changes" /> - <field var="pubsub#notify_delete" type="boolean" label="Notify subscribers when the node is deleted" /> - <field var="pubsub#notify_retract" type="boolean" label="Notify subscribers when items are removed from the node"> + <field var="pubsub#notify_config" /> + <field var="pubsub#notify_delete" /> + <field var="pubsub#notify_retract"> <value>1</value> </field> - <field var="pubsub#notify_sub" type="boolean" label="Notify owners about new subscribers and unsubscribes" /> - <field var="pubsub#persist_items" type="boolean" label="Persist items in storage" /> - <field var="pubsub#max_items" type="text-single" label="Max # of items to persist"> + <field var="pubsub#notify_sub" /> + <field var="pubsub#persist_items" /> + <field var="pubsub#max_items"> <value>10</value> </field> - <field var="pubsub#subscribe" type="boolean" label="Whether to allow subscriptions"> + <field var="pubsub#subscribe"> <value>1</value> </field> - <field var="pubsub#access_model" type="list-single" label="Specify the subscriber model"> + <field var="pubsub#access_model"> <value>open</value> </field> - <field var="pubsub#publish_model" type="list-single" label="Specify the publisher model"> + <field var="pubsub#publish_model"> <value>publishers</value> </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> </field> - <field var="pubsub#presence_based_delivery" type="boolean" label="Deliver notification only to available users" /> + <field var="pubsub#presence_based_delivery" /> </x> </configure> </pubsub> |