Mercurial > libervia-backend
comparison libervia/backend/plugins/plugin_xep_0346.py @ 4071:4b842c1fb686
refactoring: renamed `sat` package to `libervia.backend`
author | Goffi <goffi@goffi.org> |
---|---|
date | Fri, 02 Jun 2023 11:49:51 +0200 |
parents | sat/plugins/plugin_xep_0346.py@524856bd7b19 |
children | 0d7bb4df2343 |
comparison
equal
deleted
inserted
replaced
4070:d10748475025 | 4071:4b842c1fb686 |
---|---|
1 #!/usr/bin/env python3 | |
2 | |
3 # SàT plugin for XEP-0346 | |
4 # Copyright (C) 2009-2021 Jérôme Poisson (goffi@goffi.org) | |
5 | |
6 # This program is free software: you can redistribute it and/or modify | |
7 # it under the terms of the GNU Affero General Public License as published by | |
8 # the Free Software Foundation, either version 3 of the License, or | |
9 # (at your option) any later version. | |
10 | |
11 # This program is distributed in the hope that it will be useful, | |
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of | |
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
14 # GNU Affero General Public License for more details. | |
15 | |
16 # You should have received a copy of the GNU Affero General Public License | |
17 # along with this program. If not, see <http://www.gnu.org/licenses/>. | |
18 | |
19 from collections.abc import Iterable | |
20 import itertools | |
21 from typing import Optional | |
22 from zope.interface import implementer | |
23 from twisted.words.protocols.jabber import jid | |
24 from twisted.words.protocols.jabber.xmlstream import XMPPHandler | |
25 from twisted.words.xish import domish | |
26 from twisted.internet import defer | |
27 from wokkel import disco, iwokkel | |
28 from wokkel import data_form | |
29 from wokkel import generic | |
30 from libervia.backend.core.i18n import _ | |
31 from libervia.backend.core import exceptions | |
32 from libervia.backend.core.constants import Const as C | |
33 from libervia.backend.core.xmpp import SatXMPPEntity | |
34 from libervia.backend.tools import xml_tools | |
35 from libervia.backend.tools import utils | |
36 from libervia.backend.tools.common import date_utils | |
37 from libervia.backend.tools.common import data_format | |
38 from libervia.backend.core.log import getLogger | |
39 | |
40 log = getLogger(__name__) | |
41 | |
42 NS_FDP = "urn:xmpp:fdp:0" | |
43 TEMPLATE_PREFIX = "fdp/template/" | |
44 SUBMITTED_PREFIX = "fdp/submitted/" | |
45 | |
46 PLUGIN_INFO = { | |
47 C.PI_NAME: "Form Discovery and Publishing", | |
48 C.PI_IMPORT_NAME: "XEP-0346", | |
49 C.PI_TYPE: "EXP", | |
50 C.PI_PROTOCOLS: [], | |
51 C.PI_DEPENDENCIES: ["XEP-0060", "IDENTITY"], | |
52 C.PI_MAIN: "PubsubSchema", | |
53 C.PI_HANDLER: "yes", | |
54 C.PI_DESCRIPTION: _("""Handle Pubsub data schemas"""), | |
55 } | |
56 | |
57 | |
58 class PubsubSchema(object): | |
59 def __init__(self, host): | |
60 log.info(_("PubSub Schema initialization")) | |
61 self.host = host | |
62 self._p = self.host.plugins["XEP-0060"] | |
63 self._i = self.host.plugins["IDENTITY"] | |
64 host.bridge.add_method( | |
65 "ps_schema_get", | |
66 ".plugin", | |
67 in_sign="sss", | |
68 out_sign="s", | |
69 method=self._get_schema, | |
70 async_=True, | |
71 ) | |
72 host.bridge.add_method( | |
73 "ps_schema_set", | |
74 ".plugin", | |
75 in_sign="ssss", | |
76 out_sign="", | |
77 method=self._set_schema, | |
78 async_=True, | |
79 ) | |
80 host.bridge.add_method( | |
81 "ps_schema_ui_get", | |
82 ".plugin", | |
83 in_sign="sss", | |
84 out_sign="s", | |
85 method=lambda service, nodeIdentifier, profile_key: self._get_ui_schema( | |
86 service, nodeIdentifier, default_node=None, profile_key=profile_key), | |
87 async_=True, | |
88 ) | |
89 host.bridge.add_method( | |
90 "ps_schema_dict_get", | |
91 ".plugin", | |
92 in_sign="sss", | |
93 out_sign="s", | |
94 method=self._get_schema_dict, | |
95 async_=True, | |
96 ) | |
97 host.bridge.add_method( | |
98 "ps_schema_application_ns_get", | |
99 ".plugin", | |
100 in_sign="s", | |
101 out_sign="s", | |
102 method=self.get_application_ns, | |
103 ) | |
104 host.bridge.add_method( | |
105 "ps_schema_template_node_get", | |
106 ".plugin", | |
107 in_sign="s", | |
108 out_sign="s", | |
109 method=self.get_template_ns, | |
110 ) | |
111 host.bridge.add_method( | |
112 "ps_schema_submitted_node_get", | |
113 ".plugin", | |
114 in_sign="s", | |
115 out_sign="s", | |
116 method=self.get_submitted_ns, | |
117 ) | |
118 host.bridge.add_method( | |
119 "ps_items_form_get", | |
120 ".plugin", | |
121 in_sign="ssssiassss", | |
122 out_sign="(asa{ss})", | |
123 method=self._get_data_form_items, | |
124 async_=True, | |
125 ) | |
126 host.bridge.add_method( | |
127 "ps_item_form_send", | |
128 ".plugin", | |
129 in_sign="ssa{sas}ssa{ss}s", | |
130 out_sign="s", | |
131 method=self._send_data_form_item, | |
132 async_=True, | |
133 ) | |
134 | |
135 def get_handler(self, client): | |
136 return SchemaHandler() | |
137 | |
138 def get_application_ns(self, namespace): | |
139 """Retrieve application namespace, i.e. namespace without FDP prefix""" | |
140 if namespace.startswith(SUBMITTED_PREFIX): | |
141 namespace = namespace[len(SUBMITTED_PREFIX):] | |
142 elif namespace.startswith(TEMPLATE_PREFIX): | |
143 namespace = namespace[len(TEMPLATE_PREFIX):] | |
144 return namespace | |
145 | |
146 def get_template_ns(self, namespace: str) -> str: | |
147 """Returns node used for data template (i.e. schema)""" | |
148 app_ns = self.get_application_ns(namespace) | |
149 return f"{TEMPLATE_PREFIX}{app_ns}" | |
150 | |
151 def get_submitted_ns(self, namespace: str) -> str: | |
152 """Returns node to use to submit forms""" | |
153 return f"{SUBMITTED_PREFIX}{self.get_application_ns(namespace)}" | |
154 | |
155 def _get_schema_bridge_cb(self, schema_elt): | |
156 if schema_elt is None: | |
157 return "" | |
158 return schema_elt.toXml() | |
159 | |
160 def _get_schema(self, service, nodeIdentifier, profile_key=C.PROF_KEY_NONE): | |
161 client = self.host.get_client(profile_key) | |
162 service = None if not service else jid.JID(service) | |
163 d = defer.ensureDeferred(self.get_schema(client, service, nodeIdentifier)) | |
164 d.addCallback(self._get_schema_bridge_cb) | |
165 return d | |
166 | |
167 async def get_schema(self, client, service, nodeIdentifier): | |
168 """retrieve PubSub node schema | |
169 | |
170 @param service(jid.JID, None): jid of PubSub service | |
171 None to use our PEP | |
172 @param nodeIdentifier(unicode): node to get schema from | |
173 @return (domish.Element, None): schema (<x> element) | |
174 None if no schema has been set on this node | |
175 """ | |
176 app_ns = self.get_application_ns(nodeIdentifier) | |
177 node_id = f"{TEMPLATE_PREFIX}{app_ns}" | |
178 items_data = await self._p.get_items(client, service, node_id, max_items=1) | |
179 try: | |
180 schema = next(items_data[0][0].elements(data_form.NS_X_DATA, 'x')) | |
181 except IndexError: | |
182 schema = None | |
183 except StopIteration: | |
184 log.warning( | |
185 f"No schema found in item of {service!r} at node {nodeIdentifier!r}: " | |
186 f"\n{items_data[0][0].toXml()}" | |
187 ) | |
188 schema = None | |
189 return schema | |
190 | |
191 async def get_schema_form(self, client, service, nodeIdentifier, schema=None, | |
192 form_type="form", copy_form=True): | |
193 """Get data form from node's schema | |
194 | |
195 @param service(None, jid.JID): PubSub service | |
196 @param nodeIdentifier(unicode): node | |
197 @param schema(domish.Element, data_form.Form, None): node schema | |
198 if domish.Element, will be converted to data form | |
199 if data_form.Form it will be returned without modification | |
200 if None, it will be retrieved from node (imply one additional XMPP request) | |
201 @param form_type(unicode): type of the form | |
202 @param copy_form(bool): if True and if schema is already a data_form.Form, will deep copy it before returning | |
203 needed when the form is reused and it will be modified (e.g. in send_data_form_item) | |
204 @return(data_form.Form): data form | |
205 the form should not be modified if copy_form is not set | |
206 """ | |
207 if schema is None: | |
208 log.debug(_("unspecified schema, we need to request it")) | |
209 schema = await self.get_schema(client, service, nodeIdentifier) | |
210 if schema is None: | |
211 raise exceptions.DataError( | |
212 _( | |
213 "no schema specified, and this node has no schema either, we can't construct the data form" | |
214 ) | |
215 ) | |
216 elif isinstance(schema, data_form.Form): | |
217 if copy_form: | |
218 # XXX: we don't use deepcopy as it will do an infinite loop if a | |
219 # domish.Element is present in the form fields (happens for | |
220 # XEP-0315 data forms XML Element) | |
221 schema = data_form.Form( | |
222 formType = schema.formType, | |
223 title = schema.title, | |
224 instructions = schema.instructions[:], | |
225 formNamespace = schema.formNamespace, | |
226 fields = schema.fieldList, | |
227 ) | |
228 return schema | |
229 | |
230 try: | |
231 form = data_form.Form.fromElement(schema) | |
232 except data_form.Error as e: | |
233 raise exceptions.DataError(_("Invalid Schema: {msg}").format(msg=e)) | |
234 form.formType = form_type | |
235 return form | |
236 | |
237 def schema_2_xmlui(self, schema_elt): | |
238 form = data_form.Form.fromElement(schema_elt) | |
239 xmlui = xml_tools.data_form_2_xmlui(form, "") | |
240 return xmlui | |
241 | |
242 def _get_ui_schema(self, service, nodeIdentifier, default_node=None, | |
243 profile_key=C.PROF_KEY_NONE): | |
244 if not nodeIdentifier: | |
245 if not default_node: | |
246 raise ValueError(_("nodeIndentifier needs to be set")) | |
247 nodeIdentifier = default_node | |
248 client = self.host.get_client(profile_key) | |
249 service = None if not service else jid.JID(service) | |
250 d = self.get_ui_schema(client, service, nodeIdentifier) | |
251 d.addCallback(lambda xmlui: xmlui.toXml()) | |
252 return d | |
253 | |
254 def get_ui_schema(self, client, service, nodeIdentifier): | |
255 d = defer.ensureDeferred(self.get_schema(client, service, nodeIdentifier)) | |
256 d.addCallback(self.schema_2_xmlui) | |
257 return d | |
258 | |
259 def _set_schema(self, service, nodeIdentifier, schema, profile_key=C.PROF_KEY_NONE): | |
260 client = self.host.get_client(profile_key) | |
261 service = None if not service else jid.JID(service) | |
262 schema = generic.parseXml(schema.encode()) | |
263 return defer.ensureDeferred( | |
264 self.set_schema(client, service, nodeIdentifier, schema) | |
265 ) | |
266 | |
267 async def set_schema(self, client, service, nodeIdentifier, schema): | |
268 """Set or replace PubSub node schema | |
269 | |
270 @param schema(domish.Element, None): schema to set | |
271 None if schema need to be removed | |
272 """ | |
273 node_id = self.get_template_ns(nodeIdentifier) | |
274 node_options = { | |
275 self._p.OPT_ACCESS_MODEL: self._p.ACCESS_OPEN, | |
276 self._p.OPT_PERSIST_ITEMS: 1, | |
277 self._p.OPT_MAX_ITEMS: 1, | |
278 self._p.OPT_DELIVER_PAYLOADS: 1, | |
279 self._p.OPT_SEND_ITEM_SUBSCRIBE: 1, | |
280 self._p.OPT_PUBLISH_MODEL: self._p.PUBLISH_MODEL_PUBLISHERS, | |
281 } | |
282 await self._p.create_if_new_node(client, service, node_id, node_options) | |
283 await self._p.send_item(client, service, node_id, schema, self._p.ID_SINGLETON) | |
284 | |
285 def _get_schema_dict(self, service, nodeIdentifier, profile): | |
286 service = None if not service else jid.JID(service) | |
287 client = self.host.get_client(profile) | |
288 d = defer.ensureDeferred(self.get_schema_dict(client, service, nodeIdentifier)) | |
289 d.addCallback(data_format.serialise) | |
290 return d | |
291 | |
292 async def get_schema_dict( | |
293 self, | |
294 client: SatXMPPEntity, | |
295 service: Optional[jid.JID], | |
296 nodeIdentifier: str) -> dict: | |
297 """Retrieve a node schema and format it a simple dictionary | |
298 | |
299 The dictionary is made so it can be easily serialisable | |
300 """ | |
301 schema_form = await self.get_schema_form(client, service, nodeIdentifier) | |
302 return xml_tools.data_form_2_data_dict(schema_form) | |
303 | |
304 def _get_data_form_items(self, form_ns="", service="", node="", schema="", max_items=10, | |
305 item_ids=None, sub_id=None, extra="", | |
306 profile_key=C.PROF_KEY_NONE): | |
307 client = self.host.get_client(profile_key) | |
308 service = jid.JID(service) if service else None | |
309 if not node: | |
310 raise exceptions.DataError(_("empty node is not allowed")) | |
311 if schema: | |
312 schema = generic.parseXml(schema.encode("utf-8")) | |
313 else: | |
314 schema = None | |
315 max_items = None if max_items == C.NO_LIMIT else max_items | |
316 extra = self._p.parse_extra(data_format.deserialise(extra)) | |
317 d = defer.ensureDeferred( | |
318 self.get_data_form_items( | |
319 client, | |
320 service, | |
321 node, | |
322 schema, | |
323 max_items or None, | |
324 item_ids, | |
325 sub_id or None, | |
326 extra.rsm_request, | |
327 extra.extra, | |
328 form_ns=form_ns or None, | |
329 ) | |
330 ) | |
331 d.addCallback(self._p.trans_items_data) | |
332 return d | |
333 | |
334 async def get_data_form_items(self, client, service, nodeIdentifier, schema=None, | |
335 max_items=None, item_ids=None, sub_id=None, rsm_request=None, | |
336 extra=None, default_node=None, form_ns=None, filters=None): | |
337 """Get items known as being data forms, and convert them to XMLUI | |
338 | |
339 @param schema(domish.Element, data_form.Form, None): schema of the node if known | |
340 if None, it will be retrieved from node | |
341 @param default_node(unicode): node to use if nodeIdentifier is None or empty | |
342 @param form_ns (unicode, None): namespace of the form | |
343 None to accept everything, even if form has no namespace | |
344 @param filters(dict, None): same as for xml_tools.data_form_result_2_xmlui | |
345 other parameters as the same as for [get_items] | |
346 @return (list[unicode]): XMLUI of the forms | |
347 if an item is invalid (not corresponding to form_ns or not a data_form) | |
348 it will be skipped | |
349 @raise ValueError: one argument is invalid | |
350 """ | |
351 if not nodeIdentifier: | |
352 if not default_node: | |
353 raise ValueError( | |
354 _("default_node must be set if nodeIdentifier is not set") | |
355 ) | |
356 nodeIdentifier = default_node | |
357 submitted_ns = self.get_submitted_ns(nodeIdentifier) | |
358 # we need the initial form to get options of fields when suitable | |
359 schema_form = await self.get_schema_form( | |
360 client, service, nodeIdentifier, schema, form_type="result", copy_form=False | |
361 ) | |
362 items_data = await self._p.get_items( | |
363 client, | |
364 service, | |
365 submitted_ns, | |
366 max_items, | |
367 item_ids, | |
368 sub_id, | |
369 rsm_request, | |
370 extra, | |
371 ) | |
372 items, metadata = items_data | |
373 items_xmlui = [] | |
374 for item_elt in items: | |
375 for x_elt in item_elt.elements(data_form.NS_X_DATA, "x"): | |
376 form = data_form.Form.fromElement(x_elt) | |
377 if form_ns and form.formNamespace != form_ns: | |
378 log.debug( | |
379 f"form's namespace ({form.formNamespace!r}) differs from expected" | |
380 f"{form_ns!r}" | |
381 ) | |
382 continue | |
383 prepend = [ | |
384 ("label", "id"), | |
385 ("text", item_elt["id"], "id"), | |
386 ("label", "publisher"), | |
387 ] | |
388 try: | |
389 publisher = jid.JID(item_elt['publisher']) | |
390 except (KeyError, jid.InvalidFormat): | |
391 pass | |
392 else: | |
393 prepend.append(("jid", publisher, "publisher")) | |
394 xmlui = xml_tools.data_form_result_2_xmlui( | |
395 form, | |
396 schema_form, | |
397 # FIXME: conflicts with schema (i.e. if "id" or "publisher" already exists) | |
398 # are not checked | |
399 prepend=prepend, | |
400 filters=filters, | |
401 read_only=False, | |
402 ) | |
403 items_xmlui.append(xmlui) | |
404 break | |
405 return (items_xmlui, metadata) | |
406 | |
407 def _send_data_form_item(self, service, nodeIdentifier, values, schema=None, | |
408 item_id=None, extra=None, profile_key=C.PROF_KEY_NONE): | |
409 client = self.host.get_client(profile_key) | |
410 service = None if not service else jid.JID(service) | |
411 if schema: | |
412 schema = generic.parseXml(schema.encode("utf-8")) | |
413 else: | |
414 schema = None | |
415 d = defer.ensureDeferred( | |
416 self.send_data_form_item( | |
417 client, | |
418 service, | |
419 nodeIdentifier, | |
420 values, | |
421 schema, | |
422 item_id or None, | |
423 extra, | |
424 deserialise=True, | |
425 ) | |
426 ) | |
427 d.addCallback(lambda ret: ret or "") | |
428 return d | |
429 | |
430 async def send_data_form_item( | |
431 self, client, service, nodeIdentifier, values, schema=None, item_id=None, | |
432 extra=None, deserialise=False): | |
433 """Publish an item as a dataform when we know that there is a schema | |
434 | |
435 @param values(dict[key(unicode), [iterable[object], object]]): values set for the | |
436 form. If not iterable, will be put in a list. | |
437 @param schema(domish.Element, data_form.Form, None): data schema | |
438 None to retrieve data schema from node (need to do a additional XMPP call) | |
439 Schema is needed to construct data form to publish | |
440 @param deserialise(bool): if True, data are list of unicode and must be | |
441 deserialized according to expected type. | |
442 This is done in this method and not directly in _send_data_form_item because we | |
443 need to know the data type which is in the form, not availablable in | |
444 _send_data_form_item | |
445 other parameters as the same as for [self._p.send_item] | |
446 @return (unicode): id of the created item | |
447 """ | |
448 form = await self.get_schema_form( | |
449 client, service, nodeIdentifier, schema, form_type="submit" | |
450 ) | |
451 | |
452 for name, values_list in values.items(): | |
453 try: | |
454 field = form.fields[name] | |
455 except KeyError: | |
456 log.warning( | |
457 _("field {name} doesn't exist, ignoring it").format(name=name) | |
458 ) | |
459 continue | |
460 if isinstance(values_list, str) or not isinstance( | |
461 values_list, Iterable | |
462 ): | |
463 values_list = [values_list] | |
464 if deserialise: | |
465 if field.fieldType == "boolean": | |
466 values_list = [C.bool(v) for v in values_list] | |
467 elif field.fieldType == "text-multi": | |
468 # for text-multi, lines must be put on separate values | |
469 values_list = list( | |
470 itertools.chain(*[v.splitlines() for v in values_list]) | |
471 ) | |
472 elif xml_tools.is_xhtml_field(field): | |
473 values_list = [generic.parseXml(v.encode("utf-8")) | |
474 for v in values_list] | |
475 elif "jid" in (field.fieldType or ""): | |
476 values_list = [jid.JID(v) for v in values_list] | |
477 if "list" in (field.fieldType or ""): | |
478 # for lists, we check that given values are allowed in form | |
479 allowed_values = [o.value for o in field.options] | |
480 values_list = [v for v in values_list if v in allowed_values] | |
481 if not values_list: | |
482 # if values don't map to allowed values, we use default ones | |
483 values_list = field.values | |
484 elif field.ext_type == 'xml': | |
485 # FIXME: XML elements are not handled correctly, we need to know if we | |
486 # have actual XML/XHTML, or text to escape | |
487 for idx, value in enumerate(values_list[:]): | |
488 if isinstance(value, domish.Element): | |
489 if (field.value and (value.name != field.value.name | |
490 or value.uri != field.value.uri)): | |
491 # the element is not the one expected in form, so we create the right element | |
492 # to wrap the current value | |
493 wrapper_elt = domish.Element((field.value.uri, field.value.name)) | |
494 wrapper_elt.addChild(value) | |
495 values_list[idx] = wrapper_elt | |
496 else: | |
497 # we have to convert the value to a domish.Element | |
498 if field.value and field.value.uri == C.NS_XHTML: | |
499 div_elt = domish.Element((C.NS_XHTML, 'div')) | |
500 div_elt.addContent(str(value)) | |
501 values_list[idx] = div_elt | |
502 else: | |
503 # only XHTML fields are handled for now | |
504 raise NotImplementedError | |
505 | |
506 field.values = values_list | |
507 | |
508 return await self._p.send_item( | |
509 client, service, nodeIdentifier, form.toElement(), item_id, extra | |
510 ) | |
511 | |
512 ## filters ## | |
513 # filters useful for data form to XMLUI conversion # | |
514 | |
515 def value_or_publisher_filter(self, form_xmlui, widget_type, args, kwargs): | |
516 """Replace missing value by publisher's user part""" | |
517 if not args[0]: | |
518 # value is not filled: we use user part of publisher (if we have it) | |
519 try: | |
520 publisher = jid.JID(form_xmlui.named_widgets["publisher"].value) | |
521 except (KeyError, RuntimeError): | |
522 pass | |
523 else: | |
524 args[0] = publisher.user.capitalize() | |
525 return widget_type, args, kwargs | |
526 | |
527 def textbox_2_list_filter(self, form_xmlui, widget_type, args, kwargs): | |
528 """Split lines of a textbox in a list | |
529 | |
530 main use case is using a textbox for labels | |
531 """ | |
532 if widget_type != "textbox": | |
533 return widget_type, args, kwargs | |
534 widget_type = "list" | |
535 options = [o for o in args.pop(0).split("\n") if o] | |
536 kwargs = { | |
537 "options": options, | |
538 "name": kwargs.get("name"), | |
539 "styles": ("noselect", "extensible", "reducible"), | |
540 } | |
541 return widget_type, args, kwargs | |
542 | |
543 def date_filter(self, form_xmlui, widget_type, args, kwargs): | |
544 """Convert a string with a date to a unix timestamp""" | |
545 if widget_type != "string" or not args[0]: | |
546 return widget_type, args, kwargs | |
547 # we convert XMPP date to timestamp | |
548 try: | |
549 args[0] = str(date_utils.date_parse(args[0])) | |
550 except Exception as e: | |
551 log.warning(_("Can't parse date field: {msg}").format(msg=e)) | |
552 return widget_type, args, kwargs | |
553 | |
554 ## Helper methods ## | |
555 | |
556 def prepare_bridge_get(self, service, node, max_items, sub_id, extra, profile_key): | |
557 """Parse arguments received from bridge *Get methods and return higher level data | |
558 | |
559 @return (tuple): (client, service, node, max_items, extra, sub_id) usable for | |
560 internal methods | |
561 """ | |
562 client = self.host.get_client(profile_key) | |
563 service = jid.JID(service) if service else None | |
564 if not node: | |
565 node = None | |
566 max_items = None if max_items == C.NO_LIMIT else max_items | |
567 if not sub_id: | |
568 sub_id = None | |
569 extra = self._p.parse_extra(extra) | |
570 | |
571 return client, service, node, max_items, extra, sub_id | |
572 | |
573 def _get(self, service="", node="", max_items=10, item_ids=None, sub_id=None, | |
574 extra="", default_node=None, form_ns=None, filters=None, | |
575 profile_key=C.PROF_KEY_NONE): | |
576 """bridge method to retrieve data from node with schema | |
577 | |
578 this method is a helper so dependant plugins can use it directly | |
579 when adding *Get methods | |
580 extra can have the key "labels_as_list" which is a hack to convert | |
581 labels from textbox to list in XMLUI, which usually render better | |
582 in final UI. | |
583 """ | |
584 if filters is None: | |
585 filters = {} | |
586 extra = data_format.deserialise(extra) | |
587 # XXX: Q&D way to get list for labels when displaying them, but text when we | |
588 # have to modify them | |
589 if C.bool(extra.get("labels_as_list", C.BOOL_FALSE)): | |
590 filters = filters.copy() | |
591 filters["labels"] = self.textbox_2_list_filter | |
592 client, service, node, max_items, extra, sub_id = self.prepare_bridge_get( | |
593 service, node, max_items, sub_id, extra, profile_key | |
594 ) | |
595 d = defer.ensureDeferred( | |
596 self.get_data_form_items( | |
597 client, | |
598 service, | |
599 node or None, | |
600 max_items=max_items, | |
601 item_ids=item_ids, | |
602 sub_id=sub_id, | |
603 rsm_request=extra.rsm_request, | |
604 extra=extra.extra, | |
605 default_node=default_node, | |
606 form_ns=form_ns, | |
607 filters=filters, | |
608 ) | |
609 ) | |
610 d.addCallback(self._p.trans_items_data) | |
611 d.addCallback(lambda data: data_format.serialise(data)) | |
612 return d | |
613 | |
614 def prepare_bridge_set(self, service, node, schema, item_id, extra, profile_key): | |
615 """Parse arguments received from bridge *Set methods and return higher level data | |
616 | |
617 @return (tuple): (client, service, node, schema, item_id, extra) usable for | |
618 internal methods | |
619 """ | |
620 client = self.host.get_client(profile_key) | |
621 service = None if not service else jid.JID(service) | |
622 if schema: | |
623 schema = generic.parseXml(schema.encode("utf-8")) | |
624 else: | |
625 schema = None | |
626 extra = data_format.deserialise(extra) | |
627 return client, service, node or None, schema, item_id or None, extra | |
628 | |
629 async def copy_missing_values(self, client, service, node, item_id, form_ns, values): | |
630 """Retrieve values existing in original item and missing in update | |
631 | |
632 Existing item will be retrieve, and values not already specified in values will | |
633 be filled | |
634 @param service: same as for [XEP_0060.get_items] | |
635 @param node: same as for [XEP_0060.get_items] | |
636 @param item_id(unicode): id of the item to retrieve | |
637 @param form_ns (unicode, None): namespace of the form | |
638 @param values(dict): values to fill | |
639 This dict will be modified *in place* to fill value present in existing | |
640 item and missing in the dict. | |
641 """ | |
642 try: | |
643 # we get previous item | |
644 items_data = await self._p.get_items( | |
645 client, service, node, item_ids=[item_id] | |
646 ) | |
647 item_elt = items_data[0][0] | |
648 except Exception as e: | |
649 log.warning( | |
650 _("Can't get previous item, update ignored: {reason}").format( | |
651 reason=e | |
652 ) | |
653 ) | |
654 else: | |
655 # and parse it | |
656 form = data_form.findForm(item_elt, form_ns) | |
657 if form is None: | |
658 log.warning( | |
659 _("Can't parse previous item, update ignored: data form not found") | |
660 ) | |
661 else: | |
662 for name, field in form.fields.items(): | |
663 if name not in values: | |
664 values[name] = "\n".join(str(v) for v in field.values) | |
665 | |
666 def _set(self, service, node, values, schema=None, item_id=None, extra=None, | |
667 default_node=None, form_ns=None, fill_author=True, | |
668 profile_key=C.PROF_KEY_NONE): | |
669 """bridge method to set item in node with schema | |
670 | |
671 this method is a helper so dependant plugins can use it directly | |
672 when adding *Set methods | |
673 """ | |
674 client, service, node, schema, item_id, extra = self.prepare_bridge_set( | |
675 service, node, schema, item_id, extra | |
676 ) | |
677 d = defer.ensureDeferred(self.set( | |
678 client, | |
679 service, | |
680 node, | |
681 values, | |
682 schema, | |
683 item_id, | |
684 extra, | |
685 deserialise=True, | |
686 form_ns=form_ns, | |
687 default_node=default_node, | |
688 fill_author=fill_author, | |
689 )) | |
690 d.addCallback(lambda ret: ret or "") | |
691 return d | |
692 | |
693 async def set( | |
694 self, client, service, node, values, schema, item_id, extra, deserialise, | |
695 form_ns, default_node=None, fill_author=True): | |
696 """Set an item in a node with a schema | |
697 | |
698 This method can be used directly by *Set methods added by dependant plugin | |
699 @param values(dict[key(unicode), [iterable[object]|object]]): values of the items | |
700 if value is not iterable, it will be put in a list | |
701 'created' and 'updated' will be forced to current time: | |
702 - 'created' is set if item_id is None, i.e. if it's a new ticket | |
703 - 'updated' is set everytime | |
704 @param extra(dict, None): same as for [XEP-0060.send_item] with additional keys: | |
705 - update(bool): if True, get previous item data to merge with current one | |
706 if True, item_id must be set | |
707 @param form_ns (unicode, None): namespace of the form | |
708 needed when an update is done | |
709 @param default_node(unicode, None): value to use if node is not set | |
710 other arguments are same as for [self._s.send_data_form_item] | |
711 @return (unicode): id of the created item | |
712 """ | |
713 if extra is None: | |
714 extra = {} | |
715 if not node: | |
716 if default_node is None: | |
717 raise ValueError(_("default_node must be set if node is not set")) | |
718 node = default_node | |
719 node = self.get_submitted_ns(node) | |
720 now = utils.xmpp_date() | |
721 if not item_id: | |
722 values["created"] = now | |
723 elif extra.get("update", False): | |
724 if item_id is None: | |
725 raise exceptions.DataError( | |
726 _('if extra["update"] is set, item_id must be set too') | |
727 ) | |
728 await self.copy_missing_values(client, service, node, item_id, form_ns, values) | |
729 | |
730 values["updated"] = now | |
731 if fill_author: | |
732 if not values.get("author"): | |
733 id_data = await self._i.get_identity(client, None, ["nicknames"]) | |
734 values["author"] = id_data['nicknames'][0] | |
735 if not values.get("author_jid"): | |
736 values["author_jid"] = client.jid.full() | |
737 item_id = await self.send_data_form_item( | |
738 client, service, node, values, schema, item_id, extra, deserialise | |
739 ) | |
740 return item_id | |
741 | |
742 | |
743 @implementer(iwokkel.IDisco) | |
744 class SchemaHandler(XMPPHandler): | |
745 | |
746 def getDiscoInfo(self, requestor, service, nodeIdentifier=""): | |
747 return [disco.DiscoFeature(NS_FDP)] | |
748 | |
749 def getDiscoItems(self, requestor, service, nodeIdentifier=""): | |
750 return [] |