summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--docs/api/plugins/xep_0055.rst18
-rw-r--r--slixmpp/plugins/__init__.py1
-rw-r--r--slixmpp/plugins/xep_0055/__init__.py6
-rw-r--r--slixmpp/plugins/xep_0055/search.py89
-rw-r--r--slixmpp/plugins/xep_0055/stanza.py10
-rw-r--r--tests/test_stanza_xep_0055.py59
-rw-r--r--tests/test_stream_xep_0055.py170
7 files changed, 353 insertions, 0 deletions
diff --git a/docs/api/plugins/xep_0055.rst b/docs/api/plugins/xep_0055.rst
new file mode 100644
index 00000000..75abe991
--- /dev/null
+++ b/docs/api/plugins/xep_0055.rst
@@ -0,0 +1,18 @@
+
+XEP-0055: Jabber search
+=======================
+
+.. module:: slixmpp.plugins.xep_0055
+
+.. autoclass:: XEP_0055
+ :members:
+ :exclude-members: session_bind, plugin_init, plugin_end
+
+
+Stanza elements
+---------------
+
+.. automodule:: slixmpp.plugins.xep_0055.stanza
+ :members:
+ :undoc-members:
+
diff --git a/slixmpp/plugins/__init__.py b/slixmpp/plugins/__init__.py
index cc98255e..ac7482ee 100644
--- a/slixmpp/plugins/__init__.py
+++ b/slixmpp/plugins/__init__.py
@@ -23,6 +23,7 @@ __all__ = [
'xep_0049', # Private XML Storage
'xep_0050', # Ad-hoc Commands
'xep_0054', # vcard-temp
+ 'xep_0055', # Jabber Search
'xep_0059', # Result Set Management
'xep_0060', # Pubsub (Client)
'xep_0065', # SOCKS5 Bytestreams
diff --git a/slixmpp/plugins/xep_0055/__init__.py b/slixmpp/plugins/xep_0055/__init__.py
new file mode 100644
index 00000000..981cf960
--- /dev/null
+++ b/slixmpp/plugins/xep_0055/__init__.py
@@ -0,0 +1,6 @@
+from slixmpp.plugins.base import register_plugin
+
+from .search import XEP_0055
+
+
+register_plugin(XEP_0055)
diff --git a/slixmpp/plugins/xep_0055/search.py b/slixmpp/plugins/xep_0055/search.py
new file mode 100644
index 00000000..a45b52a7
--- /dev/null
+++ b/slixmpp/plugins/xep_0055/search.py
@@ -0,0 +1,89 @@
+import logging
+
+from slixmpp import CoroutineCallback, StanzaPath, Iq, register_stanza_plugin
+from slixmpp.plugins import BasePlugin
+from slixmpp.xmlstream import StanzaBase
+
+from . import stanza
+
+
+class XEP_0055(BasePlugin):
+ """
+ XEP-0055: Jabber Search
+
+ The config options are only useful for a "server-side" search feature,
+ and if the ``provide_search`` option is set to True.
+
+ API
+ ===
+
+ ``search_get_form``: customize the search form content (ie fields)
+
+ ``search_query``: return search results
+ """
+ name = "xep_0055"
+ description = "XEP-0055: Jabber search"
+ dependencies = {"xep_0004", "xep_0030"}
+ stanza = stanza
+ default_config = {
+ "form_fields": {"first", "last"},
+ "form_instructions": "",
+ "form_title": "",
+ "provide_search": True
+ }
+
+ def plugin_init(self):
+ register_stanza_plugin(Iq, stanza.Search)
+ register_stanza_plugin(stanza.Search, self.xmpp["xep_0004"].stanza.Form)
+
+ if self.provide_search:
+ self.xmpp["xep_0030"].add_feature(stanza.Search.namespace)
+ self.xmpp.register_handler(
+ CoroutineCallback(
+ "search",
+ StanzaPath("/iq/search"),
+ self._handle_search,
+ )
+ )
+ self.api.register(self._get_form, "search_get_form")
+ self.api.register(self._get_results, "search_query")
+
+ async def _handle_search(self, iq: StanzaBase):
+ if iq["search"]["form"].get_values():
+ reply = await self.api["search_query"](None, None, iq.get_from(), iq)
+ reply["search"]["form"]["type"] = "result"
+ else:
+ reply = await self.api["search_get_form"](None, None, iq.get_from(), iq)
+ reply["search"]["form"].add_field(
+ "FORM_TYPE", value=stanza.Search.namespace, ftype="hidden"
+ )
+ reply.send()
+
+ async def _get_form(self, jid, node, ifrom, iq):
+ reply = iq.reply()
+ form = reply["search"]["form"]
+ form["title"] = self.form_title
+ form["instructions"] = self.form_instructions
+ for field in self.form_fields:
+ form.add_field(field)
+ return reply
+
+ async def _get_results(self, jid, node, ifrom, iq):
+ reply = iq.reply()
+ form = reply["search"]["form"]
+ form["type"] = "result"
+
+ for field in self.form_fields:
+ form.add_reported(field)
+ return reply
+
+ def make_search_iq(self, **kwargs):
+ iq = self.xmpp.make_iq(itype="set", **kwargs)
+ iq["search"]["form"].set_type("submit")
+ iq["search"]["form"].add_field(
+ "FORM_TYPE", value=stanza.Search.namespace, ftype="hidden"
+ )
+ return iq
+
+
+log = logging.getLogger(__name__)
diff --git a/slixmpp/plugins/xep_0055/stanza.py b/slixmpp/plugins/xep_0055/stanza.py
new file mode 100644
index 00000000..18bccf7e
--- /dev/null
+++ b/slixmpp/plugins/xep_0055/stanza.py
@@ -0,0 +1,10 @@
+from typing import Set, ClassVar
+
+from slixmpp.xmlstream import ElementBase
+
+
+class Search(ElementBase):
+ namespace = "jabber:iq:search"
+ name = "query"
+ plugin_attrib = "search"
+ interfaces: ClassVar[Set[str]] = set()
diff --git a/tests/test_stanza_xep_0055.py b/tests/test_stanza_xep_0055.py
new file mode 100644
index 00000000..9ff45efa
--- /dev/null
+++ b/tests/test_stanza_xep_0055.py
@@ -0,0 +1,59 @@
+import unittest
+
+from slixmpp import register_stanza_plugin, Iq
+from slixmpp.test import SlixTest
+
+from slixmpp.plugins.xep_0055 import stanza
+
+
+class TestJabberSearch(SlixTest):
+ def setUp(self):
+ register_stanza_plugin(Iq, stanza.Search)
+ self.stream_start(plugins={"xep_0055"})
+
+ def testRequestSearchFields(self):
+ iq = self.Iq()
+ iq.set_from("juliet@capulet.com/balcony")
+ iq.set_to("characters.shakespeare.lit")
+ iq.set_type("get")
+ iq.enable("search")
+ iq["id"] = "0"
+ self.check(
+ iq,
+ """
+ <iq type='get'
+ from='juliet@capulet.com/balcony'
+ to='characters.shakespeare.lit'>
+ <query xmlns='jabber:iq:search'/>
+ </iq>
+ """,
+ )
+
+ def testSendSearch(self):
+ iq = self.xmpp["xep_0055"].make_search_iq(
+ ifrom="juliet@capulet.com/balcony", ito="characters.shakespeare.lit"
+ )
+ iq["search"]["form"].add_field(var="x-gender", value="male")
+ self.check(
+ iq,
+ """
+ <iq type='set'
+ from='juliet@capulet.com/balcony'
+ to='characters.shakespeare.lit'>
+ <query xmlns='jabber:iq:search'>
+ <x xmlns='jabber:x:data' type='submit'>
+ <field type='hidden' var='FORM_TYPE'>
+ <value>jabber:iq:search</value>
+ </field>
+ <field var='x-gender'>
+ <value>male</value>
+ </field>
+ </x>
+ </query>
+ </iq>
+ """,
+ use_values=False,
+ )
+
+
+suite = unittest.TestLoader().loadTestsFromTestCase(TestJabberSearch)
diff --git a/tests/test_stream_xep_0055.py b/tests/test_stream_xep_0055.py
new file mode 100644
index 00000000..fa028d8b
--- /dev/null
+++ b/tests/test_stream_xep_0055.py
@@ -0,0 +1,170 @@
+import unittest
+from slixmpp.test import SlixTest
+
+
+class TestJabberSearch(SlixTest):
+ def setUp(self):
+ self.stream_start(
+ mode="component",
+ plugin_config={
+ "xep_0055": {
+ "form_fields": {"first", "last"},
+ "form_instructions": "INSTRUCTIONS",
+ "form_title": "User Directory Search",
+ }
+ },
+ jid="characters.shakespeare.lit",
+ plugins={"xep_0055"}
+ )
+ self.xmpp["xep_0055"].api.register(get_results, "search_query")
+ self.xmpp["xep_0055"].api.register(get_results, "search_query")
+
+ def tearDown(self):
+ self.stream_close()
+
+ def testRequestingSearchFields(self):
+ self.recv(
+ """
+ <iq type='get'
+ from='juliet@capulet.com/balcony'
+ to='characters.shakespeare.lit'
+ id='search3'
+ xml:lang='en'>
+ <query xmlns='jabber:iq:search'/>
+ </iq>
+ """
+ )
+ self.send(
+ """
+ <iq type='result'
+ from='characters.shakespeare.lit'
+ to='juliet@capulet.com/balcony'
+ id='search3'
+ xml:lang='en'>
+ <query xmlns='jabber:iq:search'>
+ <x xmlns='jabber:x:data' type='form'>
+ <title>User Directory Search</title>
+ <instructions>INSTRUCTIONS</instructions>
+ <field type='hidden'
+ var='FORM_TYPE'>
+ <value>jabber:iq:search</value>
+ </field>
+ <field var='first'/>
+ <field var='last'/>
+ </x>
+ </query>
+ </iq>
+ """,
+ use_values=False,
+ )
+
+ def testSearchResult(self):
+ self.recv(
+ """
+ <iq type='get'
+ from='juliet@capulet.com/balcony'
+ to='characters.shakespeare.lit'
+ id='search2'
+ xml:lang='en'>
+ <query xmlns='jabber:iq:search'>
+ <x xmlns='jabber:x:data' type='submit'>
+ <field type='hidden' var='FORM_TYPE'>
+ <value>jabber:iq:search</value>
+ </field>
+ <field var='last'>
+ <value>Montague</value>
+ </field>
+ </x>
+ </query>
+ </iq>
+ """
+ )
+ self.send(
+ """
+ <iq type='result'
+ from='characters.shakespeare.lit'
+ to='juliet@capulet.com/balcony'
+ id='search2'
+ xml:lang='en'>
+ <query xmlns='jabber:iq:search'>
+ <x xmlns='jabber:x:data' type='result'>
+ <field type='hidden' var='FORM_TYPE'>
+ <value>jabber:iq:search</value>
+ </field>
+ <reported>
+ <field var='first' label='Given Name' />
+ <field var='last' label='Family Name' />
+ </reported>
+ <item>
+ <field var='first'><value>Benvolio</value></field>
+ <field var='last'><value>Montague</value></field>
+ </item>
+ </x>
+ </query>
+ </iq>
+ """,
+ use_values=False, # TypeError: element indices must be integers without that
+ )
+
+ def testSearchNoResult(self):
+ self.xmpp["xep_0055"].api.register(get_results, "search_query")
+ self.recv(
+ """
+ <iq type='get'
+ from='juliet@capulet.com/balcony'
+ to='characters.shakespeare.lit'
+ id='search2'
+ xml:lang='en'>
+ <query xmlns='jabber:iq:search'>
+ <x xmlns='jabber:x:data' type='submit'>
+ <field type='hidden' var='FORM_TYPE'>
+ <value>jabber:iq:search</value>
+ </field>
+ <field var='last'>
+ <value>Capulet</value>
+ </field>
+ </x>
+ </query>
+ </iq>
+ """
+ )
+ self.send(
+ """
+ <iq type='result'
+ from='characters.shakespeare.lit'
+ to='juliet@capulet.com/balcony'
+ id='search2'
+ xml:lang='en'>
+ <query xmlns='jabber:iq:search'>
+ <x xmlns='jabber:x:data' type='result'>
+ <field type='hidden' var='FORM_TYPE'>
+ <value>jabber:iq:search</value>
+ </field>
+ <reported>
+ <field var='first' label='Given Name' />
+ <field var='last' label='Family Name' />
+ </reported>
+ </x>
+ </query>
+ </iq>
+ """,
+ use_values=False, # TypeError: element indices must be integers without that
+ )
+
+async def get_results(jid, node, ifrom, iq):
+ reply = iq.reply()
+ form = reply["search"]["form"]
+ form["type"] = "result"
+
+ form.add_reported("first", label="Given Name")
+ form.add_reported("last", label="Family Name")
+
+ d = iq["search"]["form"].get_values()
+
+ if d["last"] == "Montague":
+ form.add_item({"first": "Benvolio", "last": "Montague"})
+
+ return reply
+
+
+suite = unittest.TestLoader().loadTestsFromTestCase(TestJabberSearch)