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, + """ + + + + """, + ) + + 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, + """ + + + + + jabber:iq:search + + + male + + + + + """, + 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( + """ + + + + """ + ) + self.send( + """ + + + + User Directory Search + INSTRUCTIONS + + jabber:iq:search + + + + + + + """, + use_values=False, + ) + + def testSearchResult(self): + self.recv( + """ + + + + + jabber:iq:search + + + Montague + + + + + """ + ) + self.send( + """ + + + + + jabber:iq:search + + + + + + + Benvolio + Montague + + + + + """, + 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( + """ + + + + + jabber:iq:search + + + Capulet + + + + + """ + ) + self.send( + """ + + + + + jabber:iq:search + + + + + + + + + """, + 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)