Merge branch 'develop' of github.com:fritzy/SleekXMPP into develop
This commit is contained in:
commit
8f1d0e7a79
2
.gitignore
vendored
2
.gitignore
vendored
@ -1,2 +1,4 @@
|
||||
*.pyc
|
||||
build/
|
||||
dist/
|
||||
MANIFEST
|
||||
|
149
examples/send_client.py
Executable file
149
examples/send_client.py
Executable 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.")
|
16
setup.py
16
setup.py
@ -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,
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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
|
||||
|
@ -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)
|
11
sleekxmpp/plugins/xep_0004/__init__.py
Normal file
11
sleekxmpp/plugins/xep_0004/__init__.py
Normal 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
|
60
sleekxmpp/plugins/xep_0004/dataforms.py
Normal file
60
sleekxmpp/plugins/xep_0004/dataforms.py
Normal 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
|
10
sleekxmpp/plugins/xep_0004/stanza/__init__.py
Normal file
10
sleekxmpp/plugins/xep_0004/stanza/__init__.py
Normal 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
|
167
sleekxmpp/plugins/xep_0004/stanza/field.py
Normal file
167
sleekxmpp/plugins/xep_0004/stanza/field.py
Normal 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
|
250
sleekxmpp/plugins/xep_0004/stanza/form.py
Normal file
250
sleekxmpp/plugins/xep_0004/stanza/form.py
Normal 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
|
@ -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")
|
12
sleekxmpp/plugins/xep_0078/__init__.py
Normal file
12
sleekxmpp/plugins/xep_0078/__init__.py
Normal 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
|
||||
|
108
sleekxmpp/plugins/xep_0078/legacyauth.py
Normal file
108
sleekxmpp/plugins/xep_0078/legacyauth.py
Normal 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
|
43
sleekxmpp/plugins/xep_0078/stanza.py
Normal file
43
sleekxmpp/plugins/xep_0078/stanza.py
Normal 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 = {}
|
||||
|
||||
|
@ -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):
|
||||
|
@ -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)
|
||||
|
@ -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):
|
||||
"""
|
||||
|
4
sleekxmpp/thirdparty/suelta/sasl.py
vendored
4
sleekxmpp/thirdparty/suelta/sasl.py
vendored
@ -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
|
||||
|
@ -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',
|
||||
fields = OrderedDict()
|
||||
fields['f1'] = {'type': 'text-single',
|
||||
'label': 'Username',
|
||||
'required': True}),
|
||||
('f2', {'type': 'text-private',
|
||||
'required': True}
|
||||
fields['f2'] = {'type': 'text-private',
|
||||
'label': 'Password',
|
||||
'required': True}),
|
||||
('f3', {'type': 'text-multi',
|
||||
'required': True}
|
||||
fields['f3'] = {'type': 'text-multi',
|
||||
'label': 'Message',
|
||||
'value': 'Enter message.\nA long one even.'}),
|
||||
('f4', {'type': 'list-single',
|
||||
'value': 'Enter message.\nA long one even.'}
|
||||
fields['f4'] = {'type': 'list-single',
|
||||
'label': 'Message Type',
|
||||
'options': [{'label': 'Cool!',
|
||||
'value': 'cool'},
|
||||
{'label': 'Urgh!',
|
||||
'value': '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']})
|
||||
|
@ -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>
|
||||
|
Loading…
Reference in New Issue
Block a user