Update XEP-0050 to use new IQ exceptions.

IqError is now caught and forwarded to the command error handler referenced
in the session.

Errors are now caught and processed by the session's error handler
whether or not the results Iq stanza includes the <command> substanza.

Added the option for blocking command calls. The blocking option is set
during start_command with block=True. Subsequent command flow methods use
session['block'] to determine their blocking behaviour.

If you use blocking commands, then you will need to wrap your command calls
in a try/except block for IqTimeout exceptions.
This commit is contained in:
Lance Stout 2011-08-13 00:00:34 -07:00
parent dcaddb8042
commit c26b716164
3 changed files with 86 additions and 23 deletions

View file

@ -136,6 +136,7 @@ class CommandUserBot(sleekxmpp.ClientXMPP):
# The session will automatically be cleared if no error # The session will automatically be cleared if no error
# handler is provided. # handler is provided.
self['xep_0050'].terminate_command(session) self['xep_0050'].terminate_command(session)
self.disconnect()
if __name__ == '__main__': if __name__ == '__main__':
@ -176,7 +177,7 @@ if __name__ == '__main__':
if opts.other is None: if opts.other is None:
opts.other = raw_input("JID Providing Commands: ") opts.other = raw_input("JID Providing Commands: ")
if opts.greeting is None: if opts.greeting is None:
opts.other = raw_input("Greeting: ") opts.greeting = raw_input("Greeting: ")
# Setup the CommandBot and register plugins. Note that while plugins may # Setup the CommandBot and register plugins. Note that while plugins may
# have interdependencies, the order in which you register them does # have interdependencies, the order in which you register them does

View file

@ -10,6 +10,7 @@ import logging
import time import time
from sleekxmpp import Iq from sleekxmpp import Iq
from sleekxmpp.exceptions import IqError
from sleekxmpp.xmlstream.handler import Callback from sleekxmpp.xmlstream.handler import Callback
from sleekxmpp.xmlstream.matcher import StanzaPath from sleekxmpp.xmlstream.matcher import StanzaPath
from sleekxmpp.xmlstream import register_stanza_plugin, JID from sleekxmpp.xmlstream import register_stanza_plugin, JID
@ -91,16 +92,6 @@ class xep_0050(base_plugin):
StanzaPath('iq@type=set/command'), StanzaPath('iq@type=set/command'),
self._handle_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) register_stanza_plugin(Iq, stanza.Command)
self.xmpp.add_event_handler('command_execute', self.xmpp.add_event_handler('command_execute',
@ -408,7 +399,7 @@ class xep_0050(base_plugin):
**kwargs) **kwargs)
def send_command(self, jid, node, ifrom=None, action='execute', def send_command(self, jid, node, ifrom=None, action='execute',
payload=None, sessionid=None, **kwargs): payload=None, sessionid=None, flow=False, **kwargs):
""" """
Create and send a command stanza, without using the provided Create and send a command stanza, without using the provided
workflow management APIs. workflow management APIs.
@ -422,6 +413,10 @@ class xep_0050(base_plugin):
payload -- Either a list of payload items, or a single payload -- Either a list of payload items, or a single
payload item such as a data form. payload item such as a data form.
sessionid -- The current session's ID value. sessionid -- The current session's ID value.
flow -- If True, process the Iq result using the
command workflow methods contained in the
session instead of returning the response
stanza itself. Defaults to False.
block -- Specify if the send call will block until a block -- Specify if the send call will block until a
response is received, or a timeout occurs. response is received, or a timeout occurs.
Defaults to True. Defaults to True.
@ -431,7 +426,7 @@ class xep_0050(base_plugin):
sleekxmpp.xmlstream.RESPONSE_TIMEOUT sleekxmpp.xmlstream.RESPONSE_TIMEOUT
callback -- Optional reference to a stream handler callback -- Optional reference to a stream handler
function. Will be executed when a reply function. Will be executed when a reply
stanza is received. stanza is received if flow=False.
""" """
iq = self.xmpp.Iq() iq = self.xmpp.Iq()
iq['type'] = 'set' iq['type'] = 'set'
@ -447,13 +442,24 @@ class xep_0050(base_plugin):
payload = [payload] payload = [payload]
for item in payload: for item in payload:
iq['command'].append(item) iq['command'].append(item)
return iq.send(**kwargs) if not flow:
return iq.send(**kwargs)
else:
if kwargs.get('block', True):
try:
result = iq.send(**kwargs)
except IqError as err:
result = err.iq
self._handle_command_result(result)
else:
iq.send(block=False, callback=self._handle_command_result)
def start_command(self, jid, node, session, ifrom=None): def start_command(self, jid, node, session, ifrom=None, block=False):
""" """
Initiate executing a command provided by a remote agent. Initiate executing a command provided by a remote agent.
The workflow provided is always non-blocking. The default workflow provided is non-blocking, but a blocking
version may be used with block=True.
The provided session dictionary should contain: The provided session dictionary should contain:
next -- A handler for processing the command result. next -- A handler for processing the command result.
@ -465,11 +471,14 @@ class xep_0050(base_plugin):
node -- The node for the desired command. node -- The node for the desired command.
session -- A dictionary of relevant session data. session -- A dictionary of relevant session data.
ifrom -- Optionally specify the sender's JID. ifrom -- Optionally specify the sender's JID.
block -- If True, block execution until a result
is received. Defaults to False.
""" """
session['jid'] = jid session['jid'] = jid
session['node'] = node session['node'] = node
session['timestamp'] = time.time() session['timestamp'] = time.time()
session['payload'] = None session['payload'] = None
session['block'] = block
iq = self.xmpp.Iq() iq = self.xmpp.Iq()
iq['type'] = 'set' iq['type'] = 'set'
iq['to'] = jid iq['to'] = jid
@ -481,7 +490,14 @@ class xep_0050(base_plugin):
sessionid = 'client:pending_' + iq['id'] sessionid = 'client:pending_' + iq['id']
session['id'] = sessionid session['id'] = sessionid
self.sessions[sessionid] = session self.sessions[sessionid] = session
iq.send(block=False) if session['block']:
try:
result = iq.send(block=True)
except IqError as err:
result = err.iq
self._handle_command_result(result)
else:
iq.send(block=False, callback=self._handle_command_result)
def continue_command(self, session): def continue_command(self, session):
""" """
@ -499,7 +515,9 @@ class xep_0050(base_plugin):
ifrom=session.get('from', None), ifrom=session.get('from', None),
action='next', action='next',
payload=session.get('payload', None), payload=session.get('payload', None),
sessionid=session['id']) sessionid=session['id'],
flow=True,
block=session['block'])
def cancel_command(self, session): def cancel_command(self, session):
""" """
@ -517,7 +535,9 @@ class xep_0050(base_plugin):
ifrom=session.get('from', None), ifrom=session.get('from', None),
action='cancel', action='cancel',
payload=session.get('payload', None), payload=session.get('payload', None),
sessionid=session['id']) sessionid=session['id'],
flow=True,
block=session['block'])
def complete_command(self, session): def complete_command(self, session):
""" """
@ -535,7 +555,9 @@ class xep_0050(base_plugin):
ifrom=session.get('from', None), ifrom=session.get('from', None),
action='complete', action='complete',
payload=session.get('payload', None), payload=session.get('payload', None),
sessionid=session['id']) sessionid=session['id'],
flow=True,
block=session['block'])
def terminate_command(self, session): def terminate_command(self, session):
""" """

