comparison src/plugins/plugin_xep_0055.py @ 1498:e3330ce65285

plugin XEP-0055: add "simple" and "advanced" modes to Jabber search: - simple search queries the service offered by the user's server, asks for a single input and compile the results of the queries done on each search field - advanced allows to select the search service and to perform a "normal" search (the user can fill in all the fields that are implemented by the service)
author souliane <souliane@mailoo.org>
date Fri, 21 Aug 2015 19:02:11 +0200
parents 3265a2639182
children 8405d622bde0
comparison
equal deleted inserted replaced
1497:7a9cef71ae43 1498:e3330ce65285
18 # along with this program. If not, see <http://www.gnu.org/licenses/>. 18 # along with this program. If not, see <http://www.gnu.org/licenses/>.
19 19
20 from sat.core.i18n import _, D_ 20 from sat.core.i18n import _, D_
21 from sat.core.log import getLogger 21 from sat.core.log import getLogger
22 log = getLogger(__name__) 22 log = getLogger(__name__)
23
23 from twisted.words.protocols.jabber.xmlstream import IQ 24 from twisted.words.protocols.jabber.xmlstream import IQ
24 from twisted.words.protocols.jabber import jid 25 from twisted.words.protocols.jabber import jid
25 from twisted.internet import defer 26 from twisted.internet import defer
26 from wokkel import data_form 27 from wokkel import data_form
28 from sat.core.constants import Const as C
27 from sat.core.exceptions import DataError 29 from sat.core.exceptions import DataError
28 from sat.memory.memory import Sessions 30 from sat.memory.memory import Sessions
29 from sat.tools import xml_tools 31 from sat.tools import xml_tools
32
33 from wokkel import disco, iwokkel
34 try:
35 from twisted.words.protocols.xmlstream import XMPPHandler
36 except ImportError:
37 from wokkel.subprotocols import XMPPHandler
38 from zope.interface import implements
39
30 40
31 NS_SEARCH = 'jabber:iq:search' 41 NS_SEARCH = 'jabber:iq:search'
32 42
33 PLUGIN_INFO = { 43 PLUGIN_INFO = {
34 "name": "Jabber Search", 44 "name": "Jabber Search",
40 "main": "XEP_0055", 50 "main": "XEP_0055",
41 "handler": "no", 51 "handler": "no",
42 "description": _("""Implementation of Jabber Search""") 52 "description": _("""Implementation of Jabber Search""")
43 } 53 }
44 54
55 # config file parameters
56 CONFIG_SECTION = "plugin search"
57 CONFIG_SERVICE_LIST = "service_list"
58
59 DEFAULT_SERVICE_LIST = ["salut.libervia.org"]
60
61 FIELD_SINGLE = "field_single" # single text field for the simple search
62 FIELD_CURRENT_SERVICE = "current_service_jid" # read-only text field for the advanced search
45 63
46 class XEP_0055(object): 64 class XEP_0055(object):
47 65
48 def __init__(self, host): 66 def __init__(self, host):
49 log.info(_("Jabber search plugin initialization")) 67 log.info(_("Jabber search plugin initialization"))
50 self.host = host 68 self.host = host
51 host.bridge.addMethod("getSearchUI", ".plugin", in_sign='ss', out_sign='s', 69
52 method=self._getSearchUI, 70 # default search services (config file + hard-coded lists)
71 self.services = [jid.JID(entry) for entry in host.memory.getConfig(CONFIG_SECTION, CONFIG_SERVICE_LIST, DEFAULT_SERVICE_LIST)]
72
73 host.bridge.addMethod("searchGetFieldsUI", ".plugin", in_sign='ss', out_sign='s',
74 method=self._getFieldsUI,
53 async=True) 75 async=True)
54 host.bridge.addMethod("searchRequest", ".plugin", in_sign='sa{ss}s', out_sign='s', 76 host.bridge.addMethod("searchRequest", ".plugin", in_sign='sa{ss}s', out_sign='s',
55 method=self._searchRequest, 77 method=self._searchRequest,
56 async=True) 78 async=True)
57 self._sessions = Sessions() 79
58 self.__menu_cb_id = host.registerCallback(self._menuCb, with_data=True) 80 self.__search_menu_id = host.registerCallback(self._getMainUI, with_data=True)
59 self.__search_request_id = host.registerCallback(self._xmluiSearchRequest, with_data=True) 81 host.importMenu((D_("Communication"), D_("Search directory")), self._getMainUI, security_limit=1, help_string=D_("Search use directory"))
60 host.importMenu((D_("Communication"), D_("Search directory")), self._searchMenu, security_limit=1, help_string=D_("Search use directory")) 82
61 83
62 def _menuCb(self, data, profile): 84 ## Main search UI (menu item callback) ##
63 entity = jid.JID(data[xml_tools.SAT_FORM_PREFIX+'jid']) 85
64 d = self.getSearchUI(entity, profile) 86
65 def gotXMLUI(xmlui): 87 def _getMainUI(self, raw_data, profile):
66 session_id, session_data = self._sessions.newSession(profile=profile) 88 """Get the XMLUI for selecting a service and searching the directory.
67 session_data['jid'] = entity 89
68 xmlui.session_id = session_id # we need to keep track of the session 90 @param raw_data (dict): data received from the frontend
69 xmlui.submit_id = self.__search_request_id 91 @param profile (unicode): %(doc_profile)s
70 return {'xmlui': xmlui.toXml()} 92 @return: a deferred XMLUI string representation
71 d.addCallback(gotXMLUI) 93 """
94 # check if the user's server offers some search services
95 d = self.host.findFeaturesSet([NS_SEARCH], profile_key=profile)
96 return d.addCallback(lambda services: self.getMainUI(list(services), raw_data, profile))
97
98 def getMainUI(self, services, raw_data, profile):
99 """Get the XMLUI for selecting a service and searching the directory.
100
101 @param services (list[jid.JID]): search services offered by the user server
102 @param raw_data (dict): data received from the frontend
103 @param profile (unicode): %(doc_profile)s
104 @return: a deferred XMLUI string representation
105 """
106 # extend services offered by user's server with the default services
107 services.extend([service for service in self.services if service not in services])
108 data = xml_tools.XMLUIResult2DataFormResult(raw_data)
109 main_ui = xml_tools.XMLUI(C.XMLUI_WINDOW, container="tabs", title=_("Search users"), submit_id=self.__search_menu_id)
110
111 d = self._addSimpleSearchUI(services, main_ui, data, profile)
112 d.addCallback(lambda dummy: self._addAdvancedSearchUI(services, main_ui, data, profile))
113 return d.addCallback(lambda dummy: {'xmlui': main_ui.toXml()})
114
115 def _addSimpleSearchUI(self, services, main_ui, data, profile):
116 """Add to the main UI a tab for the simple search.
117
118 Display a single input field and search on the main service (it actually does one search per search field and then compile the results).
119
120 @param services (list[jid.JID]): search services offered by the user server
121 @param main_ui (XMLUI): the main XMLUI instance
122 @param data (dict): form data without SAT_FORM_PREFIX
123 @param profile (unicode): %(doc_profile)s
124
125 @return: a dummy Deferred
126 """
127 service_jid = services[0] # TODO: search on all the given services, not only the first one
128
129 form = data_form.Form('form', formNamespace=NS_SEARCH)
130 form.addField(data_form.Field('text-single', FIELD_SINGLE, label=_('Search for'), value=data.get(FIELD_SINGLE, '')))
131
132 sub_cont = main_ui.main_container.addTab("simple_search", label=_("Simple search"), container=xml_tools.VerticalContainer)
133 main_ui.changeContainer(sub_cont.append(xml_tools.PairsContainer(main_ui)))
134 xml_tools.dataForm2Widgets(main_ui, form)
135
136 # FIXME: add colspan attribute to divider? (we are in a PairsContainer)
137 main_ui.addDivider('blank')
138 main_ui.addDivider('blank') # here we added a blank line before the button
139 main_ui.addDivider('blank')
140 main_ui.addButton(self.__search_menu_id, _("Search"), (FIELD_SINGLE,))
141 main_ui.addDivider('blank')
142 main_ui.addDivider('blank') # a blank line again after the button
143
144 simple_data = {key: value for key, value in data.iteritems() if key in (FIELD_SINGLE,)}
145 if simple_data:
146 log.debug("Simple search with %s on %s" % (simple_data, service_jid))
147 sub_cont.parent.setSelected(True)
148 main_ui.changeContainer(sub_cont.append(xml_tools.VerticalContainer(main_ui)))
149 main_ui.addDivider('dash')
150 d = self.searchRequest(service_jid, simple_data, profile)
151 d.addCallbacks(lambda elt: self._displaySearchResult(main_ui, elt),
152 lambda failure: main_ui.addText(failure.getErrorMessage()))
153 return d
154
155 return defer.succeed(None)
156
157 def _addAdvancedSearchUI(self, services, main_ui, data, profile):
158 """Add to the main UI a tab for the advanced search.
159
160 Display a service selector and allow to search on all the fields that are implemented by the selected service.
161
162 @param services (list[jid.JID]): search services offered by the user server
163 @param main_ui (XMLUI): the main XMLUI instance
164 @param data (dict): form data without SAT_FORM_PREFIX
165 @param profile (unicode): %(doc_profile)s
166
167 @return: a dummy Deferred
168 """
169 sub_cont = main_ui.main_container.addTab("advanced_search", label=_("Advanced search"), container=xml_tools.VerticalContainer)
170 service_selection_fields = ['service_jid', 'service_jid_extra']
171
172 if 'service_jid_extra' in data:
173 # refresh button has been pushed, select the tab
174 sub_cont.parent.setSelected(True)
175 # get the selected service
176 service_jid_s = data.get('service_jid_extra', '')
177 if not service_jid_s:
178 service_jid_s = data.get('service_jid', unicode(services[0]))
179 log.debug("Refreshing search fields for %s" % service_jid_s)
180 else:
181 service_jid_s = data.get(FIELD_CURRENT_SERVICE, unicode(services[0]))
182 services_s = [unicode(service) for service in services]
183 if service_jid_s not in services_s:
184 services_s.append(service_jid_s)
185
186 main_ui.changeContainer(sub_cont.append(xml_tools.PairsContainer(main_ui)))
187 main_ui.addLabel(_("Search on"))
188 main_ui.addList('service_jid', options=services_s, selected=service_jid_s)
189 main_ui.addLabel(_("Other service"))
190 main_ui.addString(name='service_jid_extra')
191
192 # FIXME: add colspan attribute to divider? (we are in a PairsContainer)
193 main_ui.addDivider('blank')
194 main_ui.addDivider('blank') # here we added a blank line before the button
195 main_ui.addDivider('blank')
196 main_ui.addButton(self.__search_menu_id, _("Refresh fields"), service_selection_fields)
197 main_ui.addDivider('blank')
198 main_ui.addDivider('blank') # a blank line again after the button
199 main_ui.addLabel(_("Displaying the search form for"))
200 main_ui.addString(name=FIELD_CURRENT_SERVICE, value=service_jid_s, read_only=True)
201 main_ui.addDivider('dash')
202 main_ui.addDivider('dash')
203
204 main_ui.changeContainer(sub_cont.append(xml_tools.VerticalContainer(main_ui)))
205 service_jid = jid.JID(service_jid_s)
206 d = self.getFieldsUI(service_jid, profile)
207 d.addCallbacks(self._addAdvancedForm, lambda failure: main_ui.addText(failure.getErrorMessage()),
208 [service_jid, main_ui, sub_cont, data, profile])
72 return d 209 return d
73 210
74 211 def _addAdvancedForm(self, form_elt, service_jid, main_ui, sub_cont, data, profile):
75 def _searchMenu(self, menu_data, profile): 212 """Add the search form and the search results (if there is some to display).
76 """ First XMLUI activated by menu: ask for target jid 213
77 @param profile: %(doc_profile)s 214 @param form_elt (domish.Element): form element listing the fields
78 215 @param service_jid (jid.JID): current search service
79 """ 216 @param main_ui (XMLUI): the main XMLUI instance
80 form_ui = xml_tools.XMLUI("form", title=_("Search directory"), submit_id=self.__menu_cb_id) 217 @param sub_cont (Container): the container of the current tab
81 form_ui.addText(_("Please enter the search jid"), 'instructions') 218 @param data (dict): form data without SAT_FORM_PREFIX
82 form_ui.changeContainer("pairs") 219 @param profile (unicode): %(doc_profile)s
83 form_ui.addLabel("jid") 220
84 # form_ui.addString("jid", value="users.jabberfr.org") # TODO: replace users.jabberfr.org by any XEP-0055 compatible service discovered on current server 221 @return: a dummy Deferred
85 form_ui.addString("jid", value="salut.libervia.org") # TODO: replace salut.libervia.org by any XEP-0055 compatible service discovered on current server 222 """
86 return {'xmlui': form_ui.toXml()} 223 field_list = data_form.Form.fromElement(form_elt).fieldList
87 224 adv_fields = [field.var for field in field_list if field.var]
88 def _getSearchUI(self, to_jid_s, profile_key): 225 adv_data = {key: value for key, value in data.iteritems() if key in adv_fields}
89 d = self.getSearchUI(jid.JID(to_jid_s), profile_key) 226
90 d.addCallback(lambda xmlui: xmlui.toXml()) 227 xml_tools.dataForm2Widgets(main_ui, data_form.Form.fromElement(form_elt))
228
229 # refill the submitted values
230 # FIXME: wokkel's data_form.Form.fromElement doesn't parse the values, so we do it directly in XMLUI for now
231 for widget in main_ui.current_container.elem.childNodes:
232 name = widget.getAttribute("name")
233 if adv_data.get(name):
234 widget.setAttribute("value", adv_data[name])
235
236 # FIXME: add colspan attribute to divider? (we are in a PairsContainer)
237 main_ui.addDivider('blank')
238 main_ui.addDivider('blank') # here we added a blank line before the button
239 main_ui.addDivider('blank')
240 main_ui.addButton(self.__search_menu_id, _("Search"), adv_fields + [FIELD_CURRENT_SERVICE])
241 main_ui.addDivider('blank')
242 main_ui.addDivider('blank') # a blank line again after the button
243
244 if adv_data: # display the search results
245 log.debug("Advanced search with %s on %s" % (adv_data, service_jid))
246 sub_cont.parent.setSelected(True)
247 main_ui.changeContainer(sub_cont.append(xml_tools.VerticalContainer(main_ui)))
248 main_ui.addDivider('dash')
249 d = self.searchRequest(service_jid, adv_data, profile)
250 d.addCallbacks(lambda elt: self._displaySearchResult(main_ui, elt),
251 lambda failure: main_ui.addText(failure.getErrorMessage()))
252 return d
253
254 return defer.succeed(None)
255
256
257 def _displaySearchResult(self, main_ui, elt):
258 """Display the search results.
259
260 @param main_ui (XMLUI): the main XMLUI instance
261 @param elt (domish.Element): form result element
262 """
263 if [child for child in elt.children if child.name == "item"]:
264 xml_tools.dataFormResult2AdvancedList(main_ui, elt)
265 else:
266 main_ui.addText(D_("The search gave no result"))
267
268 ## Retrieve the search fields ##
269
270
271 def _getFieldsUI(self, to_jid_s, profile_key):
272 """Ask a service to send us the list of the form fields it manages.
273
274 @param to_jid_s (unicode): XEP-0055 compliant search entity
275 @param profile_key (unicode): %(doc_profile_key)s
276 @return: a deferred XMLUI instance
277 """
278 d = self.getFieldsUI(jid.JID(to_jid_s), profile_key)
279 d.addCallback(lambda form: xml_tools.dataFormResult2XMLUI(form).toXml())
91 return d 280 return d
92 281
93 def getSearchUI(self, to_jid, profile_key): 282 def getFieldsUI(self, to_jid, profile_key):
94 """ Ask for a search interface 283 """Ask a service to send us the list of the form fields it manages.
95 @param to_jid: XEP-0055 compliant search entity 284
96 @param profile_key: %(doc_profile_key)s 285 @param to_jid (jid.JID): XEP-0055 compliant search entity
97 @return: XMLUI search interface """ 286 @param profile_key (unicode): %(doc_profile_key)s
287 @return: a deferred domish.Element
288 """
98 client = self.host.getClient(profile_key) 289 client = self.host.getClient(profile_key)
99 fields_request = IQ(client.xmlstream, 'get') 290 fields_request = IQ(client.xmlstream, 'get')
100 fields_request["from"] = client.jid.full() 291 fields_request["from"] = client.jid.full()
101 fields_request["to"] = to_jid.full() 292 fields_request["to"] = to_jid.full()
102 fields_request.addElement('query', NS_SEARCH) 293 fields_request.addElement('query', NS_SEARCH)
103 d = fields_request.send(to_jid.full()) 294 d = fields_request.send(to_jid.full())
104 d.addCallbacks(self._fieldsOk, self._fieldsErr, callbackArgs=[client.profile], errbackArgs=[client.profile]) 295 d.addCallbacks(self._getFieldsUICb, self._getFieldsUIEb)
105 return d 296 return d
106 297
107 def _fieldsOk(self, answer, profile): 298 def _getFieldsUICb(self, answer):
108 """got fields available""" 299 """Callback for self.getFieldsUI.
300
301 @param answer (domish.Element): search query element
302 @return: domish.Element
303 """
109 try: 304 try:
110 query_elts = answer.elements('jabber:iq:search', 'query').next() 305 query_elts = answer.elements('jabber:iq:search', 'query').next()
111 except StopIteration: 306 except StopIteration:
112 log.info(_("No query element found")) 307 log.info(_("No query element found"))
113 raise DataError # FIXME: StanzaError is probably more appropriate, check the RFC 308 raise DataError # FIXME: StanzaError is probably more appropriate, check the RFC
114 try: 309 try:
115 form_elt = query_elts.elements(data_form.NS_X_DATA, 'x').next() 310 form_elt = query_elts.elements(data_form.NS_X_DATA, 'x').next()
116 except StopIteration: 311 except StopIteration:
117 log.info(_("No data form found")) 312 log.info(_("No data form found"))
118 raise NotImplementedError("Only search through data form is implemented so far") 313 raise NotImplementedError("Only search through data form is implemented so far")
119 parsed_form = data_form.Form.fromElement(form_elt) 314 return form_elt
120 return xml_tools.dataForm2XMLUI(parsed_form, "") 315
121 316 def _getFieldsUIEb(self, failure):
122 def _fieldsErr(self, failure, profile): 317 """Errback to self.getFieldsUI.
123 """ Called when something is wrong with fields request """ 318
124 log.info(_("Fields request failure: %s") % unicode(failure.value)) 319 @param failure (defer.failure.Failure): twisted failure
125 return failure 320 @raise: the unchanged defer.failure.Failure
126 321 """
127 def _xmluiSearchRequest(self, raw_data, profile): 322 log.info(_("Fields request failure: %s") % unicode(failure.getErrorMessage()))
128 try: 323 raise failure
129 session_data = self._sessions.profileGet(raw_data["session_id"], profile) 324
130 except KeyError: 325
131 log.warning ("session id doesn't exist, session has probably expired") 326 ## Do the search ##
132 # TODO: send error dialog 327
133 return defer.succeed({}) 328
134 329 def _searchRequest(self, to_jid_s, search_data, profile_key):
135 data = xml_tools.XMLUIResult2DataFormResult(raw_data) 330 """Actually do a search, according to filled data.
136 entity =session_data['jid'] 331
137 d = self.searchRequest(entity, data, profile) 332 @param to_jid_s (unicode): XEP-0055 compliant search entity
138 d.addCallback(lambda xmlui: {'xmlui':xmlui.toXml()}) 333 @param search_data (dict): filled data, corresponding to the form obtained in getFieldsUI
139 del self._sessions[raw_data["session_id"]] 334 @param profile_key (unicode): %(doc_profile_key)s
335 @return: a deferred XMLUI string representation
336 """
337 d = self.searchRequest(jid.JID(to_jid_s), search_data, profile_key)
338 d.addCallback(lambda form: xml_tools.dataFormResult2XMLUI(form).toXml())
140 return d 339 return d
141 340
142 def _searchRequest(self, to_jid_s, search_dict, profile_key): 341 def searchRequest(self, to_jid, search_data, profile_key):
143 d = self.searchRequest(jid.JID(to_jid_s), search_dict, profile_key) 342 """Actually do a search, according to filled data.
144 d.addCallback(lambda xmlui: xmlui.toXml()) 343
145 return d 344 @param to_jid (jid.JID): XEP-0055 compliant search entity
146 345 @param search_data (dict): filled data, corresponding to the form obtained in getFieldsUI
147 def searchRequest(self, to_jid, search_dict, profile_key): 346 @param profile_key (unicode): %(doc_profile_key)s
148 """ Actually do a search, according to filled data 347 @return: a deferred domish.Element
149 @param to_jid: XEP-0055 compliant search entity 348 """
150 @param search_dict: filled data, corresponding to the form obtained in getSearchUI 349 if FIELD_SINGLE in search_data:
151 @param profile_key: %(doc_profile_key)s 350 value = search_data[FIELD_SINGLE]
152 @return: XMLUI search result """ 351 d = self.getFieldsUI(to_jid, profile_key)
352 d.addCallback(lambda elt: self.searchRequestMulti(to_jid, value, elt, profile_key))
353 return d
354
153 client = self.host.getClient(profile_key) 355 client = self.host.getClient(profile_key)
154 search_request = IQ(client.xmlstream, 'set') 356 search_request = IQ(client.xmlstream, 'set')
155 search_request["from"] = client.jid.full() 357 search_request["from"] = client.jid.full()
156 search_request["to"] = to_jid.full() 358 search_request["to"] = to_jid.full()
157 query_elt = search_request.addElement('query', NS_SEARCH) 359 query_elt = search_request.addElement('query', NS_SEARCH)
158 x_form = data_form.Form('submit', formNamespace = NS_SEARCH) 360 x_form = data_form.Form('submit', formNamespace=NS_SEARCH)
159 x_form.makeFields(search_dict) 361 x_form.makeFields(search_data)
160 query_elt.addChild(x_form.toElement()) 362 query_elt.addChild(x_form.toElement())
161 # TODO: XEP-0059 could be used here (with the needed new method attributes) 363 # TODO: XEP-0059 could be used here (with the needed new method attributes)
162 d = search_request.send(to_jid.full()) 364 d = search_request.send(to_jid.full())
163 d.addCallbacks(self._searchOk, self._searchErr, callbackArgs=[client.profile], errbackArgs=[client.profile]) 365 d.addCallbacks(self._searchOk, self._searchErr)
164 return d 366 return d
165 367
166 def _searchOk(self, answer, profile): 368 def searchRequestMulti(self, to_jid, value, form_elt, profile_key):
167 """got search available""" 369 """Search for a value simultaneously in all fields, returns the results compilation.
370
371 @param to_jid (jid.JID): XEP-0055 compliant search entity
372 @param value (unicode): value to search
373 @param form_elt (domish.Element): form element listing the fields
374 @param profile_key (unicode): %(doc_profile_key)s
375 @return: a deferred domish.Element
376 """
377 form = data_form.Form.fromElement(form_elt)
378 d_list = []
379 for field in [field.var for field in form.fieldList if field.var]:
380 d_list.append(self.searchRequest(to_jid, {field: value}, profile_key))
381 break
382
383 def cb(result): # return the results compiled in one domish element
384 result_elt = None
385 for success, form_elt in result:
386 if not success:
387 continue
388 if result_elt is None: # the result element is built over the first answer
389 result_elt = form_elt
390 continue
391 for item_elt in form_elt.elements('jabber:x:data', 'item'):
392 result_elt.addChild(item_elt)
393 if result_elt is None:
394 raise defer.failure.Failure(DataError(_("The search could not be performed")))
395 return result_elt
396
397 return defer.DeferredList(d_list).addCallback(cb)
398
399 def _searchOk(self, answer):
400 """Callback for self.searchRequest.
401
402 @param answer (domish.Element): search query element
403 @return: domish.Element
404 """
168 try: 405 try:
169 query_elts = answer.elements('jabber:iq:search', 'query').next() 406 query_elts = answer.elements('jabber:iq:search', 'query').next()
170 except StopIteration: 407 except StopIteration:
171 log.info(_("No query element found")) 408 log.info(_("No query element found"))
172 raise DataError # FIXME: StanzaError is probably more appropriate, check the RFC 409 raise DataError # FIXME: StanzaError is probably more appropriate, check the RFC
173 try: 410 try:
174 form_elt = query_elts.elements(data_form.NS_X_DATA, 'x').next() 411 form_elt = query_elts.elements(data_form.NS_X_DATA, 'x').next()
175 except StopIteration: 412 except StopIteration:
176 log.info(_("No data form found")) 413 log.info(_("No data form found"))
177 raise NotImplementedError("Only search through data form is implemented so far") 414 raise NotImplementedError("Only search through data form is implemented so far")
178 return xml_tools.dataFormResult2XMLUI(form_elt) 415 return form_elt
179 416
180 def _searchErr(self, failure, profile): 417 def _searchErr(self, failure):
181 """ Called when something is wrong with search request """ 418 """Errback to self.searchRequest.
182 log.info(_("Search request failure: %s") % unicode(failure.value)) 419
183 return failure 420 @param failure (defer.failure.Failure): twisted failure
421 @raise: the unchanged defer.failure.Failure
422 """
423 log.info(_("Search request failure: %s") % unicode(failure.getErrorMessage()))
424 raise failure
425
426
427 class XEP_0059_handler(XMPPHandler):
428 implements(iwokkel.IDisco)
429
430 def __init__(self, plugin_parent, profile):
431 self.plugin_parent = plugin_parent
432 self.host = plugin_parent.host
433 self.profile = profile
434
435 def getDiscoInfo(self, requestor, target, nodeIdentifier=''):
436 return [disco.DiscoFeature(NS_SEARCH)]
437
438 def getDiscoItems(self, requestor, target, nodeIdentifier=''):
439 return []
440