Mercurial > libervia-backend
comparison sat/plugins/plugin_exp_events.py @ 3902:32b38dd3ac18
plugin events: update following `Events` protoXEP submission:
update the plugin to follow the specification proposed at
https://github.com/xsf/xeps/pull/1206
Events are internally converted to a dict following a format described in
`event_data_2_event_elt` docstring.
RSVP mechanism now uses Pubsub Attachments (XEP-0470), with a user extensible data form.
Bridge methods signatures have changed.
rel 372
author | Goffi <goffi@goffi.org> |
---|---|
date | Wed, 21 Sep 2022 22:41:49 +0200 |
parents | 09f5ac48ffe3 |
children | 287938675461 |
comparison
equal
deleted
inserted
replaced
3901:43024e50b701 | 3902:32b38dd3ac18 |
---|---|
1 #!/usr/bin/env python3 | 1 #!/usr/bin/env python3 |
2 | 2 |
3 | 3 |
4 # SAT plugin to detect language (experimental) | 4 # Libervia plugin to handle events |
5 # Copyright (C) 2009-2021 Jérôme Poisson (goffi@goffi.org) | 5 # Copyright (C) 2009-2022 Jérôme Poisson (goffi@goffi.org) |
6 | 6 |
7 # This program is free software: you can redistribute it and/or modify | 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 | 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 | 9 # the Free Software Foundation, either version 3 of the License, or |
10 # (at your option) any later version. | 10 # (at your option) any later version. |
15 # GNU Affero General Public License for more details. | 15 # GNU Affero General Public License for more details. |
16 | 16 |
17 # You should have received a copy of the GNU Affero General Public License | 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/>. | 18 # along with this program. If not, see <http://www.gnu.org/licenses/>. |
19 | 19 |
20 from typing import Optional | 20 from random import seed |
21 from typing import Optional, Final, Dict, List, Union, Any, Optional | |
22 from attr import attr | |
23 | |
21 import shortuuid | 24 import shortuuid |
25 from sqlalchemy.orm.events import event | |
26 from build.lib.sat.core.xmpp import SatXMPPClient | |
22 from sat.core.i18n import _ | 27 from sat.core.i18n import _ |
23 from sat.core import exceptions | 28 from sat.core import exceptions |
24 from sat.core.constants import Const as C | 29 from sat.core.constants import Const as C |
25 from sat.core.log import getLogger | 30 from sat.core.log import getLogger |
26 from sat.core.xmpp import SatXMPPEntity | 31 from sat.core.xmpp import SatXMPPEntity |
32 from sat.core.core_types import SatXMPPEntity | |
27 from sat.tools import utils | 33 from sat.tools import utils |
34 from sat.tools import xml_tools | |
28 from sat.tools.common import uri as xmpp_uri | 35 from sat.tools.common import uri as xmpp_uri |
29 from sat.tools.common import date_utils | 36 from sat.tools.common import date_utils |
37 from sat.tools.common import data_format | |
30 from twisted.internet import defer | 38 from twisted.internet import defer |
31 from twisted.words.protocols.jabber import jid, error | 39 from twisted.words.protocols.jabber import jid, error |
32 from twisted.words.xish import domish | 40 from twisted.words.xish import domish |
33 from wokkel import disco, iwokkel | 41 from wokkel import disco, iwokkel |
34 from zope.interface import implementer | 42 from zope.interface import implementer |
35 from twisted.words.protocols.jabber.xmlstream import XMPPHandler | 43 from twisted.words.protocols.jabber.xmlstream import XMPPHandler |
36 from wokkel import pubsub | 44 from wokkel import pubsub, data_form |
37 | 45 |
38 log = getLogger(__name__) | 46 log = getLogger(__name__) |
39 | 47 |
40 | 48 |
41 PLUGIN_INFO = { | 49 PLUGIN_INFO = { |
42 C.PI_NAME: "Events", | 50 C.PI_NAME: "Events", |
43 C.PI_IMPORT_NAME: "EVENTS", | 51 C.PI_IMPORT_NAME: "EVENTS", |
44 C.PI_TYPE: "EXP", | 52 C.PI_TYPE: "EXP", |
53 C.PI_MODES: C.PLUG_MODE_BOTH, | |
45 C.PI_PROTOCOLS: [], | 54 C.PI_PROTOCOLS: [], |
46 C.PI_DEPENDENCIES: ["XEP-0060", "INVITATION", "PUBSUB_INVITATION", "LIST_INTEREST"], | 55 C.PI_DEPENDENCIES: [ |
56 "XEP-0060", "XEP-0080", "XEP-0447", "XEP-0470", # "INVITATION", "PUBSUB_INVITATION", | |
57 # "LIST_INTEREST" | |
58 ], | |
47 C.PI_RECOMMENDATIONS: ["XEP-0277", "EMAIL_INVITATION"], | 59 C.PI_RECOMMENDATIONS: ["XEP-0277", "EMAIL_INVITATION"], |
48 C.PI_MAIN: "Events", | 60 C.PI_MAIN: "Events", |
49 C.PI_HANDLER: "yes", | 61 C.PI_HANDLER: "yes", |
50 C.PI_DESCRIPTION: _("""Experimental implementation of XMPP events management"""), | 62 C.PI_DESCRIPTION: _("""XMPP Events Management"""), |
51 } | 63 } |
52 | 64 |
53 NS_EVENT = "org.salut-a-toi.event:0" | 65 NS_EVENT = "org.salut-a-toi.event:0" |
54 | 66 NS_EVENTS: Final = "urn:xmpp:events:0" |
55 | 67 NS_RSVP: Final = "urn:xmpp:events:rsvp:0" |
56 class Events(object): | 68 NS_EXTRA: Final = "urn:xmpp:events:extra:0" |
57 """Q&D module to handle event attendance answer, experimentation only""" | 69 |
70 | |
71 class Events: | |
72 namespace = NS_EVENTS | |
58 | 73 |
59 def __init__(self, host): | 74 def __init__(self, host): |
60 log.info(_("Event plugin initialization")) | 75 log.info(_("Events plugin initialization")) |
61 self.host = host | 76 self.host = host |
62 self._p = self.host.plugins["XEP-0060"] | 77 self._p = host.plugins["XEP-0060"] |
63 self._i = self.host.plugins.get("EMAIL_INVITATION") | 78 self._g = host.plugins["XEP-0080"] |
64 self._b = self.host.plugins.get("XEP-0277") | 79 self._b = host.plugins.get("XEP-0277") |
65 self.host.registerNamespace("event", NS_EVENT) | 80 self._sfs = host.plugins["XEP-0447"] |
66 self.host.plugins["PUBSUB_INVITATION"].register(NS_EVENT, self) | 81 self._a = host.plugins["XEP-0470"] |
82 # self._i = host.plugins.get("EMAIL_INVITATION") | |
83 host.registerNamespace("events", NS_EVENTS) | |
84 self._a.registerAttachmentHandler("rsvp", NS_EVENTS, self.rsvp_get, self.rsvp_set) | |
85 # host.plugins["PUBSUB_INVITATION"].register(NS_EVENTS, self) | |
67 host.bridge.addMethod( | 86 host.bridge.addMethod( |
68 "eventGet", | 87 "eventsGet", |
69 ".plugin", | 88 ".plugin", |
70 in_sign="ssss", | 89 in_sign="ssasss", |
71 out_sign="(ia{ss})", | 90 out_sign="s", |
72 method=self._eventGet, | 91 method=self._events_get, |
73 async_=True, | 92 async_=True, |
74 ) | 93 ) |
75 host.bridge.addMethod( | 94 host.bridge.addMethod( |
76 "eventCreate", | 95 "eventCreate", |
77 ".plugin", | 96 ".plugin", |
78 in_sign="ia{ss}ssss", | 97 in_sign="sssss", |
79 out_sign="s", | 98 out_sign="", |
80 method=self._eventCreate, | 99 method=self._event_create, |
81 async_=True, | 100 async_=True, |
82 ) | 101 ) |
83 host.bridge.addMethod( | 102 host.bridge.addMethod( |
84 "eventModify", | 103 "eventModify", |
85 ".plugin", | 104 ".plugin", |
86 in_sign="sssia{ss}s", | 105 in_sign="sssss", |
87 out_sign="", | 106 out_sign="", |
88 method=self._eventModify, | 107 method=self._event_modify, |
89 async_=True, | |
90 ) | |
91 host.bridge.addMethod( | |
92 "eventsList", | |
93 ".plugin", | |
94 in_sign="sss", | |
95 out_sign="aa{ss}", | |
96 method=self._eventsList, | |
97 async_=True, | 108 async_=True, |
98 ) | 109 ) |
99 host.bridge.addMethod( | 110 host.bridge.addMethod( |
100 "eventInviteeGet", | 111 "eventInviteeGet", |
101 ".plugin", | 112 ".plugin", |
102 in_sign="ssss", | 113 in_sign="sssasss", |
103 out_sign="a{ss}", | 114 out_sign="s", |
104 method=self._eventInviteeGet, | 115 method=self._event_invitee_get, |
105 async_=True, | 116 async_=True, |
106 ) | 117 ) |
107 host.bridge.addMethod( | 118 host.bridge.addMethod( |
108 "eventInviteeSet", | 119 "eventInviteeSet", |
109 ".plugin", | 120 ".plugin", |
110 in_sign="ssa{ss}s", | 121 in_sign="sssss", |
111 out_sign="", | 122 out_sign="", |
112 method=self._eventInviteeSet, | 123 method=self._event_invitee_set, |
113 async_=True, | 124 async_=True, |
114 ) | 125 ) |
115 host.bridge.addMethod( | 126 host.bridge.addMethod( |
116 "eventInviteesList", | 127 "eventInviteesList", |
117 ".plugin", | 128 ".plugin", |
210 data["item"] = link_elt["item"] | 221 data["item"] = link_elt["item"] |
211 if event_elt.getAttribute("creator") == "true": | 222 if event_elt.getAttribute("creator") == "true": |
212 data["creator"] = True | 223 data["creator"] = True |
213 return timestamp, data | 224 return timestamp, data |
214 | 225 |
215 async def getEventElement(self, client, service, node, id_): | 226 def event_elt_2_event_data(self, event_elt: domish.Element) -> Dict[str, Any]: |
216 """Retrieve event element | 227 """Convert <event/> element to event data |
217 | 228 |
218 @param service(jid.JID): pubsub service | 229 @param event_elt: <event/> element |
219 @param node(unicode): pubsub node | 230 parent <item/> element can also be used |
220 @param id_(unicode, None): event id | 231 @raise exceptions.NotFound: can't find event payload |
221 @return (domish.Element): event element | 232 @raise ValueError: something is missing or badly formed |
222 @raise exceptions.NotFound: no event element found | |
223 """ | 233 """ |
224 if not id_: | 234 if event_elt.name == "item": |
225 id_ = NS_EVENT | 235 try: |
226 items, metadata = await self._p.getItems(client, service, node, item_ids=[id_]) | 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 | |
227 try: | 263 try: |
228 event_elt = next(items[0].elements(NS_EVENT, "event")) | 264 start_elt = next(event_elt.elements(NS_EVENTS, "start")) |
229 except StopIteration: | 265 except StopIteration: |
230 raise exceptions.NotFound(_("No event element has been found")) | 266 raise exceptions.NotFound("<start/> element is missing") |
231 except IndexError: | 267 event_data["start"] = utils.parse_xmpp_date(str(start_elt)) |
232 raise exceptions.NotFound(_("No event with this id has been found")) | 268 |
233 return event_elt | 269 # end |
234 | 270 |
235 def _eventGet(self, service, node, id_="", profile_key=C.PROF_KEY_NONE): | 271 try: |
236 service = jid.JID(service) if service else None | 272 end_elt = next(event_elt.elements(NS_EVENTS, "end")) |
237 node = node if node else NS_EVENT | 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.dataForm2dataDict(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.getClient(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.getItems( | |
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 ): | |
238 client = self.host.getClient(profile_key) | 474 client = self.host.getClient(profile_key) |
239 return defer.ensureDeferred( | 475 return defer.ensureDeferred( |
240 self.eventGet(client, service, node, id_) | 476 self.event_create( |
241 ) | 477 client, |
242 | 478 data_format.deserialise(data_s), |
243 async def eventGet(self, client, service, node, id_=NS_EVENT): | 479 jid.JID(service) if service else None, |
244 """Retrieve event data | 480 node or None, |
245 | 481 event_id or None |
246 @param service(unicode, None): PubSub service | 482 ) |
247 @param node(unicode): PubSub node of the event | 483 ) |
248 @param id_(unicode): id_ with even data | 484 |
249 @return (tuple[int, dict[unicode, unicode]): event data: | 485 def event_data_2_event_elt(self, event_data: Dict[str, Any]) -> domish.Element: |
250 - timestamp of the event | 486 """Convert Event Data to corresponding Element |
251 - event metadata where key can be: | 487 |
252 location: location of the event | 488 @param event_data: data of the event with keys as follow: |
253 image: URL of a picture to use to represent event | 489 name (dict) |
254 background-image: URL of a picture to use in background | 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.dataDict2dataForm 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 | |
255 """ | 594 """ |
256 event_elt = await self.getEventElement(client, service, node, id_) | 595 event_elt = domish.Element((NS_EVENTS, "event")) |
257 | 596 try: |
258 return self._parseEventElt(event_elt) | 597 for lang, name in event_data["name"].items(): |
259 | 598 name_elt = event_elt.addElement("name", content=name) |
260 def _eventCreate( | 599 if lang: |
261 self, timestamp, data, service, node, id_="", profile_key=C.PROF_KEY_NONE | 600 name_elt["xml:lang"] = lang |
262 ): | 601 except (KeyError, TypeError): |
263 service = jid.JID(service) if service else None | 602 raise ValueError('"name" field is not a dict mapping language to event name') |
264 node = node or None | 603 try: |
265 client = self.host.getClient(profile_key) | 604 event_elt.addElement("start", content=utils.xmpp_date(event_data["start"])) |
266 data["register"] = C.bool(data.get("register", C.BOOL_FALSE)) | 605 event_elt.addElement("end", content=utils.xmpp_date(event_data["end"])) |
267 return defer.ensureDeferred( | 606 except (KeyError, TypeError, ValueError): |
268 self.eventCreate(client, timestamp, data, service, node, id_ or NS_EVENT) | 607 raise ValueError('"start" and "end" fields are mandatory') |
269 ) | 608 |
270 | 609 if "head-picture" in event_data: |
271 async def eventCreate(self, client, timestamp, data, service, node=None, event_id=NS_EVENT): | 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.dataDict2dataForm(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: | |
272 """Create or replace an event | 774 """Create or replace an event |
273 | 775 |
274 @param service(jid.JID, None): PubSub service | 776 @param event_data: data of the event (cf. [event_data_2_event_elt]) |
275 @param node(unicode, None): PubSub node of the event | 777 @param node: PubSub node of the event |
276 None will create instant node. | 778 None to use default node (default namespace for personal agenda) |
277 @param event_id(unicode): ID of the item to create. | 779 @param service: PubSub service |
278 @param timestamp(timestamp, None) | 780 None to use profile's PEP |
279 @param data(dict[unicode, unicode]): data to update | 781 @param event_id: ID of the item to create. |
280 dict will be cleared, do a copy if data are still needed | |
281 key can be: | |
282 - name: name of the event | |
283 - description: details | |
284 - image: main picture of the event | |
285 - background-image: image to use as background | |
286 - register: bool, True if we want to register the event in our local list | |
287 @return (unicode): created node | |
288 """ | 782 """ |
289 if not event_id: | |
290 raise ValueError(_("event_id must be set")) | |
291 if not service: | 783 if not service: |
292 service = client.jid.userhostJID() | 784 service = client.jid.userhostJID() |
293 if not node: | 785 if not node: |
294 node = NS_EVENT + "__" + shortuuid.uuid() | 786 node = NS_EVENTS |
295 event_elt = domish.Element((NS_EVENT, "event")) | 787 if event_id is None: |
296 if timestamp is not None and timestamp != -1: | 788 event_id = shortuuid.uuid() |
297 formatted_date = utils.xmpp_date(timestamp) | 789 event_elt = self.event_data_2_event_elt(event_data) |
298 event_elt.addElement((NS_EVENT, "date"), content=formatted_date) | 790 |
299 register = data.pop("register", False) | 791 item_elt = pubsub.Item(id=event_id, payload=event_elt) |
300 for key in ("name",): | 792 options = {self._p.OPT_ACCESS_MODEL: self._p.ACCESS_WHITELIST} |
301 if key in data: | 793 await self._p.createIfNewNode( |
302 event_elt[key] = data.pop(key) | 794 client, service, nodeIdentifier=node, options=options |
303 for key in ("description",): | 795 ) |
304 if key in data: | 796 await self._p.publish(client, service, node, items=[item_elt]) |
305 event_elt.addElement((NS_EVENT, key), content=data.pop(key)) | 797 if event_data.get("rsvp"): |
306 for key in ("image", "background-image"): | 798 await self._a.create_attachments_node(client, service, node, event_id) |
307 if key in data: | 799 |
308 elt = event_elt.addElement((NS_EVENT, key)) | 800 def _event_modify( |
309 elt["src"] = data.pop(key) | 801 self, |
310 | 802 data_s: str, |
311 # we first create the invitees and blog nodes (if not specified in data) | 803 event_id: str, |
312 for uri_type in ("invitees", "blog"): | 804 service: str, |
313 key = uri_type + "_uri" | 805 node: str, |
314 for to_delete in ("service", "node"): | 806 profile_key: str = C.PROF_KEY_NONE |
315 k = uri_type + "_" + to_delete | 807 ) -> None: |
316 if k in data: | 808 client = self.host.getClient(profile_key) |
317 del data[k] | 809 defer.ensureDeferred( |
318 if key not in data: | 810 self.event_modify( |
319 # FIXME: affiliate invitees | 811 client, |
320 uri_node = await self._p.createNode(client, service) | 812 data_format.deserialise(data_s), |
321 await self._p.setConfiguration( | 813 event_id, |
322 client, | 814 jid.JID(service) if service else None, |
323 service, | 815 node or None, |
324 uri_node, | |
325 {self._p.OPT_ACCESS_MODEL: self._p.ACCESS_WHITELIST}, | |
326 ) | |
327 uri_service = service | |
328 else: | |
329 uri = data.pop(key) | |
330 uri_data = xmpp_uri.parseXMPPUri(uri) | |
331 if uri_data["type"] != "pubsub": | |
332 raise ValueError( | |
333 _("The given URI is not valid: {uri}").format(uri=uri) | |
334 ) | |
335 uri_service = jid.JID(uri_data["path"]) | |
336 uri_node = uri_data["node"] | |
337 | |
338 elt = event_elt.addElement((NS_EVENT, uri_type)) | |
339 elt["uri"] = xmpp_uri.buildXMPPUri( | |
340 "pubsub", path=uri_service.full(), node=uri_node | |
341 ) | 816 ) |
342 | 817 ) |
343 # remaining data are put in <meta> elements | 818 |
344 for key in list(data.keys()): | 819 async def event_modify( |
345 elt = event_elt.addElement((NS_EVENT, "meta"), content=data.pop(key)) | 820 self, |
346 elt["name"] = key | 821 client: SatXMPPEntity, |
347 | 822 event_data: Dict[str, Any], |
348 item_elt = pubsub.Item(id=event_id, payload=event_elt) | 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""" | |
349 try: | 848 try: |
350 # TODO: check auto-create, no need to create node first if available | 849 rsvp_elt = next( |
351 node = await self._p.createNode(client, service, nodeIdentifier=node) | 850 attachments_elt.elements(NS_EVENTS, "rsvp") |
352 except error.StanzaError as e: | |
353 if e.condition == "conflict": | |
354 log.debug(_("requested node already exists")) | |
355 | |
356 await self._p.publish(client, service, node, items=[item_elt]) | |
357 | |
358 if register: | |
359 extra = {} | |
360 self.onInvitationPreflight( | |
361 client, "", extra, service, node, event_id, item_elt | |
362 ) | 851 ) |
363 await self.host.plugins['LIST_INTEREST'].registerPubsub( | 852 except StopIteration: |
364 client, NS_EVENT, service, node, event_id, True, | 853 pass |
365 extra.pop("name", ""), extra.pop("element"), extra | 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.setTimestamp(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 } | |
366 ) | 876 ) |
367 return node | 877 rsvp_form = data_form.Form("submit", formNamespace=NS_RSVP) |
368 | 878 rsvp_form.makeFields(rsvp_data) |
369 def _eventModify(self, service, node, id_, timestamp_update, data_update, | 879 rsvp_elt.addChild(rsvp_form.toElement()) |
370 profile_key=C.PROF_KEY_NONE): | 880 return rsvp_elt |
371 service = jid.JID(service) if service else None | 881 else: |
372 if not node: | 882 return None |
373 raise ValueError(_("missing node")) | 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.getClient(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 [getAttachments] | |
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.getAttachments( | |
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 ): | |
374 client = self.host.getClient(profile_key) | 955 client = self.host.getClient(profile_key) |
375 return defer.ensureDeferred( | 956 return defer.ensureDeferred( |
376 self.eventModify( | 957 self.event_invitee_set( |
377 client, service, node, id_ or NS_EVENT, timestamp_update or None, | 958 client, |
378 data_update | 959 jid.JID(service) if service else None, |
960 node or None, | |
961 item, | |
962 data_format.deserialise(rsvp_s) | |
379 ) | 963 ) |
380 ) | 964 ) |
381 | 965 |
382 async def eventModify( | 966 async def event_invitee_set( |
383 self, client, service, node, id_=NS_EVENT, timestamp_update=None, data_update=None | 967 self, |
384 ): | 968 client: SatXMPPEntity, |
385 """Update an event | 969 service: Optional[jid.JID], |
386 | 970 node: Optional[str], |
387 Similar as create instead that it update existing item instead of | 971 item: str, |
388 creating or replacing it. Params are the same as for [eventCreate]. | 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) | |
389 """ | 980 """ |
390 event_timestamp, event_metadata = await self.eventGet(client, service, node, id_) | 981 if service is None: |
391 new_timestamp = event_timestamp if timestamp_update is None else timestamp_update | 982 service = client.jid.userhostJID() |
392 new_data = event_metadata | 983 if node is None: |
393 if data_update: | 984 node = NS_EVENTS |
394 for k, v in data_update.items(): | 985 await self._a.setAttachments(client, { |
395 new_data[k] = v | 986 "service": service.full(), |
396 await self.eventCreate(client, new_timestamp, new_data, service, node, id_) | 987 "node": node, |
397 | 988 "id": item, |
398 def _eventsListSerialise(self, events): | 989 "extra": {"rsvp": rsvp} |
399 for timestamp, data in events: | 990 }) |
400 data["date"] = str(timestamp) | |
401 data["creator"] = C.boolConst(data.get("creator", False)) | |
402 return [e[1] for e in events] | |
403 | |
404 def _eventsList(self, service, node, profile): | |
405 service = jid.JID(service) if service else None | |
406 node = node or None | |
407 client = self.host.getClient(profile) | |
408 d = self.eventsList(client, service, node) | |
409 d.addCallback(self._eventsListSerialise) | |
410 return d | |
411 | |
412 @defer.inlineCallbacks | |
413 def eventsList(self, client, service, node=None): | |
414 """Retrieve list of registered events | |
415 | |
416 @return list(tuple(int, dict)): list of events (timestamp + metadata) | |
417 """ | |
418 items, metadata = yield self.host.plugins['LIST_INTEREST'].listInterests( | |
419 client, service, node, namespace=NS_EVENT) | |
420 events = [] | |
421 for item in items: | |
422 try: | |
423 event_elt = next(item.interest.pubsub.elements(NS_EVENT, "event")) | |
424 except StopIteration: | |
425 log.warning( | |
426 _("No event found in item {item_id}, ignoring").format( | |
427 item_id=item["id"]) | |
428 ) | |
429 else: | |
430 timestamp, data = self._parseEventElt(event_elt) | |
431 data["interest_id"] = item["id"] | |
432 events.append((timestamp, data)) | |
433 defer.returnValue(events) | |
434 | |
435 def _eventInviteeGet(self, service, node, invitee_jid_s, profile_key): | |
436 service = jid.JID(service) if service else None | |
437 node = node if node else NS_EVENT | |
438 client = self.host.getClient(profile_key) | |
439 invitee_jid = jid.JID(invitee_jid_s) if invitee_jid_s else None | |
440 return defer.ensureDeferred( | |
441 self.eventInviteeGet(client, service, node, invitee_jid) | |
442 ) | |
443 | |
444 async def eventInviteeGet(self, client, service, node, invitee_jid=None): | |
445 """Retrieve attendance from event node | |
446 | |
447 @param service(unicode, None): PubSub service | |
448 @param node(unicode): PubSub node of the event's invitees | |
449 @param invitee_jid(jid.JId, None): jid of the invitee to retrieve (None to | |
450 retrieve profile's invitation). The bare jid correspond to the PubSub item id. | |
451 @return (dict): a dict with current attendance status, | |
452 an empty dict is returned if nothing has been answered yed | |
453 """ | |
454 if invitee_jid is None: | |
455 invitee_jid = client.jid | |
456 try: | |
457 items, metadata = await self._p.getItems( | |
458 client, service, node, item_ids=[invitee_jid.userhost()] | |
459 ) | |
460 event_elt = next(items[0].elements(NS_EVENT, "invitee")) | |
461 except (exceptions.NotFound, IndexError): | |
462 # no item found, event data are not set yet | |
463 return {} | |
464 data = {} | |
465 for key in ("attend", "guests"): | |
466 try: | |
467 data[key] = event_elt[key] | |
468 except KeyError: | |
469 continue | |
470 return data | |
471 | |
472 def _eventInviteeSet(self, service, node, event_data, profile_key): | |
473 service = jid.JID(service) if service else None | |
474 node = node if node else NS_EVENT | |
475 client = self.host.getClient(profile_key) | |
476 return defer.ensureDeferred( | |
477 self.eventInviteeSet(client, service, node, event_data) | |
478 ) | |
479 | |
480 async def eventInviteeSet(self, client, service, node, data): | |
481 """Set or update attendance data in event node | |
482 | |
483 @param service(unicode, None): PubSub service | |
484 @param node(unicode): PubSub node of the event | |
485 @param data(dict[unicode, unicode]): data to update | |
486 key can be: | |
487 attend: one of "yes", "no", "maybe" | |
488 guests: an int | |
489 """ | |
490 event_elt = domish.Element((NS_EVENT, "invitee")) | |
491 for key in ("attend", "guests"): | |
492 try: | |
493 event_elt[key] = data.pop(key) | |
494 except KeyError: | |
495 pass | |
496 item_elt = pubsub.Item(id=client.jid.userhost(), payload=event_elt) | |
497 return await self._p.publish(client, service, node, items=[item_elt]) | |
498 | 991 |
499 def _eventInviteesList(self, service, node, profile_key): | 992 def _eventInviteesList(self, service, node, profile_key): |
500 service = jid.JID(service) if service else None | 993 service = jid.JID(service) if service else None |
501 node = node if node else NS_EVENT | 994 node = node if node else NS_EVENT |
502 client = self.host.getClient(profile_key) | 995 client = self.host.getClient(profile_key) |
547 _('"XEP-0277" (blog) plugin is needed for this feature') | 1040 _('"XEP-0277" (blog) plugin is needed for this feature') |
548 ) | 1041 ) |
549 if item_id is None: | 1042 if item_id is None: |
550 item_id = extra["default_item_id"] = NS_EVENT | 1043 item_id = extra["default_item_id"] = NS_EVENT |
551 | 1044 |
552 __, event_data = await self.eventGet(client, service, node, item_id) | 1045 __, event_data = await self.events_get(client, service, node, item_id) |
553 log.debug(_("got event data")) | 1046 log.debug(_("got event data")) |
554 invitees_service = jid.JID(event_data["invitees_service"]) | 1047 invitees_service = jid.JID(event_data["invitees_service"]) |
555 invitees_node = event_data["invitees_node"] | 1048 invitees_node = event_data["invitees_node"] |
556 blog_service = jid.JID(event_data["blog_service"]) | 1049 blog_service = jid.JID(event_data["blog_service"]) |
557 blog_node = event_data["blog_node"] | 1050 blog_node = event_data["blog_node"] |
671 def __init__(self, plugin_parent): | 1164 def __init__(self, plugin_parent): |
672 self.plugin_parent = plugin_parent | 1165 self.plugin_parent = plugin_parent |
673 | 1166 |
674 def getDiscoInfo(self, requestor, target, nodeIdentifier=""): | 1167 def getDiscoInfo(self, requestor, target, nodeIdentifier=""): |
675 return [ | 1168 return [ |
676 disco.DiscoFeature(NS_EVENT), | 1169 disco.DiscoFeature(NS_EVENTS), |
677 ] | 1170 ] |
678 | 1171 |
679 def getDiscoItems(self, requestor, target, nodeIdentifier=""): | 1172 def getDiscoItems(self, requestor, target, nodeIdentifier=""): |
680 return [] | 1173 return [] |