mirror of
https://github.com/correl/SleekXMPP.git
synced 2024-11-24 03:00:15 +00:00
Added new XEP-0050 implementation.
Backward incompatibility alert! Please see examples/adhoc_provider.py for how to use the new plugin implementation, or the test examples in the files tests/test_stream_xep_0050.py and tests/test_stanza_xep_0050.py. Major changes: - May now have zero-step commands. Useful if a command is intended to be a dynamic status report that doesn't require any user input. - May use payloads other than data forms, such as a completely custom stanza type. - May include multiple payload items, such as multiple data forms, or a form and a custom stanza type. - Includes a command user API for calling adhoc commands on remote agents and managing the workflow. - Added support for note elements. Todo: - Add prev action support. You may use register_plugin('old_0050') to continue using the previous XEP-0050 implementation.
This commit is contained in:
parent
4916a12b6f
commit
a3d111be12
9 changed files with 2064 additions and 0 deletions
199
examples/adhoc_provider.py
Executable file
199
examples/adhoc_provider.py
Executable file
|
@ -0,0 +1,199 @@
|
||||||
|
#!/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 CommandBot(sleekxmpp.ClientXMPP):
|
||||||
|
|
||||||
|
"""
|
||||||
|
A simple SleekXMPP bot that provides a basic
|
||||||
|
adhoc command.
|
||||||
|
"""
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
|
# We add the command after session_start has fired
|
||||||
|
# to ensure that the correct full JID is used.
|
||||||
|
|
||||||
|
# If using a component, may also pass jid keyword parameter.
|
||||||
|
|
||||||
|
self['xep_0050'].add_command(node='greeting',
|
||||||
|
name='Greeting',
|
||||||
|
handler=self._handle_command)
|
||||||
|
|
||||||
|
def _handle_command(self, iq, session):
|
||||||
|
"""
|
||||||
|
Respond to the intial request for a command.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
iq -- The iq stanza containing the command request.
|
||||||
|
session -- A dictionary of data relevant to the command
|
||||||
|
session. Additional, custom data may be saved
|
||||||
|
here to persist across handler callbacks.
|
||||||
|
"""
|
||||||
|
form = self['xep_0004'].makeForm('form', 'Greeting')
|
||||||
|
form.addField(var='greeting',
|
||||||
|
ftype='text-single',
|
||||||
|
label='Your greeting')
|
||||||
|
|
||||||
|
session['payload'] = form
|
||||||
|
session['next'] = self._handle_command_complete
|
||||||
|
session['has_next'] = False
|
||||||
|
|
||||||
|
# Other useful session values:
|
||||||
|
# session['to'] -- The JID that received the
|
||||||
|
# command request.
|
||||||
|
# session['from'] -- The JID that sent the
|
||||||
|
# command request.
|
||||||
|
# session['has_next'] = True -- There are more steps to complete
|
||||||
|
# session['allow_complete'] = True -- Allow user to finish immediately
|
||||||
|
# and possibly skip steps
|
||||||
|
# session['cancel'] = handler -- Assign a handler for if the user
|
||||||
|
# cancels the command.
|
||||||
|
# session['notes'] = [ -- Add informative notes about the
|
||||||
|
# ('info', 'Info message'), command's results.
|
||||||
|
# ('warning', 'Warning message'),
|
||||||
|
# ('error', 'Error message')]
|
||||||
|
|
||||||
|
return session
|
||||||
|
|
||||||
|
def _handle_command_complete(self, payload, session):
|
||||||
|
"""
|
||||||
|
Process a command result from the user.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
payload -- Either a single item, such as a form, or a list
|
||||||
|
of items or forms if more than one form was
|
||||||
|
provided to the user. The payload may be any
|
||||||
|
stanza, such as jabber:x:oob for out of band
|
||||||
|
data, or jabber:x:data for typical data forms.
|
||||||
|
session -- A dictionary of data relevant to the command
|
||||||
|
session. Additional, custom data may be saved
|
||||||
|
here to persist across handler callbacks.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# In this case (as is typical), the payload is a form
|
||||||
|
form = payload
|
||||||
|
|
||||||
|
greeting = form['values']['greeting']
|
||||||
|
self.send_message(mto=session['from'],
|
||||||
|
mbody="%s, World!" % greeting)
|
||||||
|
|
||||||
|
# Having no return statement is the same as unsetting the 'payload'
|
||||||
|
# and 'next' session values and returning the session.
|
||||||
|
|
||||||
|
# Unless it is the final step, always return the session dictionary.
|
||||||
|
|
||||||
|
session['payload'] = None
|
||||||
|
session['next'] = None
|
||||||
|
|
||||||
|
return session
|
||||||
|
|
||||||
|
|
||||||
|
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 CommandBot and register plugins. Note that while plugins may
|
||||||
|
# have interdependencies, the order in which you register them does
|
||||||
|
# not matter.
|
||||||
|
xmpp = CommandBot(opts.jid, opts.password)
|
||||||
|
xmpp.register_plugin('xep_0030') # Service Discovery
|
||||||
|
xmpp.register_plugin('xep_0004') # Data Forms
|
||||||
|
xmpp.register_plugin('xep_0050') # Adhoc Commands
|
||||||
|
|
||||||
|
# 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.")
|
276
examples/adhoc_user.py
Executable file
276
examples/adhoc_user.py
Executable file
|
@ -0,0 +1,276 @@
|
||||||
|
#!/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 CommandUserBot(sleekxmpp.ClientXMPP):
|
||||||
|
|
||||||
|
"""
|
||||||
|
A simple SleekXMPP bot that uses the adhoc command
|
||||||
|
provided by the adhoc_provider.py example.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, jid, password, other, greeting):
|
||||||
|
sleekxmpp.ClientXMPP.__init__(self, jid, password)
|
||||||
|
|
||||||
|
self.command_provider = other
|
||||||
|
self.greeting = greeting
|
||||||
|
|
||||||
|
# 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)
|
||||||
|
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()
|
||||||
|
|
||||||
|
# We first create a session dictionary containing:
|
||||||
|
# 'next' -- the handler to execute on a successful response
|
||||||
|
# 'error' -- the handler to execute if an error occurs
|
||||||
|
|
||||||
|
# The session may also contain custom data.
|
||||||
|
|
||||||
|
session = {'greeting': self.greeting,
|
||||||
|
'next': self._command_start,
|
||||||
|
'error': self._command_error}
|
||||||
|
|
||||||
|
self['xep_0050'].start_command(jid=self.command_provider,
|
||||||
|
node='greeting',
|
||||||
|
session=session)
|
||||||
|
|
||||||
|
def message(self, msg):
|
||||||
|
"""
|
||||||
|
Process incoming message stanzas.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
msg -- The received message stanza.
|
||||||
|
"""
|
||||||
|
logging.info(msg['body'])
|
||||||
|
|
||||||
|
def _command_start(self, iq, session):
|
||||||
|
"""
|
||||||
|
Process the initial command result.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
iq -- The iq stanza containing the command result.
|
||||||
|
session -- A dictionary of data relevant to the command
|
||||||
|
session. Additional, custom data may be saved
|
||||||
|
here to persist across handler callbacks.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# The greeting command provides a form with a single field:
|
||||||
|
# <x xmlns="jabber:x:data" type="form">
|
||||||
|
# <field var="greeting"
|
||||||
|
# type="text-single"
|
||||||
|
# label="Your greeting" />
|
||||||
|
# </x>
|
||||||
|
|
||||||
|
form = self['xep_0004'].makeForm(ftype='submit')
|
||||||
|
form.addField(var='greeting',
|
||||||
|
value=session['greeting'])
|
||||||
|
|
||||||
|
session['payload'] = form
|
||||||
|
|
||||||
|
# We don't need to process the next result.
|
||||||
|
session['next'] = None
|
||||||
|
|
||||||
|
# Other options include using:
|
||||||
|
# continue_command() -- Continue to the next step in the workflow
|
||||||
|
# cancel_command() -- Stop command execution.
|
||||||
|
|
||||||
|
self['xep_0050'].complete_command(session)
|
||||||
|
|
||||||
|
def _command_error(self, iq, session):
|
||||||
|
"""
|
||||||
|
Process an error that occurs during command execution.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
iq -- The iq stanza containing the error.
|
||||||
|
session -- A dictionary of data relevant to the command
|
||||||
|
session. Additional, custom data may be saved
|
||||||
|
here to persist across handler callbacks.
|
||||||
|
"""
|
||||||
|
logging.error("COMMAND: %s %s" % (iq['error']['condition'],
|
||||||
|
iq['error']['text']))
|
||||||
|
|
||||||
|
# Terminate the command's execution and clear its session.
|
||||||
|
# The session will automatically be cleared if no error
|
||||||
|
# handler is provided.
|
||||||
|
self['xep_0050'].terminate_command(session)
|
||||||
|
|
||||||
|
def _handle_command(self, iq, session):
|
||||||
|
"""
|
||||||
|
Respond to the intial request for a command.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
iq -- The iq stanza containing the command request.
|
||||||
|
session -- A dictionary of data relevant to the command
|
||||||
|
session. Additional, custom data may be saved
|
||||||
|
here to persist across handler callbacks.
|
||||||
|
"""
|
||||||
|
form = self['xep_0004'].makeForm('form', 'Greeting')
|
||||||
|
form.addField(var='greeting',
|
||||||
|
ftype='text-single',
|
||||||
|
label='Your greeting')
|
||||||
|
|
||||||
|
session['payload'] = form
|
||||||
|
session['next'] = self._handle_command_complete
|
||||||
|
session['has_next'] = False
|
||||||
|
|
||||||
|
# Other useful session values:
|
||||||
|
# session['to'] -- The JID that received the
|
||||||
|
# command request.
|
||||||
|
# session['from'] -- The JID that sent the
|
||||||
|
# command request.
|
||||||
|
# session['has_next'] = True -- There are more steps to complete
|
||||||
|
# session['allow_complete'] = True -- Allow user to finish immediately
|
||||||
|
# and possibly skip steps
|
||||||
|
# session['cancel'] = handler -- Assign a handler for if the user
|
||||||
|
# cancels the command.
|
||||||
|
# session['notes'] = [ -- Add informative notes about the
|
||||||
|
# ('info', 'Info message'), command's results.
|
||||||
|
# ('warning', 'Warning message'),
|
||||||
|
# ('error', 'Error message')]
|
||||||
|
|
||||||
|
return session
|
||||||
|
|
||||||
|
def _handle_command_complete(self, payload, session):
|
||||||
|
"""
|
||||||
|
Process a command result from the user.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
payload -- Either a single item, such as a form, or a list
|
||||||
|
of items or forms if more than one form was
|
||||||
|
provided to the user. The payload may be any
|
||||||
|
stanza, such as jabber:x:oob for out of band
|
||||||
|
data, or jabber:x:data for typical data forms.
|
||||||
|
session -- A dictionary of data relevant to the command
|
||||||
|
session. Additional, custom data may be saved
|
||||||
|
here to persist across handler callbacks.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# In this case (as is typical), the payload is a form
|
||||||
|
form = payload
|
||||||
|
|
||||||
|
greeting = form['values']['greeting']
|
||||||
|
self.send_message(mto=session['from'],
|
||||||
|
mbody="%s, World!" % greeting)
|
||||||
|
|
||||||
|
# Having no return statement is the same as unsetting the 'payload'
|
||||||
|
# and 'next' session values and returning the session.
|
||||||
|
|
||||||
|
# Unless it is the final step, always return the session dictionary.
|
||||||
|
|
||||||
|
session['payload'] = None
|
||||||
|
session['next'] = None
|
||||||
|
|
||||||
|
return session
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
# Setup the command line arguments.
|
||||||
|
optp = OptionParser()
|
||||||
|
|
||||||
|
# Output verbosity options.
|
||||||
|
optp.add_option('-q', '--quiet', help='set logging to ERROR',
|
||||||
|
action='store_const', dest='loglevel',
|
||||||
|
const=logging.ERROR, default=logging.INFO)
|
||||||
|
optp.add_option('-d', '--debug', help='set logging to DEBUG',
|
||||||
|
action='store_const', dest='loglevel',
|
||||||
|
const=logging.DEBUG, default=logging.INFO)
|
||||||
|
optp.add_option('-v', '--verbose', help='set logging to COMM',
|
||||||
|
action='store_const', dest='loglevel',
|
||||||
|
const=5, default=logging.INFO)
|
||||||
|
|
||||||
|
# JID and password options.
|
||||||
|
optp.add_option("-j", "--jid", dest="jid",
|
||||||
|
help="JID to use")
|
||||||
|
optp.add_option("-p", "--password", dest="password",
|
||||||
|
help="password to use")
|
||||||
|
optp.add_option("-o", "--other", dest="other",
|
||||||
|
help="JID providing commands")
|
||||||
|
optp.add_option("-g", "--greeting", dest="greeting",
|
||||||
|
help="Greeting")
|
||||||
|
|
||||||
|
opts, args = optp.parse_args()
|
||||||
|
|
||||||
|
# Setup logging.
|
||||||
|
logging.basicConfig(level=opts.loglevel,
|
||||||
|
format='%(levelname)-8s %(message)s')
|
||||||
|
|
||||||
|
if opts.jid is None:
|
||||||
|
opts.jid = raw_input("Username: ")
|
||||||
|
if opts.password is None:
|
||||||
|
opts.password = getpass.getpass("Password: ")
|
||||||
|
if opts.other is None:
|
||||||
|
opts.other = raw_input("JID Providing Commands: ")
|
||||||
|
if opts.greeting is None:
|
||||||
|
opts.other = raw_input("Greeting: ")
|
||||||
|
|
||||||
|
# Setup the CommandBot and register plugins. Note that while plugins may
|
||||||
|
# have interdependencies, the order in which you register them does
|
||||||
|
# not matter.
|
||||||
|
xmpp = CommandUserBot(opts.jid, opts.password, opts.other, opts.greeting)
|
||||||
|
xmpp.register_plugin('xep_0030') # Service Discovery
|
||||||
|
xmpp.register_plugin('xep_0004') # Data Forms
|
||||||
|
xmpp.register_plugin('xep_0050') # Adhoc Commands
|
||||||
|
|
||||||
|
# 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
setup.py
1
setup.py
|
@ -49,6 +49,7 @@ packages = [ 'sleekxmpp',
|
||||||
'sleekxmpp/plugins/xep_0009/stanza',
|
'sleekxmpp/plugins/xep_0009/stanza',
|
||||||
'sleekxmpp/plugins/xep_0030',
|
'sleekxmpp/plugins/xep_0030',
|
||||||
'sleekxmpp/plugins/xep_0030/stanza',
|
'sleekxmpp/plugins/xep_0030/stanza',
|
||||||
|
'sleekxmpp/plugins/xep_0050',
|
||||||
'sleekxmpp/plugins/xep_0059',
|
'sleekxmpp/plugins/xep_0059',
|
||||||
'sleekxmpp/plugins/xep_0085',
|
'sleekxmpp/plugins/xep_0085',
|
||||||
'sleekxmpp/plugins/xep_0092',
|
'sleekxmpp/plugins/xep_0092',
|
||||||
|
|
10
sleekxmpp/plugins/xep_0050/__init__.py
Normal file
10
sleekxmpp/plugins/xep_0050/__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_0050.stanza import Command
|
||||||
|
from sleekxmpp.plugins.xep_0050.adhoc import xep_0050
|
593
sleekxmpp/plugins/xep_0050/adhoc.py
Normal file
593
sleekxmpp/plugins/xep_0050/adhoc.py
Normal file
|
@ -0,0 +1,593 @@
|
||||||
|
"""
|
||||||
|
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 logging
|
||||||
|
import time
|
||||||
|
|
||||||
|
from sleekxmpp import Iq
|
||||||
|
from sleekxmpp.xmlstream.handler import Callback
|
||||||
|
from sleekxmpp.xmlstream.matcher import StanzaPath
|
||||||
|
from sleekxmpp.xmlstream import register_stanza_plugin, JID
|
||||||
|
from sleekxmpp.plugins.base import base_plugin
|
||||||
|
from sleekxmpp.plugins.xep_0050 import stanza
|
||||||
|
from sleekxmpp.plugins.xep_0050 import Command
|
||||||
|
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class xep_0050(base_plugin):
|
||||||
|
|
||||||
|
"""
|
||||||
|
XEP-0050: Ad-Hoc Commands
|
||||||
|
|
||||||
|
XMPP's Adhoc Commands provides a generic workflow mechanism for
|
||||||
|
interacting with applications. The result is similar to menu selections
|
||||||
|
and multi-step dialogs in normal desktop applications. Clients do not
|
||||||
|
need to know in advance what commands are provided by any particular
|
||||||
|
application or agent. While adhoc commands provide similar functionality
|
||||||
|
to Jabber-RPC, adhoc commands are used primarily for human interaction.
|
||||||
|
|
||||||
|
Also see <http://xmpp.org/extensions/xep-0050.html>
|
||||||
|
|
||||||
|
Configuration Values:
|
||||||
|
threaded -- Indicates if command events should be threaded.
|
||||||
|
Defaults to True.
|
||||||
|
|
||||||
|
Events:
|
||||||
|
command_execute -- Received a command with action="execute"
|
||||||
|
command_next -- Received a command with action="next"
|
||||||
|
command_complete -- Received a command with action="complete"
|
||||||
|
command_cancel -- Received a command with action="cancel"
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
threaded -- Indicates if command events should be threaded.
|
||||||
|
Defaults to True.
|
||||||
|
commands -- A dictionary mapping JID/node pairs to command
|
||||||
|
names and handlers.
|
||||||
|
sessions -- A dictionary or equivalent backend mapping
|
||||||
|
session IDs to dictionaries containing data
|
||||||
|
relevant to a command's session.
|
||||||
|
|
||||||
|
Methods:
|
||||||
|
plugin_init -- Overrides base_plugin.plugin_init
|
||||||
|
post_init -- Overrides base_plugin.post_init
|
||||||
|
new_session -- Return a new session ID.
|
||||||
|
prep_handlers -- Placeholder. May call with a list of handlers
|
||||||
|
to prepare them for use with the session storage
|
||||||
|
backend, if needed.
|
||||||
|
set_backend -- Replace the default session storage with some
|
||||||
|
external storage mechanism, such as a database.
|
||||||
|
The provided backend wrapper must be able to
|
||||||
|
act using the same syntax as a dictionary.
|
||||||
|
add_command -- Add a command for use by external entitites.
|
||||||
|
get_commands -- Retrieve a list of commands provided by a
|
||||||
|
remote agent.
|
||||||
|
send_command -- Send a command request to a remote agent.
|
||||||
|
start_command -- Command user API: initiate a command session
|
||||||
|
continue_command -- Command user API: proceed to the next step
|
||||||
|
cancel_command -- Command user API: cancel a command
|
||||||
|
complete_command -- Command user API: finish a command
|
||||||
|
terminate_command -- Command user API: delete a command's session
|
||||||
|
"""
|
||||||
|
|
||||||
|
def plugin_init(self):
|
||||||
|
"""Start the XEP-0050 plugin."""
|
||||||
|
self.xep = '0050'
|
||||||
|
self.description = 'Ad-Hoc Commands'
|
||||||
|
self.stanza = stanza
|
||||||
|
|
||||||
|
self.threaded = self.config.get('threaded', True)
|
||||||
|
self.commands = {}
|
||||||
|
self.sessions = self.config.get('session_db', {})
|
||||||
|
|
||||||
|
self.xmpp.register_handler(
|
||||||
|
Callback("Ad-Hoc Execute",
|
||||||
|
StanzaPath('iq@type=set/command'),
|
||||||
|
self._handle_command))
|
||||||
|
|
||||||
|
self.xmpp.register_handler(
|
||||||
|
Callback("Ad-Hoc Result",
|
||||||
|
StanzaPath('iq@type=result/command'),
|
||||||
|
self._handle_command_result))
|
||||||
|
|
||||||
|
self.xmpp.register_handler(
|
||||||
|
Callback("Ad-Hoc Error",
|
||||||
|
StanzaPath('iq@type=error/command'),
|
||||||
|
self._handle_command_result))
|
||||||
|
|
||||||
|
register_stanza_plugin(Iq, stanza.Command)
|
||||||
|
|
||||||
|
self.xmpp.add_event_handler('command_execute',
|
||||||
|
self._handle_command_start,
|
||||||
|
threaded=self.threaded)
|
||||||
|
self.xmpp.add_event_handler('command_next',
|
||||||
|
self._handle_command_next,
|
||||||
|
threaded=self.threaded)
|
||||||
|
self.xmpp.add_event_handler('command_cancel',
|
||||||
|
self._handle_command_cancel,
|
||||||
|
threaded=self.threaded)
|
||||||
|
self.xmpp.add_event_handler('command_complete',
|
||||||
|
self._handle_command_complete,
|
||||||
|
threaded=self.threaded)
|
||||||
|
|
||||||
|
def post_init(self):
|
||||||
|
"""Handle cross-plugin interactions."""
|
||||||
|
base_plugin.post_init(self)
|
||||||
|
self.xmpp['xep_0030'].add_feature(Command.namespace)
|
||||||
|
|
||||||
|
def set_backend(self, db):
|
||||||
|
"""
|
||||||
|
Replace the default session storage dictionary with
|
||||||
|
a generic, external data storage mechanism.
|
||||||
|
|
||||||
|
The replacement backend must be able to interact through
|
||||||
|
the same syntax and interfaces as a normal dictionary.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
db -- The new session storage mechanism.
|
||||||
|
"""
|
||||||
|
self.sessions = db
|
||||||
|
|
||||||
|
def prep_handlers(self, handlers, **kwargs):
|
||||||
|
"""
|
||||||
|
Prepare a list of functions for use by the backend service.
|
||||||
|
|
||||||
|
Intended to be replaced by the backend service as needed.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
handlers -- A list of function pointers
|
||||||
|
**kwargs -- Any additional parameters required by the backend.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
# =================================================================
|
||||||
|
# Server side (command provider) API
|
||||||
|
|
||||||
|
def add_command(self, jid=None, node=None, name='', handler=None):
|
||||||
|
"""
|
||||||
|
Make a new command available to external entities.
|
||||||
|
|
||||||
|
Access control may be implemented in the provided handler.
|
||||||
|
|
||||||
|
Command workflow is done across a sequence of command handlers. The
|
||||||
|
first handler is given the intial Iq stanza of the request in order
|
||||||
|
to support access control. Subsequent handlers are given only the
|
||||||
|
payload items of the command. All handlers will receive the command's
|
||||||
|
session data.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
jid -- The JID that will expose the command.
|
||||||
|
node -- The node associated with the command.
|
||||||
|
name -- A human readable name for the command.
|
||||||
|
handler -- A function that will generate the response to the
|
||||||
|
initial command request, as well as enforcing any
|
||||||
|
access control policies.
|
||||||
|
"""
|
||||||
|
if jid is None:
|
||||||
|
jid = self.xmpp.boundjid
|
||||||
|
elif isinstance(jid, str):
|
||||||
|
jid = JID(jid)
|
||||||
|
item_jid = jid.full
|
||||||
|
|
||||||
|
# Client disco uses only the bare JID
|
||||||
|
if self.xmpp.is_component:
|
||||||
|
jid = jid.full
|
||||||
|
else:
|
||||||
|
jid = jid.bare
|
||||||
|
|
||||||
|
self.xmpp['xep_0030'].add_identity(category='automation',
|
||||||
|
itype='command-list',
|
||||||
|
name='Ad-Hoc commands',
|
||||||
|
node=Command.namespace,
|
||||||
|
jid=jid)
|
||||||
|
self.xmpp['xep_0030'].add_item(jid=item_jid,
|
||||||
|
name=name,
|
||||||
|
node=Command.namespace,
|
||||||
|
subnode=node,
|
||||||
|
ijid=jid)
|
||||||
|
self.xmpp['xep_0030'].add_identity(category='automation',
|
||||||
|
itype='command-node',
|
||||||
|
name=name,
|
||||||
|
node=node,
|
||||||
|
jid=jid)
|
||||||
|
self.xmpp['xep_0030'].add_feature(Command.namespace, None, jid)
|
||||||
|
|
||||||
|
self.commands[(item_jid, node)] = (name, handler)
|
||||||
|
|
||||||
|
def new_session(self):
|
||||||
|
"""Return a new session ID."""
|
||||||
|
return str(time.time()) + '-' + self.xmpp.new_id()
|
||||||
|
|
||||||
|
def _handle_command(self, iq):
|
||||||
|
"""Raise command events based on the command action."""
|
||||||
|
self.xmpp.event('command_%s' % iq['command']['action'], iq)
|
||||||
|
|
||||||
|
def _handle_command_start(self, iq):
|
||||||
|
"""
|
||||||
|
Process an initial request to execute a command.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
iq -- The command execution request.
|
||||||
|
"""
|
||||||
|
sessionid = self.new_session()
|
||||||
|
node = iq['command']['node']
|
||||||
|
key = (iq['to'].full, node)
|
||||||
|
name, handler = self.commands.get(key, ('Not found', None))
|
||||||
|
if not handler:
|
||||||
|
log.debug('Command not found: %s, %s' % (key, self.commands))
|
||||||
|
|
||||||
|
initial_session = {'id': sessionid,
|
||||||
|
'from': iq['from'],
|
||||||
|
'to': iq['to'],
|
||||||
|
'node': node,
|
||||||
|
'payload': None,
|
||||||
|
'interfaces': '',
|
||||||
|
'payload_classes': None,
|
||||||
|
'notes': None,
|
||||||
|
'has_next': False,
|
||||||
|
'allow_complete': False,
|
||||||
|
'allow_prev': False,
|
||||||
|
'past': [],
|
||||||
|
'next': None,
|
||||||
|
'prev': None,
|
||||||
|
'cancel': None}
|
||||||
|
|
||||||
|
session = handler(iq, initial_session)
|
||||||
|
|
||||||
|
self._process_command_response(iq, session)
|
||||||
|
|
||||||
|
def _handle_command_next(self, iq):
|
||||||
|
"""
|
||||||
|
Process a request for the next step in the workflow
|
||||||
|
for a command with multiple steps.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
iq -- The command continuation request.
|
||||||
|
"""
|
||||||
|
sessionid = iq['command']['sessionid']
|
||||||
|
session = self.sessions[sessionid]
|
||||||
|
|
||||||
|
handler = session['next']
|
||||||
|
interfaces = session['interfaces']
|
||||||
|
results = []
|
||||||
|
for stanza in iq['command']['substanzas']:
|
||||||
|
if stanza.plugin_attrib in interfaces:
|
||||||
|
results.append(stanza)
|
||||||
|
if len(results) == 1:
|
||||||
|
results = results[0]
|
||||||
|
|
||||||
|
session = handler(results, session)
|
||||||
|
|
||||||
|
self._process_command_response(iq, session)
|
||||||
|
|
||||||
|
def _process_command_response(self, iq, session):
|
||||||
|
"""
|
||||||
|
Generate a command reply stanza based on the
|
||||||
|
provided session data.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
iq -- The command request stanza.
|
||||||
|
session -- A dictionary of relevant session data.
|
||||||
|
"""
|
||||||
|
sessionid = session['id']
|
||||||
|
|
||||||
|
payload = session['payload']
|
||||||
|
if not isinstance(payload, list):
|
||||||
|
payload = [payload]
|
||||||
|
|
||||||
|
session['interfaces'] = [item.plugin_attrib for item in payload]
|
||||||
|
session['payload_classes'] = [item.__class__ for item in payload]
|
||||||
|
|
||||||
|
self.sessions[sessionid] = session
|
||||||
|
|
||||||
|
for item in payload:
|
||||||
|
register_stanza_plugin(Command, item.__class__, iterable=True)
|
||||||
|
|
||||||
|
iq.reply()
|
||||||
|
iq['command']['node'] = session['node']
|
||||||
|
iq['command']['sessionid'] = session['id']
|
||||||
|
|
||||||
|
if session['next'] is None:
|
||||||
|
iq['command']['actions'] = []
|
||||||
|
iq['command']['status'] = 'completed'
|
||||||
|
elif session['has_next']:
|
||||||
|
actions = ['next']
|
||||||
|
if session['allow_complete']:
|
||||||
|
actions.append('complete')
|
||||||
|
if session['allow_prev']:
|
||||||
|
actions.append('prev')
|
||||||
|
iq['command']['actions'] = actions
|
||||||
|
iq['command']['status'] = 'executing'
|
||||||
|
else:
|
||||||
|
iq['command']['actions'] = ['complete']
|
||||||
|
iq['command']['status'] = 'executing'
|
||||||
|
|
||||||
|
iq['command']['notes'] = session['notes']
|
||||||
|
|
||||||
|
for item in payload:
|
||||||
|
iq['command'].append(item)
|
||||||
|
|
||||||
|
iq.send()
|
||||||
|
|
||||||
|
def _handle_command_cancel(self, iq):
|
||||||
|
"""
|
||||||
|
Process a request to cancel a command's execution.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
iq -- The command cancellation request.
|
||||||
|
"""
|
||||||
|
node = iq['command']['node']
|
||||||
|
sessionid = iq['command']['sessionid']
|
||||||
|
session = self.sessions[sessionid]
|
||||||
|
handler = session['cancel']
|
||||||
|
|
||||||
|
if handler:
|
||||||
|
handler(iq, session)
|
||||||
|
|
||||||
|
try:
|
||||||
|
del self.sessions[sessionid]
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
iq.reply()
|
||||||
|
iq['command']['node'] = node
|
||||||
|
iq['command']['sessionid'] = sessionid
|
||||||
|
iq['command']['status'] = 'canceled'
|
||||||
|
iq['command']['notes'] = session['notes']
|
||||||
|
iq.send()
|
||||||
|
|
||||||
|
def _handle_command_complete(self, iq):
|
||||||
|
"""
|
||||||
|
Process a request to finish the execution of command
|
||||||
|
and terminate the workflow.
|
||||||
|
|
||||||
|
All data related to the command session will be removed.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
iq -- The command completion request.
|
||||||
|
"""
|
||||||
|
node = iq['command']['node']
|
||||||
|
sessionid = iq['command']['sessionid']
|
||||||
|
session = self.sessions[sessionid]
|
||||||
|
handler = session['next']
|
||||||
|
interfaces = session['interfaces']
|
||||||
|
results = []
|
||||||
|
for stanza in iq['command']['substanzas']:
|
||||||
|
if stanza.plugin_attrib in interfaces:
|
||||||
|
results.append(stanza)
|
||||||
|
if len(results) == 1:
|
||||||
|
results = results[0]
|
||||||
|
|
||||||
|
if handler:
|
||||||
|
handler(results, session)
|
||||||
|
|
||||||
|
iq.reply()
|
||||||
|
iq['command']['node'] = node
|
||||||
|
iq['command']['sessionid'] = sessionid
|
||||||
|
iq['command']['actions'] = []
|
||||||
|
iq['command']['status'] = 'completed'
|
||||||
|
iq['command']['notes'] = session['notes']
|
||||||
|
iq.send()
|
||||||
|
|
||||||
|
del self.sessions[sessionid]
|
||||||
|
|
||||||
|
|
||||||
|
# =================================================================
|
||||||
|
# Client side (command user) API
|
||||||
|
|
||||||
|
def get_commands(self, jid, **kwargs):
|
||||||
|
"""
|
||||||
|
Return a list of commands provided by a given JID.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
jid -- The JID to query for commands.
|
||||||
|
local -- If true, then the query is for a JID/node
|
||||||
|
combination handled by this Sleek instance and
|
||||||
|
no stanzas need to be sent.
|
||||||
|
Otherwise, a disco stanza must be sent to the
|
||||||
|
remove JID to retrieve the items.
|
||||||
|
ifrom -- Specifiy the sender's JID.
|
||||||
|
block -- If true, block and wait for the stanzas' reply.
|
||||||
|
timeout -- The time in seconds to block while waiting for
|
||||||
|
a reply. If None, then wait indefinitely.
|
||||||
|
callback -- Optional callback to execute when a reply is
|
||||||
|
received instead of blocking and waiting for
|
||||||
|
the reply.
|
||||||
|
iterator -- If True, return a result set iterator using
|
||||||
|
the XEP-0059 plugin, if the plugin is loaded.
|
||||||
|
Otherwise the parameter is ignored.
|
||||||
|
"""
|
||||||
|
return self.xmpp['xep_0030'].get_items(jid=jid,
|
||||||
|
node=Command.namespace,
|
||||||
|
**kwargs)
|
||||||
|
|
||||||
|
def send_command(self, jid, node, ifrom=None, action='execute',
|
||||||
|
payload=None, sessionid=None, **kwargs):
|
||||||
|
"""
|
||||||
|
Create and send a command stanza, without using the provided
|
||||||
|
workflow management APIs.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
jid -- The JID to send the command request or result.
|
||||||
|
node -- The node for the command.
|
||||||
|
ifrom -- Specify the sender's JID.
|
||||||
|
action -- May be one of: execute, cancel, complete,
|
||||||
|
or cancel.
|
||||||
|
payload -- Either a list of payload items, or a single
|
||||||
|
payload item such as a data form.
|
||||||
|
sessionid -- The current session's ID value.
|
||||||
|
block -- Specify if the send call will block until a
|
||||||
|
response is received, or a timeout occurs.
|
||||||
|
Defaults to True.
|
||||||
|
timeout -- The length of time (in seconds) to wait for a
|
||||||
|
response before exiting the send call
|
||||||
|
if blocking is used. Defaults to
|
||||||
|
sleekxmpp.xmlstream.RESPONSE_TIMEOUT
|
||||||
|
callback -- Optional reference to a stream handler
|
||||||
|
function. Will be executed when a reply
|
||||||
|
stanza is received.
|
||||||
|
"""
|
||||||
|
iq = self.xmpp.Iq()
|
||||||
|
iq['type'] = 'set'
|
||||||
|
iq['to'] = jid
|
||||||
|
if ifrom:
|
||||||
|
iq['from'] = ifrom
|
||||||
|
iq['command']['node'] = node
|
||||||
|
iq['command']['action'] = action
|
||||||
|
if sessionid is not None:
|
||||||
|
iq['command']['sessionid'] = sessionid
|
||||||
|
if payload is not None:
|
||||||
|
if not isinstance(payload, list):
|
||||||
|
payload = [payload]
|
||||||
|
for item in payload:
|
||||||
|
iq['command'].append(item)
|
||||||
|
return iq.send(**kwargs)
|
||||||
|
|
||||||
|
def start_command(self, jid, node, session, ifrom=None):
|
||||||
|
"""
|
||||||
|
Initiate executing a command provided by a remote agent.
|
||||||
|
|
||||||
|
The workflow provided is always non-blocking.
|
||||||
|
|
||||||
|
The provided session dictionary should contain:
|
||||||
|
next -- A handler for processing the command result.
|
||||||
|
error -- A handler for processing any error stanzas
|
||||||
|
generated by the request.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
jid -- The JID to send the command request.
|
||||||
|
node -- The node for the desired command.
|
||||||
|
session -- A dictionary of relevant session data.
|
||||||
|
ifrom -- Optionally specify the sender's JID.
|
||||||
|
"""
|
||||||
|
session['jid'] = jid
|
||||||
|
session['node'] = node
|
||||||
|
session['timestamp'] = time.time()
|
||||||
|
session['payload'] = None
|
||||||
|
iq = self.xmpp.Iq()
|
||||||
|
iq['type'] = 'set'
|
||||||
|
iq['to'] = jid
|
||||||
|
if ifrom:
|
||||||
|
iq['from'] = ifrom
|
||||||
|
session['from'] = ifrom
|
||||||
|
iq['command']['node'] = node
|
||||||
|
iq['command']['action'] = 'execute'
|
||||||
|
sessionid = 'client:pending_' + iq['id']
|
||||||
|
session['id'] = sessionid
|
||||||
|
self.sessions[sessionid] = session
|
||||||
|
iq.send(block=False)
|
||||||
|
|
||||||
|
def continue_command(self, session):
|
||||||
|
"""
|
||||||
|
Execute the next action of the command.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
session -- All stored data relevant to the current
|
||||||
|
command session.
|
||||||
|
"""
|
||||||
|
sessionid = 'client:' + session['id']
|
||||||
|
self.sessions[sessionid] = session
|
||||||
|
|
||||||
|
self.send_command(session['jid'],
|
||||||
|
session['node'],
|
||||||
|
ifrom=session.get('from', None),
|
||||||
|
action='next',
|
||||||
|
payload=session.get('payload', None),
|
||||||
|
sessionid=session['id'])
|
||||||
|
|
||||||
|
def cancel_command(self, session):
|
||||||
|
"""
|
||||||
|
Cancel the execution of a command.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
session -- All stored data relevant to the current
|
||||||
|
command session.
|
||||||
|
"""
|
||||||
|
sessionid = 'client:' + session['id']
|
||||||
|
self.sessions[sessionid] = session
|
||||||
|
|
||||||
|
self.send_command(session['jid'],
|
||||||
|
session['node'],
|
||||||
|
ifrom=session.get('from', None),
|
||||||
|
action='cancel',
|
||||||
|
payload=session.get('payload', None),
|
||||||
|
sessionid=session['id'])
|
||||||
|
|
||||||
|
def complete_command(self, session):
|
||||||
|
"""
|
||||||
|
Finish the execution of a command workflow.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
session -- All stored data relevant to the current
|
||||||
|
command session.
|
||||||
|
"""
|
||||||
|
sessionid = 'client:' + session['id']
|
||||||
|
self.sessions[sessionid] = session
|
||||||
|
|
||||||
|
self.send_command(session['jid'],
|
||||||
|
session['node'],
|
||||||
|
ifrom=session.get('from', None),
|
||||||
|
action='complete',
|
||||||
|
payload=session.get('payload', None),
|
||||||
|
sessionid=session['id'])
|
||||||
|
|
||||||
|
def terminate_command(self, session):
|
||||||
|
"""
|
||||||
|
Delete a command's session after a command has completed
|
||||||
|
or an error has occured.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
session -- All stored data relevant to the current
|
||||||
|
command session.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
del self.sessions[session['id']]
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _handle_command_result(self, iq):
|
||||||
|
"""
|
||||||
|
Process the results of a command request.
|
||||||
|
|
||||||
|
Will execute the 'next' handler stored in the session
|
||||||
|
data, or the 'error' handler depending on the Iq's type.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
iq -- The command response.
|
||||||
|
"""
|
||||||
|
sessionid = 'client:' + iq['command']['sessionid']
|
||||||
|
pending = False
|
||||||
|
|
||||||
|
if sessionid not in self.sessions:
|
||||||
|
pending = True
|
||||||
|
pendingid = 'client:pending_' + iq['id']
|
||||||
|
if pendingid not in self.sessions:
|
||||||
|
return
|
||||||
|
sessionid = pendingid
|
||||||
|
|
||||||
|
session = self.sessions[sessionid]
|
||||||
|
sessionid = 'client:' + iq['command']['sessionid']
|
||||||
|
session['id'] = iq['command']['sessionid']
|
||||||
|
|
||||||
|
self.sessions[sessionid] = session
|
||||||
|
|
||||||
|
if pending:
|
||||||
|
del self.sessions[pendingid]
|
||||||
|
|
||||||
|
handler_type = 'next'
|
||||||
|
if iq['type'] == 'error':
|
||||||
|
handler_type = 'error'
|
||||||
|
handler = session.get(handler_type, None)
|
||||||
|
if handler:
|
||||||
|
handler(iq, session)
|
||||||
|
elif iq['type'] == 'error':
|
||||||
|
self.terminate_command(session)
|
||||||
|
|
||||||
|
if iq['command']['status'] == 'completed':
|
||||||
|
self.terminate_command(session)
|
185
sleekxmpp/plugins/xep_0050/stanza.py
Normal file
185
sleekxmpp/plugins/xep_0050/stanza.py
Normal file
|
@ -0,0 +1,185 @@
|
||||||
|
"""
|
||||||
|
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 Command(ElementBase):
|
||||||
|
|
||||||
|
"""
|
||||||
|
XMPP's Adhoc Commands provides a generic workflow mechanism for
|
||||||
|
interacting with applications. The result is similar to menu selections
|
||||||
|
and multi-step dialogs in normal desktop applications. Clients do not
|
||||||
|
need to know in advance what commands are provided by any particular
|
||||||
|
application or agent. While adhoc commands provide similar functionality
|
||||||
|
to Jabber-RPC, adhoc commands are used primarily for human interaction.
|
||||||
|
|
||||||
|
Also see <http://xmpp.org/extensions/xep-0050.html>
|
||||||
|
|
||||||
|
Example command stanzas:
|
||||||
|
<iq type="set">
|
||||||
|
<command xmlns="http://jabber.org/protocol/commands"
|
||||||
|
node="run_foo"
|
||||||
|
action="execute" />
|
||||||
|
</iq>
|
||||||
|
|
||||||
|
<iq type="result">
|
||||||
|
<command xmlns="http://jabber.org/protocol/commands"
|
||||||
|
node="run_foo"
|
||||||
|
sessionid="12345"
|
||||||
|
status="executing">
|
||||||
|
<actions>
|
||||||
|
<complete />
|
||||||
|
</actions>
|
||||||
|
<note type="info">Information!</note>
|
||||||
|
<x xmlns="jabber:x:data">
|
||||||
|
<field var="greeting"
|
||||||
|
type="text-single"
|
||||||
|
label="Greeting" />
|
||||||
|
</x>
|
||||||
|
</command>
|
||||||
|
</iq>
|
||||||
|
|
||||||
|
Stanza Interface:
|
||||||
|
action -- The action to perform.
|
||||||
|
actions -- The set of allowable next actions.
|
||||||
|
node -- The node associated with the command.
|
||||||
|
notes -- A list of tuples for informative notes.
|
||||||
|
sessionid -- A unique identifier for a command session.
|
||||||
|
status -- May be one of: canceled, completed, or executing.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
actions -- A set of allowed action values.
|
||||||
|
statuses -- A set of allowed status values.
|
||||||
|
next_actions -- A set of allowed next action names.
|
||||||
|
|
||||||
|
Methods:
|
||||||
|
get_action -- Return the requested action.
|
||||||
|
get_actions -- Return the allowable next actions.
|
||||||
|
set_actions -- Set the allowable next actions.
|
||||||
|
del_actions -- Remove the current set of next actions.
|
||||||
|
get_notes -- Return a list of informative note data.
|
||||||
|
set_notes -- Set informative notes.
|
||||||
|
del_notes -- Remove any note data.
|
||||||
|
add_note -- Add a single note.
|
||||||
|
"""
|
||||||
|
|
||||||
|
name = 'command'
|
||||||
|
namespace = 'http://jabber.org/protocol/commands'
|
||||||
|
plugin_attrib = 'command'
|
||||||
|
interfaces = set(('action', 'sessionid', 'node',
|
||||||
|
'status', 'actions', 'notes'))
|
||||||
|
actions = set(('cancel', 'complete', 'execute', 'next', 'prev'))
|
||||||
|
statuses = set(('canceled', 'completed', 'executing'))
|
||||||
|
next_actions = set(('prev', 'next', 'complete'))
|
||||||
|
|
||||||
|
def get_action(self):
|
||||||
|
"""
|
||||||
|
Return the value of the action attribute.
|
||||||
|
|
||||||
|
If the Iq stanza's type is "set" then use a default
|
||||||
|
value of "execute".
|
||||||
|
"""
|
||||||
|
if self.parent()['type'] == 'set':
|
||||||
|
return self._get_attr('action', default='execute')
|
||||||
|
return self._get_attr('action')
|
||||||
|
|
||||||
|
def set_actions(self, values):
|
||||||
|
"""
|
||||||
|
Assign the set of allowable next actions.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
values -- A list containing any combination of:
|
||||||
|
'prev', 'next', and 'complete'
|
||||||
|
"""
|
||||||
|
self.del_actions()
|
||||||
|
if values:
|
||||||
|
self._set_sub_text('{%s}actions' % self.namespace, '', True)
|
||||||
|
actions = self.find('{%s}actions' % self.namespace)
|
||||||
|
for val in values:
|
||||||
|
if val in self.next_actions:
|
||||||
|
action = ET.Element('{%s}%s' % (self.namespace, val))
|
||||||
|
actions.append(action)
|
||||||
|
|
||||||
|
def get_actions(self):
|
||||||
|
"""
|
||||||
|
Return the set of allowable next actions.
|
||||||
|
"""
|
||||||
|
actions = []
|
||||||
|
actions_xml = self.find('{%s}actions' % self.namespace)
|
||||||
|
if actions_xml is not None:
|
||||||
|
for action in self.next_actions:
|
||||||
|
action_xml = actions_xml.find('{%s}%s' % (self.namespace,
|
||||||
|
action))
|
||||||
|
if action_xml is not None:
|
||||||
|
actions.append(action)
|
||||||
|
return actions
|
||||||
|
|
||||||
|
def del_actions(self):
|
||||||
|
"""
|
||||||
|
Remove all allowable next actions.
|
||||||
|
"""
|
||||||
|
self._del_sub('{%s}actions' % self.namespace)
|
||||||
|
|
||||||
|
def get_notes(self):
|
||||||
|
"""
|
||||||
|
Return a list of note information.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
[('info', 'Some informative data'),
|
||||||
|
('warning', 'Use caution'),
|
||||||
|
('error', 'The command ran, but had errors')]
|
||||||
|
"""
|
||||||
|
notes = []
|
||||||
|
notes_xml = self.findall('{%s}note' % self.namespace)
|
||||||
|
for note in notes_xml:
|
||||||
|
notes.append((note.attrib.get('type', 'info'),
|
||||||
|
note.text))
|
||||||
|
return notes
|
||||||
|
|
||||||
|
def set_notes(self, notes):
|
||||||
|
"""
|
||||||
|
Add multiple notes to the command result.
|
||||||
|
|
||||||
|
Each note is a tuple, with the first item being one of:
|
||||||
|
'info', 'warning', or 'error', and the second item being
|
||||||
|
any human readable message.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
[('info', 'Some informative data'),
|
||||||
|
('warning', 'Use caution'),
|
||||||
|
('error', 'The command ran, but had errors')]
|
||||||
|
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
notes -- A list of tuples of note information.
|
||||||
|
"""
|
||||||
|
self.del_notes()
|
||||||
|
for note in notes:
|
||||||
|
self.add_note(note[1], note[0])
|
||||||
|
|
||||||
|
def del_notes(self):
|
||||||
|
"""
|
||||||
|
Remove all notes associated with the command result.
|
||||||
|
"""
|
||||||
|
notes_xml = self.findall('{%s}note' % self.namespace)
|
||||||
|
for note in notes_xml:
|
||||||
|
self.xml.remove(note)
|
||||||
|
|
||||||
|
def add_note(self, msg='', ntype='info'):
|
||||||
|
"""
|
||||||
|
Add a single note annotation to the command.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
msg -- A human readable message.
|
||||||
|
ntype -- One of: 'info', 'warning', 'error'
|
||||||
|
"""
|
||||||
|
xml = ET.Element('{%s}note' % self.namespace)
|
||||||
|
xml.attrib['type'] = ntype
|
||||||
|
xml.text = msg
|
||||||
|
self.xml.append(xml)
|
114
tests/test_stanza_xep_0050.py
Normal file
114
tests/test_stanza_xep_0050.py
Normal file
|
@ -0,0 +1,114 @@
|
||||||
|
from sleekxmpp import Iq
|
||||||
|
from sleekxmpp.test import *
|
||||||
|
from sleekxmpp.plugins.xep_0050 import Command
|
||||||
|
|
||||||
|
|
||||||
|
class TestAdHocCommandStanzas(SleekTest):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
register_stanza_plugin(Iq, Command)
|
||||||
|
|
||||||
|
def testAction(self):
|
||||||
|
"""Test using the action attribute."""
|
||||||
|
iq = self.Iq()
|
||||||
|
iq['type'] = 'set'
|
||||||
|
iq['command']['node'] = 'foo'
|
||||||
|
|
||||||
|
iq['command']['action'] = 'execute'
|
||||||
|
self.failUnless(iq['command']['action'] == 'execute')
|
||||||
|
|
||||||
|
iq['command']['action'] = 'complete'
|
||||||
|
self.failUnless(iq['command']['action'] == 'complete')
|
||||||
|
|
||||||
|
iq['command']['action'] = 'cancel'
|
||||||
|
self.failUnless(iq['command']['action'] == 'cancel')
|
||||||
|
|
||||||
|
def testSetActions(self):
|
||||||
|
"""Test setting next actions in a command stanza."""
|
||||||
|
iq = self.Iq()
|
||||||
|
iq['type'] = 'result'
|
||||||
|
iq['command']['node'] = 'foo'
|
||||||
|
iq['command']['actions'] = ['prev', 'next']
|
||||||
|
|
||||||
|
self.check(iq, """
|
||||||
|
<iq id="0" type="result">
|
||||||
|
<command xmlns="http://jabber.org/protocol/commands"
|
||||||
|
node="foo">
|
||||||
|
<actions>
|
||||||
|
<prev />
|
||||||
|
<next />
|
||||||
|
</actions>
|
||||||
|
</command>
|
||||||
|
</iq>
|
||||||
|
""")
|
||||||
|
|
||||||
|
def testGetActions(self):
|
||||||
|
"""Test retrieving next actions from a command stanza."""
|
||||||
|
iq = self.Iq()
|
||||||
|
iq['command']['node'] = 'foo'
|
||||||
|
iq['command']['actions'] = ['prev', 'next']
|
||||||
|
|
||||||
|
results = iq['command']['actions']
|
||||||
|
expected = ['prev', 'next']
|
||||||
|
self.assertEqual(results, expected,
|
||||||
|
"Incorrect next actions: %s" % results)
|
||||||
|
|
||||||
|
def testDelActions(self):
|
||||||
|
"""Test removing next actions from a command stanza."""
|
||||||
|
iq = self.Iq()
|
||||||
|
iq['type'] = 'result'
|
||||||
|
iq['command']['node'] = 'foo'
|
||||||
|
iq['command']['actions'] = ['prev', 'next']
|
||||||
|
|
||||||
|
del iq['command']['actions']
|
||||||
|
|
||||||
|
self.check(iq, """
|
||||||
|
<iq id="0" type="result">
|
||||||
|
<command xmlns="http://jabber.org/protocol/commands"
|
||||||
|
node="foo" />
|
||||||
|
</iq>
|
||||||
|
""")
|
||||||
|
|
||||||
|
def testAddNote(self):
|
||||||
|
"""Test adding a command note."""
|
||||||
|
iq = self.Iq()
|
||||||
|
iq['type'] = 'result'
|
||||||
|
iq['command']['node'] = 'foo'
|
||||||
|
iq['command'].add_note('Danger!', ntype='warning')
|
||||||
|
|
||||||
|
self.check(iq, """
|
||||||
|
<iq id="0" type="result">
|
||||||
|
<command xmlns="http://jabber.org/protocol/commands"
|
||||||
|
node="foo">
|
||||||
|
<note type="warning">Danger!</note>
|
||||||
|
</command>
|
||||||
|
</iq>
|
||||||
|
""")
|
||||||
|
|
||||||
|
def testNotes(self):
|
||||||
|
"""Test using command notes."""
|
||||||
|
iq = self.Iq()
|
||||||
|
iq['type'] = 'result'
|
||||||
|
iq['command']['node'] = 'foo'
|
||||||
|
|
||||||
|
notes = [('info', 'Interesting...'),
|
||||||
|
('warning', 'Danger!'),
|
||||||
|
('error', "I can't let you do that")]
|
||||||
|
iq['command']['notes'] = notes
|
||||||
|
|
||||||
|
self.failUnless(iq['command']['notes'] == notes,
|
||||||
|
"Notes don't match: %s %s" % (notes, iq['command']['notes']))
|
||||||
|
|
||||||
|
self.check(iq, """
|
||||||
|
<iq id="0" type="result">
|
||||||
|
<command xmlns="http://jabber.org/protocol/commands"
|
||||||
|
node="foo">
|
||||||
|
<note type="info">Interesting...</note>
|
||||||
|
<note type="warning">Danger!</note>
|
||||||
|
<note type="error">I can't let you do that</note>
|
||||||
|
</command>
|
||||||
|
</iq>
|
||||||
|
""")
|
||||||
|
|
||||||
|
|
||||||
|
suite = unittest.TestLoader().loadTestsFromTestCase(TestAdHocCommandStanzas)
|
686
tests/test_stream_xep_0050.py
Normal file
686
tests/test_stream_xep_0050.py
Normal file
|
@ -0,0 +1,686 @@
|
||||||
|
import time
|
||||||
|
import threading
|
||||||
|
|
||||||
|
from sleekxmpp.test import *
|
||||||
|
|
||||||
|
|
||||||
|
class TestAdHocCommands(SleekTest):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.stream_start(mode='client',
|
||||||
|
plugins=['xep_0030', 'xep_0004', 'xep_0050'])
|
||||||
|
|
||||||
|
# Real session IDs don't make for nice tests, so use
|
||||||
|
# a dummy value.
|
||||||
|
self.xmpp['xep_0050'].new_session = lambda: '_sessionid_'
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
self.stream_close()
|
||||||
|
|
||||||
|
def testZeroStepCommand(self):
|
||||||
|
"""Test running a command with no steps."""
|
||||||
|
|
||||||
|
def handle_command(iq, session):
|
||||||
|
form = self.xmpp['xep_0004'].makeForm(ftype='result')
|
||||||
|
form.addField(var='foo', ftype='text-single',
|
||||||
|
label='Foo', value='bar')
|
||||||
|
|
||||||
|
session['payload'] = form
|
||||||
|
session['next'] = None
|
||||||
|
session['has_next'] = False
|
||||||
|
|
||||||
|
return session
|
||||||
|
|
||||||
|
self.xmpp['xep_0050'].add_command('tester@localhost', 'foo',
|
||||||
|
'Do Foo', handle_command)
|
||||||
|
|
||||||
|
self.recv("""
|
||||||
|
<iq id="11" type="set" to="tester@localhost" from="foo@bar">
|
||||||
|
<command xmlns="http://jabber.org/protocol/commands"
|
||||||
|
node="foo"
|
||||||
|
action="execute" />
|
||||||
|
</iq>
|
||||||
|
""")
|
||||||
|
|
||||||
|
self.send("""
|
||||||
|
<iq id="11" type="result" to="foo@bar">
|
||||||
|
<command xmlns="http://jabber.org/protocol/commands"
|
||||||
|
node="foo"
|
||||||
|
status="completed"
|
||||||
|
sessionid="_sessionid_">
|
||||||
|
<x xmlns="jabber:x:data" type="result">
|
||||||
|
<field var="foo" label="Foo" type="text-single">
|
||||||
|
<value>bar</value>
|
||||||
|
</field>
|
||||||
|
</x>
|
||||||
|
</command>
|
||||||
|
</iq>
|
||||||
|
""")
|
||||||
|
|
||||||
|
def testOneStepCommand(self):
|
||||||
|
"""Test running a single step command."""
|
||||||
|
results = []
|
||||||
|
|
||||||
|
def handle_command(iq, session):
|
||||||
|
|
||||||
|
def handle_form(form, session):
|
||||||
|
results.append(form['values']['foo'])
|
||||||
|
|
||||||
|
form = self.xmpp['xep_0004'].makeForm('form')
|
||||||
|
form.addField(var='foo', ftype='text-single', label='Foo')
|
||||||
|
|
||||||
|
session['payload'] = form
|
||||||
|
session['next'] = handle_form
|
||||||
|
session['has_next'] = False
|
||||||
|
|
||||||
|
return session
|
||||||
|
|
||||||
|
self.xmpp['xep_0050'].add_command('tester@localhost', 'foo',
|
||||||
|
'Do Foo', handle_command)
|
||||||
|
|
||||||
|
self.recv("""
|
||||||
|
<iq id="11" type="set" to="tester@localhost" from="foo@bar">
|
||||||
|
<command xmlns="http://jabber.org/protocol/commands"
|
||||||
|
node="foo"
|
||||||
|
action="execute" />
|
||||||
|
</iq>
|
||||||
|
""")
|
||||||
|
|
||||||
|
self.send("""
|
||||||
|
<iq id="11" type="result" to="foo@bar">
|
||||||
|
<command xmlns="http://jabber.org/protocol/commands"
|
||||||
|
node="foo"
|
||||||
|
status="executing"
|
||||||
|
sessionid="_sessionid_">
|
||||||
|
<actions>
|
||||||
|
<complete />
|
||||||
|
</actions>
|
||||||
|
<x xmlns="jabber:x:data" type="form">
|
||||||
|
<field var="foo" label="Foo" type="text-single" />
|
||||||
|
</x>
|
||||||
|
</command>
|
||||||
|
</iq>
|
||||||
|
""")
|
||||||
|
|
||||||
|
self.recv("""
|
||||||
|
<iq id="12" type="set" to="tester@localhost" from="foo@bar">
|
||||||
|
<command xmlns="http://jabber.org/protocol/commands"
|
||||||
|
node="foo"
|
||||||
|
action="complete"
|
||||||
|
sessionid="_sessionid_">
|
||||||
|
<x xmlns="jabber:x:data" type="submit">
|
||||||
|
<field var="foo" label="Foo" type="text-single">
|
||||||
|
<value>blah</value>
|
||||||
|
</field>
|
||||||
|
</x>
|
||||||
|
</command>
|
||||||
|
</iq>
|
||||||
|
""")
|
||||||
|
|
||||||
|
self.send("""
|
||||||
|
<iq id="12" type="result" to="foo@bar">
|
||||||
|
<command xmlns="http://jabber.org/protocol/commands"
|
||||||
|
node="foo"
|
||||||
|
status="completed"
|
||||||
|
sessionid="_sessionid_" />
|
||||||
|
</iq>
|
||||||
|
""")
|
||||||
|
|
||||||
|
self.assertEqual(results, ['blah'],
|
||||||
|
"Command handler was not executed: %s" % results)
|
||||||
|
|
||||||
|
def testTwoStepCommand(self):
|
||||||
|
"""Test using a two-stage command."""
|
||||||
|
results = []
|
||||||
|
|
||||||
|
def handle_command(iq, session):
|
||||||
|
|
||||||
|
def handle_step2(form, session):
|
||||||
|
results.append(form['values']['bar'])
|
||||||
|
|
||||||
|
def handle_step1(form, session):
|
||||||
|
results.append(form['values']['foo'])
|
||||||
|
|
||||||
|
form = self.xmpp['xep_0004'].makeForm('form')
|
||||||
|
form.addField(var='bar', ftype='text-single', label='Bar')
|
||||||
|
|
||||||
|
session['payload'] = form
|
||||||
|
session['next'] = handle_step2
|
||||||
|
session['has_next'] = False
|
||||||
|
|
||||||
|
return session
|
||||||
|
|
||||||
|
form = self.xmpp['xep_0004'].makeForm('form')
|
||||||
|
form.addField(var='foo', ftype='text-single', label='Foo')
|
||||||
|
|
||||||
|
session['payload'] = form
|
||||||
|
session['next'] = handle_step1
|
||||||
|
session['has_next'] = True
|
||||||
|
|
||||||
|
return session
|
||||||
|
|
||||||
|
self.xmpp['xep_0050'].add_command('tester@localhost', 'foo',
|
||||||
|
'Do Foo', handle_command)
|
||||||
|
|
||||||
|
self.recv("""
|
||||||
|
<iq id="11" type="set" to="tester@localhost" from="foo@bar">
|
||||||
|
<command xmlns="http://jabber.org/protocol/commands"
|
||||||
|
node="foo"
|
||||||
|
action="execute" />
|
||||||
|
</iq>
|
||||||
|
""")
|
||||||
|
|
||||||
|
self.send("""
|
||||||
|
<iq id="11" type="result" to="foo@bar">
|
||||||
|
<command xmlns="http://jabber.org/protocol/commands"
|
||||||
|
node="foo"
|
||||||
|
status="executing"
|
||||||
|
sessionid="_sessionid_">
|
||||||
|
<actions>
|
||||||
|
<next />
|
||||||
|
</actions>
|
||||||
|
<x xmlns="jabber:x:data" type="form">
|
||||||
|
<field var="foo" label="Foo" type="text-single" />
|
||||||
|
</x>
|
||||||
|
</command>
|
||||||
|
</iq>
|
||||||
|
""")
|
||||||
|
|
||||||
|
self.recv("""
|
||||||
|
<iq id="12" type="set" to="tester@localhost" from="foo@bar">
|
||||||
|
<command xmlns="http://jabber.org/protocol/commands"
|
||||||
|
node="foo"
|
||||||
|
action="next"
|
||||||
|
sessionid="_sessionid_">
|
||||||
|
<x xmlns="jabber:x:data" type="submit">
|
||||||
|
<field var="foo" label="Foo" type="text-single">
|
||||||
|
<value>blah</value>
|
||||||
|
</field>
|
||||||
|
</x>
|
||||||
|
</command>
|
||||||
|
</iq>
|
||||||
|
""")
|
||||||
|
|
||||||
|
self.send("""
|
||||||
|
<iq id="12" type="result" to="foo@bar">
|
||||||
|
<command xmlns="http://jabber.org/protocol/commands"
|
||||||
|
node="foo"
|
||||||
|
status="executing"
|
||||||
|
sessionid="_sessionid_">
|
||||||
|
<actions>
|
||||||
|
<complete />
|
||||||
|
</actions>
|
||||||
|
<x xmlns="jabber:x:data" type="form">
|
||||||
|
<field var="bar" label="Bar" type="text-single" />
|
||||||
|
</x>
|
||||||
|
</command>
|
||||||
|
</iq>
|
||||||
|
""")
|
||||||
|
|
||||||
|
self.recv("""
|
||||||
|
<iq id="13" type="set" to="tester@localhost" from="foo@bar">
|
||||||
|
<command xmlns="http://jabber.org/protocol/commands"
|
||||||
|
node="foo"
|
||||||
|
action="complete"
|
||||||
|
sessionid="_sessionid_">
|
||||||
|
<x xmlns="jabber:x:data" type="submit">
|
||||||
|
<field var="bar" label="Bar" type="text-single">
|
||||||
|
<value>meh</value>
|
||||||
|
</field>
|
||||||
|
</x>
|
||||||
|
</command>
|
||||||
|
</iq>
|
||||||
|
""")
|
||||||
|
self.send("""
|
||||||
|
<iq id="13" type="result" to="foo@bar">
|
||||||
|
<command xmlns="http://jabber.org/protocol/commands"
|
||||||
|
node="foo"
|
||||||
|
status="completed"
|
||||||
|
sessionid="_sessionid_" />
|
||||||
|
</iq>
|
||||||
|
""")
|
||||||
|
|
||||||
|
self.assertEqual(results, ['blah', 'meh'],
|
||||||
|
"Command handler was not executed: %s" % results)
|
||||||
|
|
||||||
|
def testCancelCommand(self):
|
||||||
|
"""Test canceling command."""
|
||||||
|
results = []
|
||||||
|
|
||||||
|
def handle_command(iq, session):
|
||||||
|
|
||||||
|
def handle_form(form, session):
|
||||||
|
results.append(form['values']['foo'])
|
||||||
|
|
||||||
|
def handle_cancel(iq, session):
|
||||||
|
results.append('canceled')
|
||||||
|
|
||||||
|
form = self.xmpp['xep_0004'].makeForm('form')
|
||||||
|
form.addField(var='foo', ftype='text-single', label='Foo')
|
||||||
|
|
||||||
|
session['payload'] = form
|
||||||
|
session['next'] = handle_form
|
||||||
|
session['cancel'] = handle_cancel
|
||||||
|
session['has_next'] = False
|
||||||
|
|
||||||
|
return session
|
||||||
|
|
||||||
|
self.xmpp['xep_0050'].add_command('tester@localhost', 'foo',
|
||||||
|
'Do Foo', handle_command)
|
||||||
|
|
||||||
|
self.recv("""
|
||||||
|
<iq id="11" type="set" to="tester@localhost" from="foo@bar">
|
||||||
|
<command xmlns="http://jabber.org/protocol/commands"
|
||||||
|
node="foo"
|
||||||
|
action="execute" />
|
||||||
|
</iq>
|
||||||
|
""")
|
||||||
|
|
||||||
|
self.send("""
|
||||||
|
<iq id="11" type="result" to="foo@bar">
|
||||||
|
<command xmlns="http://jabber.org/protocol/commands"
|
||||||
|
node="foo"
|
||||||
|
status="executing"
|
||||||
|
sessionid="_sessionid_">
|
||||||
|
<actions>
|
||||||
|
<complete />
|
||||||
|
</actions>
|
||||||
|
<x xmlns="jabber:x:data" type="form">
|
||||||
|
<field var="foo" label="Foo" type="text-single" />
|
||||||
|
</x>
|
||||||
|
</command>
|
||||||
|
</iq>
|
||||||
|
""")
|
||||||
|
|
||||||
|
self.recv("""
|
||||||
|
<iq id="12" type="set" to="tester@localhost" from="foo@bar">
|
||||||
|
<command xmlns="http://jabber.org/protocol/commands"
|
||||||
|
node="foo"
|
||||||
|
action="cancel"
|
||||||
|
sessionid="_sessionid_">
|
||||||
|
<x xmlns="jabber:x:data" type="submit">
|
||||||
|
<field var="foo" label="Foo" type="text-single">
|
||||||
|
<value>blah</value>
|
||||||
|
</field>
|
||||||
|
</x>
|
||||||
|
</command>
|
||||||
|
</iq>
|
||||||
|
""")
|
||||||
|
|
||||||
|
self.send("""
|
||||||
|
<iq id="12" type="result" to="foo@bar">
|
||||||
|
<command xmlns="http://jabber.org/protocol/commands"
|
||||||
|
node="foo"
|
||||||
|
status="canceled"
|
||||||
|
sessionid="_sessionid_" />
|
||||||
|
</iq>
|
||||||
|
""")
|
||||||
|
|
||||||
|
self.assertEqual(results, ['canceled'],
|
||||||
|
"Cancelation handler not executed: %s" % results)
|
||||||
|
|
||||||
|
def testCommandNote(self):
|
||||||
|
"""Test adding notes to commands."""
|
||||||
|
|
||||||
|
def handle_command(iq, session):
|
||||||
|
form = self.xmpp['xep_0004'].makeForm(ftype='result')
|
||||||
|
form.addField(var='foo', ftype='text-single',
|
||||||
|
label='Foo', value='bar')
|
||||||
|
|
||||||
|
session['payload'] = form
|
||||||
|
session['next'] = None
|
||||||
|
session['has_next'] = False
|
||||||
|
session['notes'] = [('info', 'testing notes')]
|
||||||
|
|
||||||
|
return session
|
||||||
|
|
||||||
|
self.xmpp['xep_0050'].add_command('tester@localhost', 'foo',
|
||||||
|
'Do Foo', handle_command)
|
||||||
|
|
||||||
|
self.recv("""
|
||||||
|
<iq id="11" type="set" to="tester@localhost" from="foo@bar">
|
||||||
|
<command xmlns="http://jabber.org/protocol/commands"
|
||||||
|
node="foo"
|
||||||
|
action="execute" />
|
||||||
|
</iq>
|
||||||
|
""")
|
||||||
|
|
||||||
|
self.send("""
|
||||||
|
<iq id="11" type="result" to="foo@bar">
|
||||||
|
<command xmlns="http://jabber.org/protocol/commands"
|
||||||
|
node="foo"
|
||||||
|
status="completed"
|
||||||
|
sessionid="_sessionid_">
|
||||||
|
<note type="info">testing notes</note>
|
||||||
|
<x xmlns="jabber:x:data" type="result">
|
||||||
|
<field var="foo" label="Foo" type="text-single">
|
||||||
|
<value>bar</value>
|
||||||
|
</field>
|
||||||
|
</x>
|
||||||
|
</command>
|
||||||
|
</iq>
|
||||||
|
""")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def testMultiPayloads(self):
|
||||||
|
"""Test using commands with multiple payloads."""
|
||||||
|
results = []
|
||||||
|
|
||||||
|
def handle_command(iq, session):
|
||||||
|
|
||||||
|
def handle_form(forms, session):
|
||||||
|
for form in forms:
|
||||||
|
results.append(form['values']['FORM_TYPE'])
|
||||||
|
|
||||||
|
form1 = self.xmpp['xep_0004'].makeForm('form')
|
||||||
|
form1.addField(var='FORM_TYPE', ftype='hidden', value='form_1')
|
||||||
|
form1.addField(var='foo', ftype='text-single', label='Foo')
|
||||||
|
|
||||||
|
form2 = self.xmpp['xep_0004'].makeForm('form')
|
||||||
|
form2.addField(var='FORM_TYPE', ftype='hidden', value='form_2')
|
||||||
|
form2.addField(var='foo', ftype='text-single', label='Foo')
|
||||||
|
|
||||||
|
session['payload'] = [form1, form2]
|
||||||
|
session['next'] = handle_form
|
||||||
|
session['has_next'] = False
|
||||||
|
|
||||||
|
return session
|
||||||
|
|
||||||
|
self.xmpp['xep_0050'].add_command('tester@localhost', 'foo',
|
||||||
|
'Do Foo', handle_command)
|
||||||
|
|
||||||
|
self.recv("""
|
||||||
|
<iq id="11" type="set" to="tester@localhost" from="foo@bar">
|
||||||
|
<command xmlns="http://jabber.org/protocol/commands"
|
||||||
|
node="foo"
|
||||||
|
action="execute" />
|
||||||
|
</iq>
|
||||||
|
""")
|
||||||
|
|
||||||
|
self.send("""
|
||||||
|
<iq id="11" type="result" to="foo@bar">
|
||||||
|
<command xmlns="http://jabber.org/protocol/commands"
|
||||||
|
node="foo"
|
||||||
|
status="executing"
|
||||||
|
sessionid="_sessionid_">
|
||||||
|
<actions>
|
||||||
|
<complete />
|
||||||
|
</actions>
|
||||||
|
<x xmlns="jabber:x:data" type="form">
|
||||||
|
<field var="FORM_TYPE" type="hidden">
|
||||||
|
<value>form_1</value>
|
||||||
|
</field>
|
||||||
|
<field var="foo" label="Foo" type="text-single" />
|
||||||
|
</x>
|
||||||
|
<x xmlns="jabber:x:data" type="form">
|
||||||
|
<field var="FORM_TYPE" type="hidden">
|
||||||
|
<value>form_2</value>
|
||||||
|
</field>
|
||||||
|
<field var="foo" label="Foo" type="text-single" />
|
||||||
|
</x>
|
||||||
|
</command>
|
||||||
|
</iq>
|
||||||
|
""")
|
||||||
|
|
||||||
|
self.recv("""
|
||||||
|
<iq id="12" type="set" to="tester@localhost" from="foo@bar">
|
||||||
|
<command xmlns="http://jabber.org/protocol/commands"
|
||||||
|
node="foo"
|
||||||
|
action="complete"
|
||||||
|
sessionid="_sessionid_">
|
||||||
|
<x xmlns="jabber:x:data" type="submit">
|
||||||
|
<field var="FORM_TYPE" type="hidden">
|
||||||
|
<value>form_1</value>
|
||||||
|
</field>
|
||||||
|
<field var="foo" type="text-single">
|
||||||
|
<value>bar</value>
|
||||||
|
</field>
|
||||||
|
</x>
|
||||||
|
<x xmlns="jabber:x:data" type="submit">
|
||||||
|
<field var="FORM_TYPE" type="hidden">
|
||||||
|
<value>form_2</value>
|
||||||
|
</field>
|
||||||
|
<field var="foo" type="text-single">
|
||||||
|
<value>bar</value>
|
||||||
|
</field>
|
||||||
|
</x>
|
||||||
|
</command>
|
||||||
|
</iq>
|
||||||
|
""")
|
||||||
|
|
||||||
|
self.send("""
|
||||||
|
<iq id="12" type="result" to="foo@bar">
|
||||||
|
<command xmlns="http://jabber.org/protocol/commands"
|
||||||
|
node="foo"
|
||||||
|
status="completed"
|
||||||
|
sessionid="_sessionid_" />
|
||||||
|
</iq>
|
||||||
|
""")
|
||||||
|
|
||||||
|
self.assertEqual(results, [['form_1'], ['form_2']],
|
||||||
|
"Command handler was not executed: %s" % results)
|
||||||
|
|
||||||
|
def testClientAPI(self):
|
||||||
|
"""Test using client-side API for commands."""
|
||||||
|
results = []
|
||||||
|
|
||||||
|
def handle_complete(iq, session):
|
||||||
|
for item in session['custom_data']:
|
||||||
|
results.append(item)
|
||||||
|
|
||||||
|
def handle_step2(iq, session):
|
||||||
|
form = self.xmpp['xep_0004'].makeForm(ftype='submit')
|
||||||
|
form.addField(var='bar', value='123')
|
||||||
|
|
||||||
|
session['custom_data'].append('baz')
|
||||||
|
session['payload'] = form
|
||||||
|
session['next'] = handle_complete
|
||||||
|
self.xmpp['xep_0050'].complete_command(session)
|
||||||
|
|
||||||
|
def handle_step1(iq, session):
|
||||||
|
form = self.xmpp['xep_0004'].makeForm(ftype='submit')
|
||||||
|
form.addField(var='foo', value='42')
|
||||||
|
|
||||||
|
session['custom_data'].append('bar')
|
||||||
|
session['payload'] = form
|
||||||
|
session['next'] = handle_step2
|
||||||
|
self.xmpp['xep_0050'].continue_command(session)
|
||||||
|
|
||||||
|
session = {'custom_data': ['foo'],
|
||||||
|
'next': handle_step1}
|
||||||
|
|
||||||
|
self.xmpp['xep_0050'].start_command(
|
||||||
|
'foo@example.com',
|
||||||
|
'test_client',
|
||||||
|
session)
|
||||||
|
|
||||||
|
self.send("""
|
||||||
|
<iq id="1" to="foo@example.com" type="set">
|
||||||
|
<command xmlns="http://jabber.org/protocol/commands"
|
||||||
|
node="test_client"
|
||||||
|
action="execute" />
|
||||||
|
</iq>
|
||||||
|
""")
|
||||||
|
|
||||||
|
self.recv("""
|
||||||
|
<iq id="1" to="foo@example.com" type="result">
|
||||||
|
<command xmlns="http://jabber.org/protocol/commands"
|
||||||
|
node="test_client"
|
||||||
|
sessionid="_sessionid_"
|
||||||
|
status="executing">
|
||||||
|
<x xmlns="jabber:x:data" type="form">
|
||||||
|
<field var="foo" type="text-single" />
|
||||||
|
</x>
|
||||||
|
</command>
|
||||||
|
</iq>
|
||||||
|
""")
|
||||||
|
|
||||||
|
self.send("""
|
||||||
|
<iq id="2" to="foo@example.com" type="set">
|
||||||
|
<command xmlns="http://jabber.org/protocol/commands"
|
||||||
|
node="test_client"
|
||||||
|
sessionid="_sessionid_"
|
||||||
|
action="next">
|
||||||
|
<x xmlns="jabber:x:data" type="submit">
|
||||||
|
<field var="foo">
|
||||||
|
<value>42</value>
|
||||||
|
</field>
|
||||||
|
</x>
|
||||||
|
</command>
|
||||||
|
</iq>
|
||||||
|
""")
|
||||||
|
|
||||||
|
self.recv("""
|
||||||
|
<iq id="2" to="foo@example.com" type="result">
|
||||||
|
<command xmlns="http://jabber.org/protocol/commands"
|
||||||
|
node="test_client"
|
||||||
|
sessionid="_sessionid_"
|
||||||
|
status="executing">
|
||||||
|
<x xmlns="jabber:x:data" type="form">
|
||||||
|
<field var="bar" type="text-single" />
|
||||||
|
</x>
|
||||||
|
</command>
|
||||||
|
</iq>
|
||||||
|
""")
|
||||||
|
|
||||||
|
self.send("""
|
||||||
|
<iq id="3" to="foo@example.com" type="set">
|
||||||
|
<command xmlns="http://jabber.org/protocol/commands"
|
||||||
|
node="test_client"
|
||||||
|
sessionid="_sessionid_"
|
||||||
|
action="complete">
|
||||||
|
<x xmlns="jabber:x:data" type="submit">
|
||||||
|
<field var="bar">
|
||||||
|
<value>123</value>
|
||||||
|
</field>
|
||||||
|
</x>
|
||||||
|
</command>
|
||||||
|
</iq>
|
||||||
|
""")
|
||||||
|
|
||||||
|
self.recv("""
|
||||||
|
<iq id="3" to="foo@example.com" type="result">
|
||||||
|
<command xmlns="http://jabber.org/protocol/commands"
|
||||||
|
node="test_client"
|
||||||
|
sessionid="_sessionid_"
|
||||||
|
status="completed" />
|
||||||
|
</iq>
|
||||||
|
""")
|
||||||
|
|
||||||
|
# Give the event queue time to process
|
||||||
|
time.sleep(0.3)
|
||||||
|
|
||||||
|
self.failUnless(results == ['foo', 'bar', 'baz'],
|
||||||
|
'Incomplete command workflow: %s' % results)
|
||||||
|
|
||||||
|
def testClientAPICancel(self):
|
||||||
|
"""Test using client-side cancel API for commands."""
|
||||||
|
results = []
|
||||||
|
|
||||||
|
def handle_canceled(iq, session):
|
||||||
|
for item in session['custom_data']:
|
||||||
|
results.append(item)
|
||||||
|
|
||||||
|
def handle_step1(iq, session):
|
||||||
|
session['custom_data'].append('bar')
|
||||||
|
session['next'] = handle_canceled
|
||||||
|
self.xmpp['xep_0050'].cancel_command(session)
|
||||||
|
|
||||||
|
session = {'custom_data': ['foo'],
|
||||||
|
'next': handle_step1}
|
||||||
|
|
||||||
|
self.xmpp['xep_0050'].start_command(
|
||||||
|
'foo@example.com',
|
||||||
|
'test_client',
|
||||||
|
session)
|
||||||
|
|
||||||
|
self.send("""
|
||||||
|
<iq id="1" to="foo@example.com" type="set">
|
||||||
|
<command xmlns="http://jabber.org/protocol/commands"
|
||||||
|
node="test_client"
|
||||||
|
action="execute" />
|
||||||
|
</iq>
|
||||||
|
""")
|
||||||
|
|
||||||
|
self.recv("""
|
||||||
|
<iq id="1" to="foo@example.com" type="result">
|
||||||
|
<command xmlns="http://jabber.org/protocol/commands"
|
||||||
|
node="test_client"
|
||||||
|
sessionid="_sessionid_"
|
||||||
|
status="executing">
|
||||||
|
<x xmlns="jabber:x:data" type="form">
|
||||||
|
<field var="foo" type="text-single" />
|
||||||
|
</x>
|
||||||
|
</command>
|
||||||
|
</iq>
|
||||||
|
""")
|
||||||
|
|
||||||
|
self.send("""
|
||||||
|
<iq id="2" to="foo@example.com" type="set">
|
||||||
|
<command xmlns="http://jabber.org/protocol/commands"
|
||||||
|
node="test_client"
|
||||||
|
sessionid="_sessionid_"
|
||||||
|
action="cancel" />
|
||||||
|
</iq>
|
||||||
|
""")
|
||||||
|
|
||||||
|
self.recv("""
|
||||||
|
<iq id="2" to="foo@example.com" type="result">
|
||||||
|
<command xmlns="http://jabber.org/protocol/commands"
|
||||||
|
node="test_client"
|
||||||
|
sessionid="_sessionid_"
|
||||||
|
status="canceled" />
|
||||||
|
</iq>
|
||||||
|
""")
|
||||||
|
|
||||||
|
# Give the event queue time to process
|
||||||
|
time.sleep(0.3)
|
||||||
|
|
||||||
|
self.failUnless(results == ['foo', 'bar'],
|
||||||
|
'Incomplete command workflow: %s' % results)
|
||||||
|
|
||||||
|
def testClientAPIError(self):
|
||||||
|
"""Test using client-side error API for commands."""
|
||||||
|
results = []
|
||||||
|
|
||||||
|
def handle_error(iq, session):
|
||||||
|
for item in session['custom_data']:
|
||||||
|
results.append(item)
|
||||||
|
|
||||||
|
session = {'custom_data': ['foo'],
|
||||||
|
'error': handle_error}
|
||||||
|
|
||||||
|
self.xmpp['xep_0050'].start_command(
|
||||||
|
'foo@example.com',
|
||||||
|
'test_client',
|
||||||
|
session)
|
||||||
|
|
||||||
|
self.send("""
|
||||||
|
<iq id="1" to="foo@example.com" type="set">
|
||||||
|
<command xmlns="http://jabber.org/protocol/commands"
|
||||||
|
node="test_client"
|
||||||
|
action="execute" />
|
||||||
|
</iq>
|
||||||
|
""")
|
||||||
|
|
||||||
|
self.recv("""
|
||||||
|
<iq id="1" to="foo@example.com" type="error">
|
||||||
|
<command xmlns="http://jabber.org/protocol/commands"
|
||||||
|
node="test_client"
|
||||||
|
action="execute" />
|
||||||
|
<error type='cancel'>
|
||||||
|
<item-not-found xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/>
|
||||||
|
</error>
|
||||||
|
</iq>
|
||||||
|
""")
|
||||||
|
|
||||||
|
# Give the event queue time to process
|
||||||
|
time.sleep(0.3)
|
||||||
|
|
||||||
|
self.failUnless(results == ['foo'],
|
||||||
|
'Incomplete command workflow: %s' % results)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
suite = unittest.TestLoader().loadTestsFromTestCase(TestAdHocCommands)
|
Loading…
Reference in a new issue