diff options
-rw-r--r-- | slixmpp/jid.py | 22 | ||||
-rw-r--r-- | slixmpp/plugins/xep_0004/stanza/form.py | 7 | ||||
-rw-r--r-- | slixmpp/plugins/xep_0050/adhoc.py | 42 | ||||
-rw-r--r-- | tests/test_stanza_xep_0060.py | 2 | ||||
-rw-r--r-- | tests/test_stream_xep_0050.py | 448 | ||||
-rw-r--r-- | tests/test_stream_xep_0060.py | 10 |
6 files changed, 495 insertions, 36 deletions
diff --git a/slixmpp/jid.py b/slixmpp/jid.py index c84b4aa0..ee5ef987 100644 --- a/slixmpp/jid.py +++ b/slixmpp/jid.py @@ -5,12 +5,16 @@ # Part of Slixmpp: The Slick XMPP Library # :copyright: (c) 2011 Nathanael C. Fritz # :license: MIT, see LICENSE for more details +from __future__ import annotations + import re import socket -from copy import deepcopy from functools import lru_cache -from typing import Optional +from typing import ( + Optional, + Union, +) from slixmpp.stringprep import nodeprep, resourceprep, idna, StringprepError @@ -42,7 +46,7 @@ JID_UNESCAPE_TRANSFORMATIONS = {'\\20': ' ', # TODO: Find the best cache size for a standard usage. @lru_cache(maxsize=1024) -def _parse_jid(data): +def _parse_jid(data: str): """ Parse string data into the node, domain, and resource components of a JID, if possible. @@ -305,7 +309,7 @@ class JID: __slots__ = ('_node', '_domain', '_resource', '_bare', '_full') - def __init__(self, jid: Optional[str] = None): + def __init__(self, jid: Optional[Union[str, 'JID']] = None): if not jid: self._node = '' self._domain = '' @@ -347,23 +351,23 @@ class JID: else self._bare) @property - def node(self): + def node(self) -> str: return self._node @property - def domain(self): + def domain(self) -> str: return self._domain @property - def resource(self): + def resource(self) -> str: return self._resource @property - def bare(self): + def bare(self) -> str: return self._bare @property - def full(self): + def full(self) -> str: return self._full @node.setter diff --git a/slixmpp/plugins/xep_0004/stanza/form.py b/slixmpp/plugins/xep_0004/stanza/form.py index 9af96a4c..c04193f0 100644 --- a/slixmpp/plugins/xep_0004/stanza/form.py +++ b/slixmpp/plugins/xep_0004/stanza/form.py @@ -19,6 +19,7 @@ class Form(ElementBase): namespace = 'jabber:x:data' name = 'x' plugin_attrib = 'form' + plugin_multi_attrib = 'forms' interfaces = OrderedSet(('instructions', 'reported', 'title', 'type', 'items', 'values')) sub_interfaces = {'title'} form_types = {'cancel', 'form', 'result', 'submit'} @@ -47,7 +48,8 @@ class Form(ElementBase): fields = self.get_fields() for var in fields: field = fields[var] - del field['type'] + if field['type'] != 'hidden': + del field['type'] del field['label'] del field['desc'] del field['required'] @@ -73,7 +75,8 @@ class Form(ElementBase): for option in options: field.add_option(**option) else: - del field['type'] + if field['type'] != 'hidden': + del field['type'] self.append(field) return field diff --git a/slixmpp/plugins/xep_0050/adhoc.py b/slixmpp/plugins/xep_0050/adhoc.py index 5f3bc81c..072ec5aa 100644 --- a/slixmpp/plugins/xep_0050/adhoc.py +++ b/slixmpp/plugins/xep_0050/adhoc.py @@ -3,6 +3,7 @@ # Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout # This file is part of Slixmpp. # See the file LICENSE for copying permission. +import asyncio import logging import time @@ -164,25 +165,25 @@ class XEP_0050(BasePlugin): self.xmpp.event('command', 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'] sessionid = iq['command']['sessionid'] session = self.sessions.get(sessionid) if session is None: - return self._handle_command_start(iq) + return await self._handle_command_start(iq) if action in ('next', 'execute'): - return self._handle_command_next(iq) + return await self._handle_command_next(iq) if action == 'prev': - return self._handle_command_prev(iq) + return await self._handle_command_prev(iq) if action == 'complete': - return self._handle_command_complete(iq) + return await self._handle_command_complete(iq) if action == 'cancel': - return self._handle_command_cancel(iq) + return await self._handle_command_cancel(iq) return None - def _handle_command_start(self, iq): + async def _handle_command_start(self, iq): """ Process an initial request to execute a command. @@ -222,11 +223,11 @@ class XEP_0050(BasePlugin): 'prev': None, 'cancel': None} - session = handler(iq, initial_session) + session = await _await_if_needed(handler, iq, initial_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 for a command with multiple steps. @@ -246,13 +247,13 @@ class XEP_0050(BasePlugin): if len(results) == 1: results = results[0] - session = handler(results, session) + session = await _await_if_needed(handler, results, session) self._process_command_response(iq, session) else: 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 for a command with multiple steps. @@ -272,7 +273,7 @@ class XEP_0050(BasePlugin): if len(results) == 1: results = results[0] - session = handler(results, session) + session = await _await_if_needed(handler, results, session) self._process_command_response(iq, session) else: @@ -334,7 +335,7 @@ class XEP_0050(BasePlugin): iq.send() - def _handle_command_cancel(self, iq): + async def _handle_command_cancel(self, iq): """ Process a request to cancel a command's execution. @@ -348,7 +349,7 @@ class XEP_0050(BasePlugin): if session: handler = session['cancel'] if handler: - handler(iq, session) + await _await_if_needed(handler, iq, session) del self.sessions[sessionid] iq = iq.reply() iq['command']['node'] = node @@ -360,7 +361,7 @@ class XEP_0050(BasePlugin): 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 and terminate the workflow. @@ -385,7 +386,7 @@ class XEP_0050(BasePlugin): results = results[0] if handler: - handler(results, session) + await _await_if_needed(handler, results, session) del self.sessions[sessionid] @@ -616,3 +617,12 @@ class XEP_0050(BasePlugin): if iq['command']['status'] == 'completed': 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) diff --git a/tests/test_stanza_xep_0060.py b/tests/test_stanza_xep_0060.py index d05bc3d0..671f9cd0 100644 --- a/tests/test_stanza_xep_0060.py +++ b/tests/test_stanza_xep_0060.py @@ -314,7 +314,7 @@ class TestPubsubStanzas(SlixTest): <create node="testnode2" /> <configure> <x xmlns="jabber:x:data" type="submit"> - <field var="FORM_TYPE"> + <field var="FORM_TYPE" type="hidden"> <value>http://jabber.org/protocol/pubsub#node_config</value> </field> <field var="pubsub#node_type"> diff --git a/tests/test_stream_xep_0050.py b/tests/test_stream_xep_0050.py index 37cd233d..519c860e 100644 --- a/tests/test_stream_xep_0050.py +++ b/tests/test_stream_xep_0050.py @@ -47,7 +47,6 @@ class TestAdHocCommands(SlixTest): session['has_next'] = False return session - self.xmpp['xep_0050'].add_command('tester@localhost', 'foo', 'Do Foo', handle_command) @@ -418,8 +417,6 @@ class TestAdHocCommands(SlixTest): </iq> """) - - def testMultiPayloads(self): """Test using commands with multiple payloads.""" results = [] @@ -519,6 +516,451 @@ class TestAdHocCommands(SlixTest): self.assertEqual(results, [['form_1'], ['form_2']], "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): """Test using client-side API for commands.""" results = [] diff --git a/tests/test_stream_xep_0060.py b/tests/test_stream_xep_0060.py index da543f96..a5fd0bdc 100644 --- a/tests/test_stream_xep_0060.py +++ b/tests/test_stream_xep_0060.py @@ -72,7 +72,7 @@ class TestStreamPubsub(SlixTest): <field var="pubsub#access_model"> <value>whitelist</value> </field> - <field var="FORM_TYPE"> + <field var="FORM_TYPE" type="hidden"> <value>http://jabber.org/protocol/pubsub#node_config</value> </field> </x> @@ -210,7 +210,7 @@ class TestStreamPubsub(SlixTest): <subscribe node="somenode" jid="tester@localhost" /> <options> <x xmlns="jabber:x:data" type="submit"> - <field var="FORM_TYPE"> + <field var="FORM_TYPE" type="hidden"> <value>http://jabber.org/protocol/pubsub#subscribe_options</value> </field> <field var="pubsub#digest"> @@ -358,7 +358,7 @@ class TestStreamPubsub(SlixTest): <pubsub xmlns="http://jabber.org/protocol/pubsub#owner"> <configure node="somenode"> <x xmlns="jabber:x:data" type="submit"> - <field var="FORM_TYPE"> + <field var="FORM_TYPE" type="hidden"> <value>http://jabber.org/protocol/pubsub#node_config</value> </field> <field var="pubsub#title"> @@ -441,7 +441,7 @@ class TestStreamPubsub(SlixTest): </publish> <publish-options> <x xmlns="jabber:x:data" type="submit"> - <field var="FORM_TYPE"> + <field var="FORM_TYPE" type="hidden"> <value>http://jabber.org/protocol/pubsub#publish-options</value> </field> <field var="pubsub#access_model"> @@ -622,7 +622,7 @@ class TestStreamPubsub(SlixTest): <pubsub xmlns="http://jabber.org/protocol/pubsub"> <options node="somenode" jid="tester@localhost"> <x xmlns="jabber:x:data" type="submit"> - <field var="FORM_TYPE"> + <field var="FORM_TYPE" type="hidden"> <value>http://jabber.org/protocol/pubsub#subscribe_options</value> </field> <field var="pubsub#digest"> |