Mercurial > libervia-backend
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 |