comparison sat/plugins/plugin_xep_0346.py @ 3452:bb0225aaf4e6

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