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 []