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