Mercurial > libervia-backend
comparison libervia/backend/plugins/plugin_misc_lists.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_misc_lists.py@524856bd7b19 |
children | 0d7bb4df2343 |
comparison
equal
deleted
inserted
replaced
4070:d10748475025 | 4071:4b842c1fb686 |
---|---|
1 #!/usr/bin/env python3 | |
2 | |
3 # Copyright (C) 2009-2021 Jérôme Poisson (goffi@goffi.org) | |
4 | |
5 # This program is free software: you can redistribute it and/or modify | |
6 # it under the terms of the GNU Affero General Public License as published by | |
7 # the Free Software Foundation, either version 3 of the License, or | |
8 # (at your option) any later version. | |
9 | |
10 # This program is distributed in the hope that it will be useful, | |
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of | |
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
13 # GNU Affero General Public License for more details. | |
14 | |
15 # You should have received a copy of the GNU Affero General Public License | |
16 # along with this program. If not, see <http://www.gnu.org/licenses/>. | |
17 | |
18 import shortuuid | |
19 from typing import List, Tuple, Optional | |
20 from twisted.internet import defer | |
21 from twisted.words.xish import domish | |
22 from twisted.words.protocols.jabber import jid | |
23 from libervia.backend.core.i18n import _, D_ | |
24 from libervia.backend.core.xmpp import SatXMPPEntity | |
25 from libervia.backend.core.constants import Const as C | |
26 from libervia.backend.tools import xml_tools | |
27 from libervia.backend.tools.common import uri | |
28 from libervia.backend.tools.common import data_format | |
29 from libervia.backend.core.log import getLogger | |
30 | |
31 log = getLogger(__name__) | |
32 | |
33 # XXX: this plugin was formely named "tickets", thus the namespace keeps this | |
34 # name | |
35 APP_NS_TICKETS = "org.salut-a-toi.tickets:0" | |
36 NS_TICKETS_TYPE = "org.salut-a-toi.tickets#type:0" | |
37 | |
38 PLUGIN_INFO = { | |
39 C.PI_NAME: _("Pubsub Lists"), | |
40 C.PI_IMPORT_NAME: "LISTS", | |
41 C.PI_TYPE: "EXP", | |
42 C.PI_PROTOCOLS: [], | |
43 C.PI_DEPENDENCIES: ["XEP-0060", "XEP-0346", "XEP-0277", "IDENTITY", | |
44 "PUBSUB_INVITATION"], | |
45 C.PI_MAIN: "PubsubLists", | |
46 C.PI_HANDLER: "no", | |
47 C.PI_DESCRIPTION: _("""Pubsub lists management plugin"""), | |
48 } | |
49 | |
50 TEMPLATES = { | |
51 "todo": { | |
52 "name": D_("TODO List"), | |
53 "icon": "check", | |
54 "fields": [ | |
55 {"name": "title"}, | |
56 {"name": "author"}, | |
57 {"name": "created"}, | |
58 {"name": "updated"}, | |
59 {"name": "time_limit"}, | |
60 {"name": "labels", "type": "text-multi"}, | |
61 { | |
62 "name": "status", | |
63 "label": D_("status"), | |
64 "type": "list-single", | |
65 "options": [ | |
66 { | |
67 "label": D_("to do"), | |
68 "value": "todo" | |
69 }, | |
70 { | |
71 "label": D_("in progress"), | |
72 "value": "in_progress" | |
73 }, | |
74 { | |
75 "label": D_("done"), | |
76 "value": "done" | |
77 }, | |
78 ], | |
79 "value": "todo" | |
80 }, | |
81 { | |
82 "name": "priority", | |
83 "label": D_("priority"), | |
84 "type": "list-single", | |
85 "options": [ | |
86 { | |
87 "label": D_("major"), | |
88 "value": "major" | |
89 }, | |
90 { | |
91 "label": D_("normal"), | |
92 "value": "normal" | |
93 }, | |
94 { | |
95 "label": D_("minor"), | |
96 "value": "minor" | |
97 }, | |
98 ], | |
99 "value": "normal" | |
100 }, | |
101 {"name": "body", "type": "xhtml"}, | |
102 {"name": "comments_uri"}, | |
103 ] | |
104 }, | |
105 "grocery": { | |
106 "name": D_("Grocery List"), | |
107 "icon": "basket", | |
108 "fields": [ | |
109 {"name": "name", "label": D_("name")}, | |
110 {"name": "quantity", "label": D_("quantity")}, | |
111 { | |
112 "name": "status", | |
113 "label": D_("status"), | |
114 "type": "list-single", | |
115 "options": [ | |
116 { | |
117 "label": D_("to buy"), | |
118 "value": "to_buy" | |
119 }, | |
120 { | |
121 "label": D_("bought"), | |
122 "value": "bought" | |
123 }, | |
124 ], | |
125 "value": "to_buy" | |
126 }, | |
127 ] | |
128 }, | |
129 "tickets": { | |
130 "name": D_("Tickets"), | |
131 "icon": "clipboard", | |
132 "fields": [ | |
133 {"name": "title"}, | |
134 {"name": "author"}, | |
135 {"name": "created"}, | |
136 {"name": "updated"}, | |
137 {"name": "labels", "type": "text-multi"}, | |
138 { | |
139 "name": "type", | |
140 "label": D_("type"), | |
141 "type": "list-single", | |
142 "options": [ | |
143 { | |
144 "label": D_("bug"), | |
145 "value": "bug" | |
146 }, | |
147 { | |
148 "label": D_("feature request"), | |
149 "value": "feature" | |
150 }, | |
151 ], | |
152 "value": "bug" | |
153 }, | |
154 { | |
155 "name": "status", | |
156 "label": D_("status"), | |
157 "type": "list-single", | |
158 "options": [ | |
159 { | |
160 "label": D_("queued"), | |
161 "value": "queued" | |
162 }, | |
163 { | |
164 "label": D_("started"), | |
165 "value": "started" | |
166 }, | |
167 { | |
168 "label": D_("review"), | |
169 "value": "review" | |
170 }, | |
171 { | |
172 "label": D_("closed"), | |
173 "value": "closed" | |
174 }, | |
175 ], | |
176 "value": "queued" | |
177 }, | |
178 { | |
179 "name": "priority", | |
180 "label": D_("priority"), | |
181 "type": "list-single", | |
182 "options": [ | |
183 { | |
184 "label": D_("major"), | |
185 "value": "major" | |
186 }, | |
187 { | |
188 "label": D_("normal"), | |
189 "value": "normal" | |
190 }, | |
191 { | |
192 "label": D_("minor"), | |
193 "value": "minor" | |
194 }, | |
195 ], | |
196 "value": "normal" | |
197 }, | |
198 {"name": "body", "type": "xhtml"}, | |
199 {"name": "comments_uri"}, | |
200 ] | |
201 } | |
202 } | |
203 | |
204 | |
205 class PubsubLists: | |
206 | |
207 def __init__(self, host): | |
208 log.info(_("Pubsub lists plugin initialization")) | |
209 self.host = host | |
210 self._s = self.host.plugins["XEP-0346"] | |
211 self.namespace = self._s.get_submitted_ns(APP_NS_TICKETS) | |
212 host.register_namespace("tickets", APP_NS_TICKETS) | |
213 host.register_namespace("tickets_type", NS_TICKETS_TYPE) | |
214 self.host.plugins["PUBSUB_INVITATION"].register( | |
215 APP_NS_TICKETS, self | |
216 ) | |
217 self._p = self.host.plugins["XEP-0060"] | |
218 self._m = self.host.plugins["XEP-0277"] | |
219 host.bridge.add_method( | |
220 "list_get", | |
221 ".plugin", | |
222 in_sign="ssiassss", | |
223 out_sign="s", | |
224 method=lambda service, node, max_items, items_ids, sub_id, extra, profile_key: | |
225 self._s._get( | |
226 service, | |
227 node, | |
228 max_items, | |
229 items_ids, | |
230 sub_id, | |
231 extra, | |
232 default_node=self.namespace, | |
233 form_ns=APP_NS_TICKETS, | |
234 filters={ | |
235 "author": self._s.value_or_publisher_filter, | |
236 "created": self._s.date_filter, | |
237 "updated": self._s.date_filter, | |
238 "time_limit": self._s.date_filter, | |
239 }, | |
240 profile_key=profile_key), | |
241 async_=True, | |
242 ) | |
243 host.bridge.add_method( | |
244 "list_set", | |
245 ".plugin", | |
246 in_sign="ssa{sas}ssss", | |
247 out_sign="s", | |
248 method=self._set, | |
249 async_=True, | |
250 ) | |
251 host.bridge.add_method( | |
252 "list_delete_item", | |
253 ".plugin", | |
254 in_sign="sssbs", | |
255 out_sign="", | |
256 method=self._delete, | |
257 async_=True, | |
258 ) | |
259 host.bridge.add_method( | |
260 "list_schema_get", | |
261 ".plugin", | |
262 in_sign="sss", | |
263 out_sign="s", | |
264 method=lambda service, nodeIdentifier, profile_key: self._s._get_ui_schema( | |
265 service, nodeIdentifier, default_node=self.namespace, | |
266 profile_key=profile_key), | |
267 async_=True, | |
268 ) | |
269 host.bridge.add_method( | |
270 "lists_list", | |
271 ".plugin", | |
272 in_sign="sss", | |
273 out_sign="s", | |
274 method=self._lists_list, | |
275 async_=True, | |
276 ) | |
277 host.bridge.add_method( | |
278 "list_templates_names_get", | |
279 ".plugin", | |
280 in_sign="ss", | |
281 out_sign="s", | |
282 method=self._get_templates_names, | |
283 ) | |
284 host.bridge.add_method( | |
285 "list_template_get", | |
286 ".plugin", | |
287 in_sign="sss", | |
288 out_sign="s", | |
289 method=self._get_template, | |
290 ) | |
291 host.bridge.add_method( | |
292 "list_template_create", | |
293 ".plugin", | |
294 in_sign="ssss", | |
295 out_sign="(ss)", | |
296 method=self._create_template, | |
297 async_=True, | |
298 ) | |
299 | |
300 async def on_invitation_preflight( | |
301 self, | |
302 client: SatXMPPEntity, | |
303 namespace: str, | |
304 name: str, | |
305 extra: dict, | |
306 service: jid.JID, | |
307 node: str, | |
308 item_id: Optional[str], | |
309 item_elt: domish.Element | |
310 ) -> None: | |
311 try: | |
312 schema = await self._s.get_schema_form(client, service, node) | |
313 except Exception as e: | |
314 log.warning(f"Can't retrive node schema as {node!r} [{service}]: {e}") | |
315 else: | |
316 try: | |
317 field_type = schema[NS_TICKETS_TYPE] | |
318 except KeyError: | |
319 log.debug("no type found in list schema") | |
320 else: | |
321 list_elt = extra["element"] = domish.Element((APP_NS_TICKETS, "list")) | |
322 list_elt["type"] = field_type | |
323 | |
324 def _set(self, service, node, values, schema=None, item_id=None, extra_s='', | |
325 profile_key=C.PROF_KEY_NONE): | |
326 client, service, node, schema, item_id, extra = self._s.prepare_bridge_set( | |
327 service, node, schema, item_id, extra_s, profile_key | |
328 ) | |
329 d = defer.ensureDeferred(self.set( | |
330 client, service, node, values, schema, item_id, extra, deserialise=True | |
331 )) | |
332 d.addCallback(lambda ret: ret or "") | |
333 return d | |
334 | |
335 async def set( | |
336 self, client, service, node, values, schema=None, item_id=None, extra=None, | |
337 deserialise=False, form_ns=APP_NS_TICKETS | |
338 ): | |
339 """Publish a tickets | |
340 | |
341 @param node(unicode, None): Pubsub node to use | |
342 None to use default tickets node | |
343 @param values(dict[key(unicode), [iterable[object]|object]]): values of the ticket | |
344 | |
345 if value is not iterable, it will be put in a list | |
346 'created' and 'updated' will be forced to current time: | |
347 - 'created' is set if item_id is None, i.e. if it's a new ticket | |
348 - 'updated' is set everytime | |
349 @param extra(dict, None): same as for [XEP-0060.send_item] with additional keys: | |
350 - update(bool): if True, get previous item data to merge with current one | |
351 if True, item_id must be set | |
352 other arguments are same as for [self._s.send_data_form_item] | |
353 @return (unicode): id of the created item | |
354 """ | |
355 if not node: | |
356 node = self.namespace | |
357 | |
358 if not item_id: | |
359 comments_service = await self._m.get_comments_service(client, service) | |
360 | |
361 # we need to use uuid for comments node, because we don't know item id in | |
362 # advance (we don't want to set it ourselves to let the server choose, so we | |
363 # can have a nicer id if serial ids is activated) | |
364 comments_node = self._m.get_comments_node( | |
365 node + "_" + str(shortuuid.uuid()) | |
366 ) | |
367 options = { | |
368 self._p.OPT_ACCESS_MODEL: self._p.ACCESS_OPEN, | |
369 self._p.OPT_PERSIST_ITEMS: 1, | |
370 self._p.OPT_DELIVER_PAYLOADS: 1, | |
371 self._p.OPT_SEND_ITEM_SUBSCRIBE: 1, | |
372 self._p.OPT_PUBLISH_MODEL: self._p.ACCESS_OPEN, | |
373 } | |
374 await self._p.createNode(client, comments_service, comments_node, options) | |
375 values["comments_uri"] = uri.build_xmpp_uri( | |
376 "pubsub", | |
377 subtype="microblog", | |
378 path=comments_service.full(), | |
379 node=comments_node, | |
380 ) | |
381 | |
382 return await self._s.set( | |
383 client, service, node, values, schema, item_id, extra, deserialise, form_ns | |
384 ) | |
385 | |
386 def _delete( | |
387 self, service_s, nodeIdentifier, itemIdentifier, notify, profile_key | |
388 ): | |
389 client = self.host.get_client(profile_key) | |
390 return defer.ensureDeferred(self.delete( | |
391 client, | |
392 jid.JID(service_s) if service_s else None, | |
393 nodeIdentifier, | |
394 itemIdentifier, | |
395 notify | |
396 )) | |
397 | |
398 async def delete( | |
399 self, | |
400 client: SatXMPPEntity, | |
401 service: Optional[jid.JID], | |
402 node: Optional[str], | |
403 itemIdentifier: str, | |
404 notify: Optional[bool] = None | |
405 ) -> None: | |
406 if not node: | |
407 node = self.namespace | |
408 return await self._p.retract_items( | |
409 service, node, (itemIdentifier,), notify, client.profile | |
410 ) | |
411 | |
412 def _lists_list(self, service, node, profile): | |
413 service = jid.JID(service) if service else None | |
414 node = node or None | |
415 client = self.host.get_client(profile) | |
416 d = defer.ensureDeferred(self.lists_list(client, service, node)) | |
417 d.addCallback(data_format.serialise) | |
418 return d | |
419 | |
420 async def lists_list( | |
421 self, client, service: Optional[jid.JID], node: Optional[str]=None | |
422 ) -> List[dict]: | |
423 """Retrieve list of pubsub lists registered in personal interests | |
424 | |
425 @return list: list of lists metadata | |
426 """ | |
427 items, metadata = await self.host.plugins['LIST_INTEREST'].list_interests( | |
428 client, service, node, namespace=APP_NS_TICKETS) | |
429 lists = [] | |
430 for item in items: | |
431 interest_elt = item.interest | |
432 if interest_elt is None: | |
433 log.warning(f"invalid interest for {client.profile}: {item.toXml}") | |
434 continue | |
435 if interest_elt.getAttribute("namespace") != APP_NS_TICKETS: | |
436 continue | |
437 pubsub_elt = interest_elt.pubsub | |
438 list_data = { | |
439 "id": item["id"], | |
440 "name": interest_elt["name"], | |
441 "service": pubsub_elt["service"], | |
442 "node": pubsub_elt["node"], | |
443 "creator": C.bool(pubsub_elt.getAttribute("creator", C.BOOL_FALSE)), | |
444 } | |
445 try: | |
446 list_elt = next(pubsub_elt.elements(APP_NS_TICKETS, "list")) | |
447 except StopIteration: | |
448 pass | |
449 else: | |
450 list_type = list_data["type"] = list_elt["type"] | |
451 if list_type in TEMPLATES: | |
452 list_data["icon_name"] = TEMPLATES[list_type]["icon"] | |
453 lists.append(list_data) | |
454 | |
455 return lists | |
456 | |
457 def _get_templates_names(self, language, profile): | |
458 client = self.host.get_client(profile) | |
459 return data_format.serialise(self.get_templates_names(client, language)) | |
460 | |
461 def get_templates_names(self, client, language: str) -> list: | |
462 """Retrieve well known list templates""" | |
463 | |
464 templates = [{"id": tpl_id, "name": d["name"], "icon": d["icon"]} | |
465 for tpl_id, d in TEMPLATES.items()] | |
466 return templates | |
467 | |
468 def _get_template(self, name, language, profile): | |
469 client = self.host.get_client(profile) | |
470 return data_format.serialise(self.get_template(client, name, language)) | |
471 | |
472 def get_template(self, client, name: str, language: str) -> dict: | |
473 """Retrieve a well known template""" | |
474 return TEMPLATES[name] | |
475 | |
476 def _create_template(self, template_id, name, access_model, profile): | |
477 client = self.host.get_client(profile) | |
478 d = defer.ensureDeferred(self.create_template( | |
479 client, template_id, name, access_model | |
480 )) | |
481 d.addCallback(lambda node_data: (node_data[0].full(), node_data[1])) | |
482 return d | |
483 | |
484 async def create_template( | |
485 self, client, template_id: str, name: str, access_model: str | |
486 ) -> Tuple[jid.JID, str]: | |
487 """Create a list from a template""" | |
488 name = name.strip() | |
489 if not name: | |
490 name = shortuuid.uuid() | |
491 fields = TEMPLATES[template_id]["fields"].copy() | |
492 fields.insert( | |
493 0, | |
494 {"type": "hidden", "name": NS_TICKETS_TYPE, "value": template_id} | |
495 ) | |
496 schema = xml_tools.data_dict_2_data_form( | |
497 {"namespace": APP_NS_TICKETS, "fields": fields} | |
498 ).toElement() | |
499 | |
500 service = client.jid.userhostJID() | |
501 node = self._s.get_submitted_ns(f"{APP_NS_TICKETS}_{name}") | |
502 options = { | |
503 self._p.OPT_ACCESS_MODEL: access_model, | |
504 } | |
505 if template_id == "grocery": | |
506 # for grocery list, we want all publishers to be able to set all items | |
507 # XXX: should node options be in TEMPLATE? | |
508 options[self._p.OPT_OVERWRITE_POLICY] = self._p.OWPOL_ANY_PUB | |
509 await self._p.createNode(client, service, node, options) | |
510 await self._s.set_schema(client, service, node, schema) | |
511 list_elt = domish.Element((APP_NS_TICKETS, "list")) | |
512 list_elt["type"] = template_id | |
513 try: | |
514 await self.host.plugins['LIST_INTEREST'].register_pubsub( | |
515 client, APP_NS_TICKETS, service, node, creator=True, | |
516 name=name, element=list_elt) | |
517 except Exception as e: | |
518 log.warning(f"Can't add list to interests: {e}") | |
519 return service, node |