comparison libervia/backend/plugins/plugin_xep_0471.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_0471.py@524856bd7b19
children bc898879af34
comparison
equal deleted inserted replaced
4070:d10748475025 4071:4b842c1fb686
1 #!/usr/bin/env python3
2
3
4 # Libervia plugin to handle events
5 # Copyright (C) 2009-2022 Jérôme Poisson (goffi@goffi.org)
6
7 # This program is free software: you can redistribute it and/or modify
8 # it under the terms of the GNU Affero General Public License as published by
9 # the Free Software Foundation, either version 3 of the License, or
10 # (at your option) any later version.
11
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU Affero General Public License for more details.
16
17 # You should have received a copy of the GNU Affero General Public License
18 # along with this program. If not, see <http://www.gnu.org/licenses/>.
19
20 from random import seed
21 from typing import Optional, Final, Dict, List, Union, Any, Optional
22 from attr import attr
23
24 import shortuuid
25 from sqlalchemy.orm.events import event
26 from libervia.backend.core.xmpp import SatXMPPClient
27 from libervia.backend.core.i18n import _
28 from libervia.backend.core import exceptions
29 from libervia.backend.core.constants import Const as C
30 from libervia.backend.core.log import getLogger
31 from libervia.backend.core.xmpp import SatXMPPEntity
32 from libervia.backend.core.core_types import SatXMPPEntity
33 from libervia.backend.tools import utils
34 from libervia.backend.tools import xml_tools
35 from libervia.backend.tools.common import uri as xmpp_uri
36 from libervia.backend.tools.common import date_utils
37 from libervia.backend.tools.common import data_format
38 from twisted.internet import defer
39 from twisted.words.protocols.jabber import jid, error
40 from twisted.words.xish import domish
41 from wokkel import disco, iwokkel
42 from zope.interface import implementer
43 from twisted.words.protocols.jabber.xmlstream import XMPPHandler
44 from wokkel import pubsub, data_form
45
46 log = getLogger(__name__)
47
48
49 PLUGIN_INFO = {
50 C.PI_NAME: "Events",
51 C.PI_IMPORT_NAME: "XEP-0471",
52 C.PI_TYPE: "XEP",
53 C.PI_MODES: C.PLUG_MODE_BOTH,
54 C.PI_PROTOCOLS: [],
55 C.PI_DEPENDENCIES: [
56 "XEP-0060", "XEP-0080", "XEP-0447", "XEP-0470", # "INVITATION", "PUBSUB_INVITATION",
57 # "LIST_INTEREST"
58 ],
59 C.PI_RECOMMENDATIONS: ["XEP-0277", "EMAIL_INVITATION"],
60 C.PI_MAIN: "XEP_0471",
61 C.PI_HANDLER: "yes",
62 C.PI_DESCRIPTION: _("""Calendar Events"""),
63 }
64
65 NS_EVENT = "org.salut-a-toi.event:0"
66 NS_EVENTS: Final = "urn:xmpp:events:0"
67 NS_RSVP: Final = "urn:xmpp:events:rsvp:0"
68 NS_EXTRA: Final = "urn:xmpp:events:extra:0"
69
70
71 class XEP_0471:
72 namespace = NS_EVENTS
73
74 def __init__(self, host):
75 log.info(_("Events plugin initialization"))
76 self.host = host
77 self._p = host.plugins["XEP-0060"]
78 self._g = host.plugins["XEP-0080"]
79 self._b = host.plugins.get("XEP-0277")
80 self._sfs = host.plugins["XEP-0447"]
81 self._a = host.plugins["XEP-0470"]
82 # self._i = host.plugins.get("EMAIL_INVITATION")
83 host.register_namespace("events", NS_EVENTS)
84 self._a.register_attachment_handler("rsvp", NS_EVENTS, self.rsvp_get, self.rsvp_set)
85 # host.plugins["PUBSUB_INVITATION"].register(NS_EVENTS, self)
86 host.bridge.add_method(
87 "events_get",
88 ".plugin",
89 in_sign="ssasss",
90 out_sign="s",
91 method=self._events_get,
92 async_=True,
93 )
94 host.bridge.add_method(
95 "event_create",
96 ".plugin",
97 in_sign="sssss",
98 out_sign="",
99 method=self._event_create,
100 async_=True,
101 )
102 host.bridge.add_method(
103 "event_modify",
104 ".plugin",
105 in_sign="sssss",
106 out_sign="",
107 method=self._event_modify,
108 async_=True,
109 )
110 host.bridge.add_method(
111 "event_invitee_get",
112 ".plugin",
113 in_sign="sssasss",
114 out_sign="s",
115 method=self._event_invitee_get,
116 async_=True,
117 )
118 host.bridge.add_method(
119 "event_invitee_set",
120 ".plugin",
121 in_sign="sssss",
122 out_sign="",
123 method=self._event_invitee_set,
124 async_=True,
125 )
126 host.bridge.add_method(
127 "event_invitees_list",
128 ".plugin",
129 in_sign="sss",
130 out_sign="a{sa{ss}}",
131 method=self._event_invitees_list,
132 async_=True,
133 ),
134 host.bridge.add_method(
135 "event_invite",
136 ".plugin",
137 in_sign="sssss",
138 out_sign="",
139 method=self._invite,
140 async_=True,
141 )
142 host.bridge.add_method(
143 "event_invite_by_email",
144 ".plugin",
145 in_sign="ssssassssssss",
146 out_sign="",
147 method=self._invite_by_email,
148 async_=True,
149 )
150
151 def get_handler(self, client):
152 return EventsHandler(self)
153
154 def _parse_event_elt(self, event_elt):
155 """Helper method to parse event element
156
157 @param (domish.Element): event_elt
158 @return (tuple[int, dict[unicode, unicode]): timestamp, event_data
159 """
160 try:
161 timestamp = date_utils.date_parse(next(event_elt.elements(NS_EVENT, "date")))
162 except StopIteration:
163 timestamp = -1
164
165 data = {}
166
167 for key in ("name",):
168 try:
169 data[key] = event_elt[key]
170 except KeyError:
171 continue
172
173 for elt_name in ("description",):
174 try:
175 elt = next(event_elt.elements(NS_EVENT, elt_name))
176 except StopIteration:
177 continue
178 else:
179 data[elt_name] = str(elt)
180
181 for elt_name in ("image", "background-image"):
182 try:
183 image_elt = next(event_elt.elements(NS_EVENT, elt_name))
184 data[elt_name] = image_elt["src"]
185 except StopIteration:
186 continue
187 except KeyError:
188 log.warning(_("no src found for image"))
189
190 for uri_type in ("invitees", "blog"):
191 try:
192 elt = next(event_elt.elements(NS_EVENT, uri_type))
193 uri = data[uri_type + "_uri"] = elt["uri"]
194 uri_data = xmpp_uri.parse_xmpp_uri(uri)
195 if uri_data["type"] != "pubsub":
196 raise ValueError
197 except StopIteration:
198 log.warning(_("no {uri_type} element found!").format(uri_type=uri_type))
199 except KeyError:
200 log.warning(_("incomplete {uri_type} element").format(uri_type=uri_type))
201 except ValueError:
202 log.warning(_("bad {uri_type} element").format(uri_type=uri_type))
203 else:
204 data[uri_type + "_service"] = uri_data["path"]
205 data[uri_type + "_node"] = uri_data["node"]
206
207 for meta_elt in event_elt.elements(NS_EVENT, "meta"):
208 key = meta_elt["name"]
209 if key in data:
210 log.warning(
211 "Ignoring conflicting meta element: {xml}".format(
212 xml=meta_elt.toXml()
213 )
214 )
215 continue
216 data[key] = str(meta_elt)
217 if event_elt.link:
218 link_elt = event_elt.link
219 data["service"] = link_elt["service"]
220 data["node"] = link_elt["node"]
221 data["item"] = link_elt["item"]
222 if event_elt.getAttribute("creator") == "true":
223 data["creator"] = True
224 return timestamp, data
225
226 def event_elt_2_event_data(self, event_elt: domish.Element) -> Dict[str, Any]:
227 """Convert <event/> element to event data
228
229 @param event_elt: <event/> element
230 parent <item/> element can also be used
231 @raise exceptions.NotFound: can't find event payload
232 @raise ValueError: something is missing or badly formed
233 """
234 if event_elt.name == "item":
235 try:
236 event_elt = next(event_elt.elements(NS_EVENTS, "event"))
237 except StopIteration:
238 raise exceptions.NotFound("<event/> payload is missing")
239
240 event_data: Dict[str, Any] = {}
241
242 # id
243
244 parent_elt = event_elt.parent
245 if parent_elt is not None and parent_elt.hasAttribute("id"):
246 event_data["id"] = parent_elt["id"]
247
248 # name
249
250 name_data: Dict[str, str] = {}
251 event_data["name"] = name_data
252 for name_elt in event_elt.elements(NS_EVENTS, "name"):
253 lang = name_elt.getAttribute("xml:lang", "")
254 if lang in name_data:
255 raise ValueError("<name/> elements don't have distinct xml:lang")
256 name_data[lang] = str(name_elt)
257
258 if not name_data:
259 raise exceptions.NotFound("<name/> element is missing")
260
261 # start
262
263 try:
264 start_elt = next(event_elt.elements(NS_EVENTS, "start"))
265 except StopIteration:
266 raise exceptions.NotFound("<start/> element is missing")
267 event_data["start"] = utils.parse_xmpp_date(str(start_elt))
268
269 # end
270
271 try:
272 end_elt = next(event_elt.elements(NS_EVENTS, "end"))
273 except StopIteration:
274 raise exceptions.NotFound("<end/> element is missing")
275 event_data["end"] = utils.parse_xmpp_date(str(end_elt))
276
277 # head-picture
278
279 head_pic_elt = next(event_elt.elements(NS_EVENTS, "head-picture"), None)
280 if head_pic_elt is not None:
281 event_data["head-picture"] = self._sfs.parse_file_sharing_elt(head_pic_elt)
282
283 # description
284
285 seen_desc = set()
286 for description_elt in event_elt.elements(NS_EVENTS, "description"):
287 lang = description_elt.getAttribute("xml:lang", "")
288 desc_type = description_elt.getAttribute("type", "text")
289 lang_type = (lang, desc_type)
290 if lang_type in seen_desc:
291 raise ValueError(
292 "<description/> elements don't have distinct xml:lang/type"
293 )
294 seen_desc.add(lang_type)
295 descriptions = event_data.setdefault("descriptions", [])
296 description_data = {"description": str(description_elt)}
297 if lang:
298 description_data["language"] = lang
299 if desc_type:
300 description_data["type"] = desc_type
301 descriptions.append(description_data)
302
303 # categories
304
305 for category_elt in event_elt.elements(NS_EVENTS, "category"):
306 try:
307 category_data = {
308 "term": category_elt["term"]
309 }
310 except KeyError:
311 log.warning(
312 "<category/> element is missing mandatory term: "
313 f"{category_elt.toXml()}"
314 )
315 continue
316 wd = category_elt.getAttribute("wd")
317 if wd:
318 category_data["wikidata_id"] = wd
319 lang = category_elt.getAttribute("xml:lang")
320 if lang:
321 category_data["language"] = lang
322 event_data.setdefault("categories", []).append(category_data)
323
324 # locations
325
326 seen_location_ids = set()
327 for location_elt in event_elt.elements(NS_EVENTS, "location"):
328 location_id = location_elt.getAttribute("id", "")
329 if location_id in seen_location_ids:
330 raise ValueError("<location/> elements don't have distinct IDs")
331 seen_location_ids.add(location_id)
332 location_data = self._g.parse_geoloc_elt(location_elt)
333 if location_id:
334 location_data["id"] = location_id
335 lang = location_elt.getAttribute("xml:lang", "")
336 if lang:
337 location_data["language"] = lang
338 event_data.setdefault("locations", []).append(location_data)
339
340 # RSVPs
341
342 seen_rsvp_lang = set()
343 for rsvp_elt in event_elt.elements(NS_EVENTS, "rsvp"):
344 rsvp_lang = rsvp_elt.getAttribute("xml:lang", "")
345 if rsvp_lang in seen_rsvp_lang:
346 raise ValueError("<rsvp/> elements don't have distinct xml:lang")
347 seen_rsvp_lang.add(rsvp_lang)
348 rsvp_form = data_form.findForm(rsvp_elt, NS_RSVP)
349 if rsvp_form is None:
350 log.warning(f"RSVP form is missing: {rsvp_elt.toXml()}")
351 continue
352 rsvp_data = xml_tools.data_form_2_data_dict(rsvp_form)
353 if rsvp_lang:
354 rsvp_data["language"] = rsvp_lang
355 event_data.setdefault("rsvp", []).append(rsvp_data)
356
357 # linked pubsub nodes
358
359 for name in ("invitees", "comments", "blog", "schedule"):
360 elt = next(event_elt.elements(NS_EVENTS, name), None)
361 if elt is not None:
362 try:
363 event_data[name] = {
364 "service": elt["jid"],
365 "node": elt["node"]
366 }
367 except KeyError:
368 log.warning(f"invalid {name} element: {elt.toXml()}")
369
370 # attachments
371
372 attachments_elt = next(event_elt.elements(NS_EVENTS, "attachments"), None)
373 if attachments_elt:
374 attachments = event_data["attachments"] = []
375 for file_sharing_elt in attachments_elt.elements(
376 self._sfs.namespace, "file-sharing"):
377 try:
378 file_sharing_data = self._sfs.parse_file_sharing_elt(file_sharing_elt)
379 except Exception as e:
380 log.warning(f"invalid attachment: {e}\n{file_sharing_elt.toXml()}")
381 continue
382 attachments.append(file_sharing_data)
383
384 # extra
385
386 extra_elt = next(event_elt.elements(NS_EVENTS, "extra"), None)
387 if extra_elt is not None:
388 extra_form = data_form.findForm(extra_elt, NS_EXTRA)
389 if extra_form is None:
390 log.warning(f"extra form is missing: {extra_elt.toXml()}")
391 else:
392 extra_data = event_data["extra"] = {}
393 for name, value in extra_form.items():
394 if name.startswith("accessibility:"):
395 extra_data.setdefault("accessibility", {})[name[14:]] = value
396 elif name == "accessibility":
397 log.warning(
398 'ignoring "accessibility" key which is not standard: '
399 f"{extra_form.toElement().toXml()}"
400 )
401 else:
402 extra_data[name] = value
403
404 # external
405
406 external_elt = next(event_elt.elements(NS_EVENTS, "external"), None)
407 if external_elt:
408 try:
409 event_data["external"] = {
410 "jid": external_elt["jid"],
411 "node": external_elt["node"],
412 "item": external_elt["item"]
413 }
414 except KeyError:
415 log.warning(f"invalid <external/> element: {external_elt.toXml()}")
416
417 return event_data
418
419 def _events_get(
420 self, service: str, node: str, event_ids: List[str], extra: str, profile_key: str
421 ):
422 client = self.host.get_client(profile_key)
423 d = defer.ensureDeferred(
424 self.events_get(
425 client,
426 jid.JID(service) if service else None,
427 node if node else NS_EVENTS,
428 event_ids,
429 data_format.deserialise(extra)
430 )
431 )
432 d.addCallback(data_format.serialise)
433 return d
434
435 async def events_get(
436 self,
437 client: SatXMPPEntity,
438 service: Optional[jid.JID],
439 node: str = NS_EVENTS,
440 events_ids: Optional[List[str]] = None,
441 extra: Optional[dict] = None,
442 ) -> List[Dict[str, Any]]:
443 """Retrieve event data
444
445 @param service: pubsub service
446 @param node: pubsub node
447 @param event_id: pubsub item ID
448 @return: event data:
449 """
450 if service is None:
451 service = client.jid.userhostJID()
452 items, __ = await self._p.get_items(
453 client, service, node, item_ids=events_ids, extra=extra
454 )
455 events = []
456 for item in items:
457 try:
458 events.append(self.event_elt_2_event_data((item)))
459 except (ValueError, exceptions.NotFound):
460 log.warning(
461 f"Can't parse event for item {item['id']}: {item.toXml()}"
462 )
463
464 return events
465
466 def _event_create(
467 self,
468 data_s: str,
469 service: str,
470 node: str,
471 event_id: str = "",
472 profile_key: str = C.PROF_KEY_NONE
473 ):
474 client = self.host.get_client(profile_key)
475 return defer.ensureDeferred(
476 self.event_create(
477 client,
478 data_format.deserialise(data_s),
479 jid.JID(service) if service else None,
480 node or None,
481 event_id or None
482 )
483 )
484
485 def event_data_2_event_elt(self, event_data: Dict[str, Any]) -> domish.Element:
486 """Convert Event Data to corresponding Element
487
488 @param event_data: data of the event with keys as follow:
489 name (dict)
490 map of language to name
491 empty string can be used as key if no language is specified
492 this key is mandatory
493 start (int|float)
494 starting time of the event
495 this key is mandatory
496 end (int|float)
497 ending time of the event
498 this key is mandatory
499 head-picture(dict)
500 file sharing data for the main picture to use to represent the event
501 description(list[dict])
502 list of descriptions. If there are several descriptions, they must have
503 distinct (language, type).
504 Description data is dict which following keys:
505 description(str)
506 the description itself, either in plain text or xhtml
507 this key is mandatory
508 language(str)
509 ISO-639 language code
510 type(str)
511 type of the description, either "text" (default) or "xhtml"
512 categories(list[dict])
513 each category is a dict with following keys:
514 term(str)
515 human readable short text of the category
516 this key is mandatory
517 wikidata_id(str)
518 Entity ID from WikiData
519 language(str)
520 ISO-639 language code
521 locations(list[dict])
522 list of location dict as used in plugin XEP-0080 [get_geoloc_elt].
523 If several locations are used, they must have distinct IDs
524 rsvp(list[dict])
525 RSVP data. The dict is a data dict as used in
526 sat.tools.xml_tools.data_dict_2_data_form with some extra keys.
527 The "attending" key is automatically added if it's not already present,
528 except if the "no_default" key is present. Thus, an empty dict can be used
529 to use default RSVP.
530 If several dict are present in the list, they must have different "lang"
531 keys.
532 Following extra key can be used:
533 language(str)
534 ISO-639 code for language used in the form
535 no_default(bool)
536 if True, the "attending" field won't be automatically added
537 invitees(dict)
538 link to pubsub node holding invitees list.
539 Following keys are mandatory:
540 service(str)
541 pubsub service where the node is
542 node (str)
543 pubsub node to use
544 comments(dict)
545 link to pubsub node holding XEP-0277 comments on the event itself.
546 Following keys are mandatory:
547 service(str)
548 pubsub service where the node is
549 node (str)
550 pubsub node to use
551 blog(dict)
552 link to pubsub node holding a blog about the event.
553 Following keys are mandatory:
554 service(str)
555 pubsub service where the node is
556 node (str)
557 pubsub node to use
558 schedule(dict)
559 link to pubsub node holding an events node describing the schedule of this
560 event.
561 Following keys are mandatory:
562 service(str)
563 pubsub service where the node is
564 node (str)
565 pubsub node to use
566 attachments[list[dict]]
567 list of file sharing data about all kind of attachments of interest for
568 the event.
569 extra(dict)
570 extra information about the event.
571 Keys can be:
572 website(str)
573 main website about the event
574 status(str)
575 status of the event.
576 Can be one of "confirmed", "tentative" or "cancelled"
577 languages(list[str])
578 ISO-639 codes for languages which will be mainly spoken at the
579 event
580 accessibility(dict)
581 accessibility informations.
582 Keys can be:
583 wheelchair
584 tell if the event is accessible to wheelchair.
585 Value can be "full", "partial" or "no"
586 external(dict):
587 if present, this event is a link to an external one.
588 Keys (all mandatory) are:
589 jid: pubsub service
590 node: pubsub node
591 item: event id
592 @return: Event element
593 @raise ValueError: some expected data were missing or incorrect
594 """
595 event_elt = domish.Element((NS_EVENTS, "event"))
596 try:
597 for lang, name in event_data["name"].items():
598 name_elt = event_elt.addElement("name", content=name)
599 if lang:
600 name_elt["xml:lang"] = lang
601 except (KeyError, TypeError):
602 raise ValueError('"name" field is not a dict mapping language to event name')
603 try:
604 event_elt.addElement("start", content=utils.xmpp_date(event_data["start"]))
605 event_elt.addElement("end", content=utils.xmpp_date(event_data["end"]))
606 except (KeyError, TypeError, ValueError):
607 raise ValueError('"start" and "end" fields are mandatory')
608
609 if "head-picture" in event_data:
610 head_pic_data = event_data["head-picture"]
611 head_picture_elt = event_elt.addElement("head-picture")
612 head_picture_elt.addChild(self._sfs.get_file_sharing_elt(**head_pic_data))
613
614 seen_desc = set()
615 if "descriptions" in event_data:
616 for desc_data in event_data["descriptions"]:
617 desc_type = desc_data.get("type", "text")
618 lang = desc_data.get("language") or ""
619 lang_type = (lang, desc_type)
620 if lang_type in seen_desc:
621 raise ValueError(
622 '"xml:lang" and "type" is not unique among descriptions: '
623 f"{desc_data}"
624 )
625 seen_desc.add(lang_type)
626 try:
627 description = desc_data["description"]
628 except KeyError:
629 log.warning(f"description is missing in {desc_data!r}")
630 continue
631
632 if desc_type == "text":
633 description_elt = event_elt.addElement(
634 "description", content=description
635 )
636 elif desc_type == "xhtml":
637 description_elt = event_elt.addElement("description")
638 div_elt = xml_tools.parse(description, namespace=C.NS_XHTML)
639 description_elt.addChild(div_elt)
640 else:
641 log.warning(f"unknown description type {desc_type!r}")
642 continue
643 if lang:
644 description_elt["xml:lang"] = lang
645 for category_data in event_data.get("categories", []):
646 try:
647 category_term = category_data["term"]
648 except KeyError:
649 log.warning(f'"term" is missing categories data: {category_data}')
650 continue
651 category_elt = event_elt.addElement("category")
652 category_elt["term"] = category_term
653 category_wd = category_data.get("wikidata_id")
654 if category_wd:
655 category_elt["wd"] = category_wd
656 category_lang = category_data.get("language")
657 if category_lang:
658 category_elt["xml:lang"] = category_lang
659
660 seen_location_ids = set()
661 for location_data in event_data.get("locations", []):
662 location_id = location_data.get("id", "")
663 if location_id in seen_location_ids:
664 raise ValueError("locations must have distinct IDs")
665 seen_location_ids.add(location_id)
666 location_elt = event_elt.addElement("location")
667 location_elt.addChild(self._g.get_geoloc_elt(location_data))
668 if location_id:
669 location_elt["id"] = location_id
670
671 rsvp_data_list: Optional[List[dict]] = event_data.get("rsvp")
672 if rsvp_data_list is not None:
673 seen_lang = set()
674 for rsvp_data in rsvp_data_list:
675 if not rsvp_data:
676 # we use a minimum data if an empty dict is received. It will be later
677 # filled with defaut "attending" field.
678 rsvp_data = {"fields": []}
679 rsvp_elt = event_elt.addElement("rsvp")
680 lang = rsvp_data.get("language", "")
681 if lang in seen_lang:
682 raise ValueError(
683 "If several RSVP are specified, they must have distinct "
684 f"languages. {lang!r} language has been used several times."
685 )
686 seen_lang.add(lang)
687 if lang:
688 rsvp_elt["xml:lang"] = lang
689 if not rsvp_data.get("no_default", False):
690 try:
691 next(f for f in rsvp_data["fields"] if f["name"] == "attending")
692 except StopIteration:
693 rsvp_data["fields"].append({
694 "type": "list-single",
695 "name": "attending",
696 "label": "Attending",
697 "options": [
698 {"label": "maybe", "value": "maybe"},
699 {"label": "yes", "value": "yes"},
700 {"label": "no", "value": "no"}
701 ],
702 "required": True
703 })
704 rsvp_data["namespace"] = NS_RSVP
705 rsvp_form = xml_tools.data_dict_2_data_form(rsvp_data)
706 rsvp_elt.addChild(rsvp_form.toElement())
707
708 for node_type in ("invitees", "comments", "blog", "schedule"):
709 node_data = event_data.get(node_type)
710 if not node_data:
711 continue
712 try:
713 service, node = node_data["service"], node_data["node"]
714 except KeyError:
715 log.warning(f"invalid node data for {node_type}: {node_data}")
716 else:
717 pub_node_elt = event_elt.addElement(node_type)
718 pub_node_elt["jid"] = service
719 pub_node_elt["node"] = node
720
721 attachments = event_data.get("attachments")
722 if attachments:
723 attachments_elt = event_elt.addElement("attachments")
724 for attachment_data in attachments:
725 attachments_elt.addChild(
726 self._sfs.get_file_sharing_elt(**attachment_data)
727 )
728
729 extra = event_data.get("extra")
730 if extra:
731 extra_form = data_form.Form(
732 "result",
733 formNamespace=NS_EXTRA
734 )
735 for node_type in ("website", "status"):
736 if node_type in extra:
737 extra_form.addField(
738 data_form.Field(var=node_type, value=extra[node_type])
739 )
740 if "languages" in extra:
741 extra_form.addField(
742 data_form.Field(
743 "list-multi", var="languages", values=extra["languages"]
744 )
745 )
746 for node_type, value in extra.get("accessibility", {}).items():
747 extra_form.addField(
748 data_form.Field(var=f"accessibility:{node_type}", value=value)
749 )
750
751 extra_elt = event_elt.addElement("extra")
752 extra_elt.addChild(extra_form.toElement())
753
754 if "external" in event_data:
755 external_data = event_data["external"]
756 external_elt = event_elt.addElement("external")
757 for node_type in ("jid", "node", "item"):
758 try:
759 value = external_data[node_type]
760 except KeyError:
761 raise ValueError(f"Invalid external data: {external_data}")
762 external_elt[node_type] = value
763
764 return event_elt
765
766 async def event_create(
767 self,
768 client: SatXMPPEntity,
769 event_data: Dict[str, Any],
770 service: Optional[jid.JID] = None,
771 node: Optional[str] = None,
772 event_id: Optional[str] = None,
773 ) -> None:
774 """Create or replace an event
775
776 @param event_data: data of the event (cf. [event_data_2_event_elt])
777 @param node: PubSub node of the event
778 None to use default node (default namespace for personal agenda)
779 @param service: PubSub service
780 None to use profile's PEP
781 @param event_id: ID of the item to create.
782 """
783 if not service:
784 service = client.jid.userhostJID()
785 if not node:
786 node = NS_EVENTS
787 if event_id is None:
788 event_id = shortuuid.uuid()
789 event_elt = self.event_data_2_event_elt(event_data)
790
791 item_elt = pubsub.Item(id=event_id, payload=event_elt)
792 options = {self._p.OPT_ACCESS_MODEL: self._p.ACCESS_WHITELIST}
793 await self._p.create_if_new_node(
794 client, service, nodeIdentifier=node, options=options
795 )
796 await self._p.publish(client, service, node, items=[item_elt])
797 if event_data.get("rsvp"):
798 await self._a.create_attachments_node(client, service, node, event_id)
799
800 def _event_modify(
801 self,
802 data_s: str,
803 event_id: str,
804 service: str,
805 node: str,
806 profile_key: str = C.PROF_KEY_NONE
807 ) -> None:
808 client = self.host.get_client(profile_key)
809 defer.ensureDeferred(
810 self.event_modify(
811 client,
812 data_format.deserialise(data_s),
813 event_id,
814 jid.JID(service) if service else None,
815 node or None,
816 )
817 )
818
819 async def event_modify(
820 self,
821 client: SatXMPPEntity,
822 event_data: Dict[str, Any],
823 event_id: str,
824 service: Optional[jid.JID] = None,
825 node: Optional[str] = None,
826 ) -> None:
827 """Update an event
828
829 Similar as create instead that it update existing item instead of
830 creating or replacing it. Params are the same as for [event_create].
831 """
832 if not service:
833 service = client.jid.userhostJID()
834 if not node:
835 node = NS_EVENTS
836 old_event = (await self.events_get(client, service, node, [event_id]))[0]
837 old_event.update(event_data)
838 event_data = old_event
839 await self.event_create(client, event_data, service, node, event_id)
840
841 def rsvp_get(
842 self,
843 client: SatXMPPEntity,
844 attachments_elt: domish.Element,
845 data: Dict[str, Any],
846 ) -> None:
847 """Get RSVP answers from attachments"""
848 try:
849 rsvp_elt = next(
850 attachments_elt.elements(NS_EVENTS, "rsvp")
851 )
852 except StopIteration:
853 pass
854 else:
855 rsvp_form = data_form.findForm(rsvp_elt, NS_RSVP)
856 if rsvp_form is not None:
857 data["rsvp"] = rsvp_data = dict(rsvp_form)
858 self._a.set_timestamp(rsvp_elt, rsvp_data)
859
860 def rsvp_set(
861 self,
862 client: SatXMPPEntity,
863 data: Dict[str, Any],
864 former_elt: Optional[domish.Element]
865 ) -> Optional[domish.Element]:
866 """update the <reaction> attachment"""
867 rsvp_data = data["extra"].get("rsvp")
868 if rsvp_data is None:
869 return former_elt
870 elif rsvp_data:
871 rsvp_elt = domish.Element(
872 (NS_EVENTS, "rsvp"),
873 attribs = {
874 "timestamp": utils.xmpp_date()
875 }
876 )
877 rsvp_form = data_form.Form("submit", formNamespace=NS_RSVP)
878 rsvp_form.makeFields(rsvp_data)
879 rsvp_elt.addChild(rsvp_form.toElement())
880 return rsvp_elt
881 else:
882 return None
883
884 def _event_invitee_get(
885 self,
886 service: str,
887 node: str,
888 item: str,
889 invitees_s: List[str],
890 extra: str,
891 profile_key: str
892 ) -> defer.Deferred:
893 client = self.host.get_client(profile_key)
894 if invitees_s:
895 invitees = [jid.JID(i) for i in invitees_s]
896 else:
897 invitees = None
898 d = defer.ensureDeferred(
899 self.event_invitee_get(
900 client,
901 jid.JID(service) if service else None,
902 node or None,
903 item,
904 invitees,
905 data_format.deserialise(extra)
906 )
907 )
908 d.addCallback(lambda ret: data_format.serialise(ret))
909 return d
910
911 async def event_invitee_get(
912 self,
913 client: SatXMPPEntity,
914 service: Optional[jid.JID],
915 node: Optional[str],
916 item: str,
917 invitees: Optional[List[jid.JID]] = None,
918 extra: Optional[Dict[str, Any]] = None,
919 ) -> Dict[str, Dict[str, Any]]:
920 """Retrieve attendance from event node
921
922 @param service: PubSub service
923 @param node: PubSub node of the event
924 @param item: PubSub item of the event
925 @param invitees: if set, only retrieve RSVPs from those guests
926 @param extra: extra data used to retrieve items as for [get_attachments]
927 @return: mapping of invitee bare JID to their RSVP
928 an empty dict is returned if nothing has been answered yed
929 """
930 if service is None:
931 service = client.jid.userhostJID()
932 if node is None:
933 node = NS_EVENTS
934 attachments, metadata = await self._a.get_attachments(
935 client, service, node, item, invitees, extra
936 )
937 ret = {}
938 for attachment in attachments:
939 try:
940 rsvp = attachment["rsvp"]
941 except KeyError:
942 continue
943 ret[attachment["from"]] = rsvp
944
945 return ret
946
947 def _event_invitee_set(
948 self,
949 service: str,
950 node: str,
951 item: str,
952 rsvp_s: str,
953 profile_key: str
954 ):
955 client = self.host.get_client(profile_key)
956 return defer.ensureDeferred(
957 self.event_invitee_set(
958 client,
959 jid.JID(service) if service else None,
960 node or None,
961 item,
962 data_format.deserialise(rsvp_s)
963 )
964 )
965
966 async def event_invitee_set(
967 self,
968 client: SatXMPPEntity,
969 service: Optional[jid.JID],
970 node: Optional[str],
971 item: str,
972 rsvp: Dict[str, Any],
973 ) -> None:
974 """Set or update attendance data in event node
975
976 @param service: PubSub service
977 @param node: PubSub node of the event
978 @param item: PubSub item of the event
979 @param rsvp: RSVP data (values to submit to the form)
980 """
981 if service is None:
982 service = client.jid.userhostJID()
983 if node is None:
984 node = NS_EVENTS
985 await self._a.set_attachements(client, {
986 "service": service.full(),
987 "node": node,
988 "id": item,
989 "extra": {"rsvp": rsvp}
990 })
991
992 def _event_invitees_list(self, service, node, profile_key):
993 service = jid.JID(service) if service else None
994 node = node if node else NS_EVENT
995 client = self.host.get_client(profile_key)
996 return defer.ensureDeferred(
997 self.event_invitees_list(client, service, node)
998 )
999
1000 async def event_invitees_list(self, client, service, node):
1001 """Retrieve attendance from event node
1002
1003 @param service(unicode, None): PubSub service
1004 @param node(unicode): PubSub node of the event
1005 @return (dict): a dict with current attendance status,
1006 an empty dict is returned if nothing has been answered yed
1007 """
1008 items, metadata = await self._p.get_items(client, service, node)
1009 invitees = {}
1010 for item in items:
1011 try:
1012 event_elt = next(item.elements(NS_EVENT, "invitee"))
1013 except StopIteration:
1014 # no item found, event data are not set yet
1015 log.warning(_(
1016 "no data found for {item_id} (service: {service}, node: {node})"
1017 .format(item_id=item["id"], service=service, node=node)))
1018 else:
1019 data = {}
1020 for key in ("attend", "guests"):
1021 try:
1022 data[key] = event_elt[key]
1023 except KeyError:
1024 continue
1025 invitees[item["id"]] = data
1026 return invitees
1027
1028 async def invite_preflight(
1029 self,
1030 client: SatXMPPEntity,
1031 invitee_jid: jid.JID,
1032 service: jid.JID,
1033 node: str,
1034 item_id: Optional[str] = None,
1035 name: str = '',
1036 extra: Optional[dict] = None,
1037 ) -> None:
1038 if self._b is None:
1039 raise exceptions.FeatureNotFound(
1040 _('"XEP-0277" (blog) plugin is needed for this feature')
1041 )
1042 if item_id is None:
1043 item_id = extra["default_item_id"] = NS_EVENT
1044
1045 __, event_data = await self.events_get(client, service, node, item_id)
1046 log.debug(_("got event data"))
1047 invitees_service = jid.JID(event_data["invitees_service"])
1048 invitees_node = event_data["invitees_node"]
1049 blog_service = jid.JID(event_data["blog_service"])
1050 blog_node = event_data["blog_node"]
1051 await self._p.set_node_affiliations(
1052 client, invitees_service, invitees_node, {invitee_jid: "publisher"}
1053 )
1054 log.debug(
1055 f"affiliation set on invitee node (jid: {invitees_service}, "
1056 f"node: {invitees_node!r})"
1057 )
1058 await self._p.set_node_affiliations(
1059 client, blog_service, blog_node, {invitee_jid: "member"}
1060 )
1061 blog_items, __ = await self._b.mb_get(client, blog_service, blog_node, None)
1062
1063 for item in blog_items:
1064 try:
1065 comments_service = jid.JID(item["comments_service"])
1066 comments_node = item["comments_node"]
1067 except KeyError:
1068 log.debug(
1069 "no comment service set for item {item_id}".format(
1070 item_id=item["id"]
1071 )
1072 )
1073 else:
1074 await self._p.set_node_affiliations(
1075 client, comments_service, comments_node, {invitee_jid: "publisher"}
1076 )
1077 log.debug(_("affiliation set on blog and comments nodes"))
1078
1079 def _invite(self, invitee_jid, service, node, item_id, profile):
1080 return self.host.plugins["PUBSUB_INVITATION"]._send_pubsub_invitation(
1081 invitee_jid, service, node, item_id or NS_EVENT, profile_key=profile
1082 )
1083
1084 def _invite_by_email(self, service, node, id_=NS_EVENT, email="", emails_extra=None,
1085 name="", host_name="", language="", url_template="",
1086 message_subject="", message_body="",
1087 profile_key=C.PROF_KEY_NONE):
1088 client = self.host.get_client(profile_key)
1089 kwargs = {
1090 "profile": client.profile,
1091 "emails_extra": [str(e) for e in emails_extra],
1092 }
1093 for key in (
1094 "email",
1095 "name",
1096 "host_name",
1097 "language",
1098 "url_template",
1099 "message_subject",
1100 "message_body",
1101 ):
1102 value = locals()[key]
1103 kwargs[key] = str(value)
1104 return defer.ensureDeferred(self.invite_by_email(
1105 client, jid.JID(service) if service else None, node, id_ or NS_EVENT, **kwargs
1106 ))
1107
1108 async def invite_by_email(self, client, service, node, id_=NS_EVENT, **kwargs):
1109 """High level method to create an email invitation to an event
1110
1111 @param service(unicode, None): PubSub service
1112 @param node(unicode): PubSub node of the event
1113 @param id_(unicode): id_ with even data
1114 """
1115 if self._i is None:
1116 raise exceptions.FeatureNotFound(
1117 _('"Invitations" plugin is needed for this feature')
1118 )
1119 if self._b is None:
1120 raise exceptions.FeatureNotFound(
1121 _('"XEP-0277" (blog) plugin is needed for this feature')
1122 )
1123 service = service or client.jid.userhostJID()
1124 event_uri = xmpp_uri.build_xmpp_uri(
1125 "pubsub", path=service.full(), node=node, item=id_
1126 )
1127 kwargs["extra"] = {"event_uri": event_uri}
1128 invitation_data = await self._i.create(**kwargs)
1129 invitee_jid = invitation_data["jid"]
1130 log.debug(_("invitation created"))
1131 # now that we have a jid, we can send normal invitation
1132 await self.invite(client, invitee_jid, service, node, id_)
1133
1134 def on_invitation_preflight(
1135 self,
1136 client: SatXMPPEntity,
1137 name: str,
1138 extra: dict,
1139 service: jid.JID,
1140 node: str,
1141 item_id: Optional[str],
1142 item_elt: domish.Element
1143 ) -> None:
1144 event_elt = item_elt.event
1145 link_elt = event_elt.addElement("link")
1146 link_elt["service"] = service.full()
1147 link_elt["node"] = node
1148 link_elt["item"] = item_id
1149 __, event_data = self._parse_event_elt(event_elt)
1150 try:
1151 name = event_data["name"]
1152 except KeyError:
1153 pass
1154 else:
1155 extra["name"] = name
1156 if 'image' in event_data:
1157 extra["thumb_url"] = event_data['image']
1158 extra["element"] = event_elt
1159
1160
1161 @implementer(iwokkel.IDisco)
1162 class EventsHandler(XMPPHandler):
1163
1164 def __init__(self, plugin_parent):
1165 self.plugin_parent = plugin_parent
1166
1167 def getDiscoInfo(self, requestor, target, nodeIdentifier=""):
1168 return [
1169 disco.DiscoFeature(NS_EVENTS),
1170 ]
1171
1172 def getDiscoItems(self, requestor, target, nodeIdentifier=""):
1173 return []