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:
Lance Stout 2011-03-23 23:00:41 -04:00
parent 4916a12b6f
commit a3d111be12
9 changed files with 2064 additions and 0 deletions

199
examples/adhoc_provider.py Executable file
View 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
View 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.")

View file

@ -49,6 +49,7 @@ packages = [ 'sleekxmpp',
'sleekxmpp/plugins/xep_0009/stanza',
'sleekxmpp/plugins/xep_0030',
'sleekxmpp/plugins/xep_0030/stanza',
'sleekxmpp/plugins/xep_0050',
'sleekxmpp/plugins/xep_0059',
'sleekxmpp/plugins/xep_0085',
'sleekxmpp/plugins/xep_0092',

View file

@ -0,0 +1,10 @@
"""
SleekXMPP: The Sleek XMPP Library
Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout
This file is part of SleekXMPP.
See the file LICENSE for copying permission.
"""
from sleekxmpp.plugins.xep_0050.stanza import Command
from sleekxmpp.plugins.xep_0050.adhoc import xep_0050

View 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)

View 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)

View 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)

View 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)