XEP-0050 Make handle_command_xxx async

This commit is contained in:
Nicoco K 2021-03-09 19:24:43 +01:00 committed by mathieui
parent e97f5ccb9c
commit eac5ad50a8
2 changed files with 471 additions and 19 deletions

View file

@ -3,6 +3,7 @@
# Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout # Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout
# This file is part of Slixmpp. # This file is part of Slixmpp.
# See the file LICENSE for copying permission. # See the file LICENSE for copying permission.
import asyncio
import logging import logging
import time import time
@ -164,25 +165,25 @@ class XEP_0050(BasePlugin):
self.xmpp.event('command', iq) self.xmpp.event('command', iq)
self.xmpp.event('command_%s' % iq['command']['action'], iq) self.xmpp.event('command_%s' % iq['command']['action'], iq)
def _handle_command_all(self, iq: Iq) -> None: async def _handle_command_all(self, iq: Iq) -> None:
action = iq['command']['action'] action = iq['command']['action']
sessionid = iq['command']['sessionid'] sessionid = iq['command']['sessionid']
session = self.sessions.get(sessionid) session = self.sessions.get(sessionid)
if session is None: if session is None:
return self._handle_command_start(iq) return await self._handle_command_start(iq)
if action in ('next', 'execute'): if action in ('next', 'execute'):
return self._handle_command_next(iq) return await self._handle_command_next(iq)
if action == 'prev': if action == 'prev':
return self._handle_command_prev(iq) return await self._handle_command_prev(iq)
if action == 'complete': if action == 'complete':
return self._handle_command_complete(iq) return await self._handle_command_complete(iq)
if action == 'cancel': if action == 'cancel':
return self._handle_command_cancel(iq) return await self._handle_command_cancel(iq)
return None return None
def _handle_command_start(self, iq): async def _handle_command_start(self, iq):
""" """
Process an initial request to execute a command. Process an initial request to execute a command.
@ -222,11 +223,11 @@ class XEP_0050(BasePlugin):
'prev': None, 'prev': None,
'cancel': None} 'cancel': None}
session = handler(iq, initial_session) session = await _await_if_needed(handler, iq, initial_session)
self._process_command_response(iq, session) self._process_command_response(iq, session)
def _handle_command_next(self, iq): async def _handle_command_next(self, iq):
""" """
Process a request for the next step in the workflow Process a request for the next step in the workflow
for a command with multiple steps. for a command with multiple steps.
@ -246,13 +247,13 @@ class XEP_0050(BasePlugin):
if len(results) == 1: if len(results) == 1:
results = results[0] results = results[0]
session = handler(results, session) session = await _await_if_needed(handler, results, session)
self._process_command_response(iq, session) self._process_command_response(iq, session)
else: else:
raise XMPPError('item-not-found') raise XMPPError('item-not-found')
def _handle_command_prev(self, iq): async def _handle_command_prev(self, iq):
""" """
Process a request for the prev step in the workflow Process a request for the prev step in the workflow
for a command with multiple steps. for a command with multiple steps.
@ -272,7 +273,7 @@ class XEP_0050(BasePlugin):
if len(results) == 1: if len(results) == 1:
results = results[0] results = results[0]
session = handler(results, session) session = await _await_if_needed(handler, results, session)
self._process_command_response(iq, session) self._process_command_response(iq, session)
else: else:
@ -334,7 +335,7 @@ class XEP_0050(BasePlugin):
iq.send() iq.send()
def _handle_command_cancel(self, iq): async def _handle_command_cancel(self, iq):
""" """
Process a request to cancel a command's execution. Process a request to cancel a command's execution.
@ -348,7 +349,7 @@ class XEP_0050(BasePlugin):
if session: if session:
handler = session['cancel'] handler = session['cancel']
if handler: if handler:
handler(iq, session) await _await_if_needed(handler, iq, session)
del self.sessions[sessionid] del self.sessions[sessionid]
iq = iq.reply() iq = iq.reply()
iq['command']['node'] = node iq['command']['node'] = node
@ -360,7 +361,7 @@ class XEP_0050(BasePlugin):
raise XMPPError('item-not-found') raise XMPPError('item-not-found')
def _handle_command_complete(self, iq): async def _handle_command_complete(self, iq):
""" """
Process a request to finish the execution of command Process a request to finish the execution of command
and terminate the workflow. and terminate the workflow.
@ -385,7 +386,7 @@ class XEP_0050(BasePlugin):
results = results[0] results = results[0]
if handler: if handler:
handler(results, session) await _await_if_needed(handler, results, session)
del self.sessions[sessionid] del self.sessions[sessionid]
@ -616,3 +617,12 @@ class XEP_0050(BasePlugin):
if iq['command']['status'] == 'completed': if iq['command']['status'] == 'completed':
self.terminate_command(session) self.terminate_command(session)
async def _await_if_needed(handler, *args):
if asyncio.iscoroutinefunction(handler):
log.debug(f"%s is async", handler)
return await handler(*args)
else:
log.debug(f"%s is sync", handler)
return handler(*args)

View file

@ -47,7 +47,6 @@ class TestAdHocCommands(SlixTest):
session['has_next'] = False session['has_next'] = False
return session return session
self.xmpp['xep_0050'].add_command('tester@localhost', 'foo', self.xmpp['xep_0050'].add_command('tester@localhost', 'foo',
'Do Foo', handle_command) 'Do Foo', handle_command)
@ -418,8 +417,6 @@ class TestAdHocCommands(SlixTest):
</iq> </iq>
""") """)
def testMultiPayloads(self): def testMultiPayloads(self):
"""Test using commands with multiple payloads.""" """Test using commands with multiple payloads."""
results = [] results = []
@ -519,6 +516,451 @@ class TestAdHocCommands(SlixTest):
self.assertEqual(results, [['form_1'], ['form_2']], self.assertEqual(results, [['form_1'], ['form_2']],
"Command handler was not executed: %s" % results) "Command handler was not executed: %s" % results)
def testZeroStepCommandAsync(self):
"""Test running a command with no steps."""
async def handle_command(iq, session):
form = self.xmpp['xep_0004'].make_form(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 testOneStepCommandAsync(self):
"""Test running a single step command."""
results = []
async def handle_command(iq, session):
async def handle_form(form, session):
results.append(form.get_values()['foo'])
session['payload'] = None
form = self.xmpp['xep_0004'].make_form('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 testTwoStepCommandAsync(self):
"""Test using a two-stage command."""
results = []
async def handle_command(iq, session):
async def handle_step2(form, session):
results.append(form.get_values()['bar'])
session['payload'] = None
async def handle_step1(form, session):
results.append(form.get_values()['foo'])
form = self.xmpp['xep_0004'].make_form('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'].make_form('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 testCancelCommandAsync(self):
"""Test canceling command."""
results = []
async def handle_command(iq, session):
async def handle_form(form, session):
results.append(form['values']['foo'])
async def handle_cancel(iq, session):
results.append('canceled')
form = self.xmpp['xep_0004'].make_form('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 testCommandNoteAsync(self):
"""Test adding notes to commands."""
async def handle_command(iq, session):
form = self.xmpp['xep_0004'].make_form(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 testMultiPayloadsAsync(self):
"""Test using commands with multiple payloads."""
results = []
async def handle_command(iq, session):
async def handle_form(forms, session):
for form in forms:
results.append(form.get_values()['FORM_TYPE'])
session['payload'] = None
form1 = self.xmpp['xep_0004'].make_form('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'].make_form('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): def testClientAPI(self):
"""Test using client-side API for commands.""" """Test using client-side API for commands."""
results = [] results = []