summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--slixmpp/jid.py22
-rw-r--r--slixmpp/plugins/xep_0004/stanza/form.py7
-rw-r--r--slixmpp/plugins/xep_0050/adhoc.py42
-rw-r--r--tests/test_stanza_xep_0060.py2
-rw-r--r--tests/test_stream_xep_0050.py448
-rw-r--r--tests/test_stream_xep_0060.py10
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">