View file

@ -504,7 +504,7 @@ class TestAdHocCommands(SleekTest):
""") """)
self.recv(""" self.recv("""
<iq id="1" to="foo@example.com" type="result"> <iq id="1" from="foo@example.com" type="result">
<command xmlns="http://jabber.org/protocol/commands" <command xmlns="http://jabber.org/protocol/commands"
node="test_client" node="test_client"
sessionid="_sessionid_" sessionid="_sessionid_"
@ -532,7 +532,7 @@ class TestAdHocCommands(SleekTest):
""") """)
self.recv(""" self.recv("""
<iq id="2" to="foo@example.com" type="result"> <iq id="2" from="foo@example.com" type="result">
<command xmlns="http://jabber.org/protocol/commands" <command xmlns="http://jabber.org/protocol/commands"
node="test_client" node="test_client"
sessionid="_sessionid_" sessionid="_sessionid_"
@ -560,7 +560,7 @@ class TestAdHocCommands(SleekTest):
""") """)
self.recv(""" self.recv("""
<iq id="3" to="foo@example.com" type="result"> <iq id="3" from="foo@example.com" type="result">
<command xmlns="http://jabber.org/protocol/commands" <command xmlns="http://jabber.org/protocol/commands"
node="test_client" node="test_client"
sessionid="_sessionid_" sessionid="_sessionid_"
@ -681,6 +681,46 @@ class TestAdHocCommands(SleekTest):
self.failUnless(results == ['foo'], self.failUnless(results == ['foo'],
'Incomplete command workflow: %s' % results) 'Incomplete command workflow: %s' % results)
def testClientAPIErrorStrippedResponse(self):
"""Test errors that don't include the command substanza."""
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">
<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) suite = unittest.TestLoader().loadTestsFromTestCase(TestAdHocCommands)