diff --git a/sat/plugins/plugin_exp_events.py b/sat/plugins/plugin_exp_events.py
index d3bc0e7f..569b33fe 100644
--- a/sat/plugins/plugin_exp_events.py
+++ b/sat/plugins/plugin_exp_events.py
@@ -1,8 +1,8 @@
#!/usr/bin/env python3
-# SAT plugin to detect language (experimental)
-# Copyright (C) 2009-2021 Jérôme Poisson (goffi@goffi.org)
+# Libervia plugin to handle events
+# Copyright (C) 2009-2022 Jérôme Poisson (goffi@goffi.org)
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
@@ -17,23 +17,31 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
-from typing import Optional
+from random import seed
+from typing import Optional, Final, Dict, List, Union, Any, Optional
+from attr import attr
+
import shortuuid
+from sqlalchemy.orm.events import event
+from build.lib.sat.core.xmpp import SatXMPPClient
from sat.core.i18n import _
from sat.core import exceptions
from sat.core.constants import Const as C
from sat.core.log import getLogger
from sat.core.xmpp import SatXMPPEntity
+from sat.core.core_types import SatXMPPEntity
from sat.tools import utils
+from sat.tools import xml_tools
from sat.tools.common import uri as xmpp_uri
from sat.tools.common import date_utils
+from sat.tools.common import data_format
from twisted.internet import defer
from twisted.words.protocols.jabber import jid, error
from twisted.words.xish import domish
from wokkel import disco, iwokkel
from zope.interface import implementer
from twisted.words.protocols.jabber.xmlstream import XMPPHandler
-from wokkel import pubsub
+from wokkel import pubsub, data_form
log = getLogger(__name__)
@@ -42,74 +50,77 @@ PLUGIN_INFO = {
C.PI_NAME: "Events",
C.PI_IMPORT_NAME: "EVENTS",
C.PI_TYPE: "EXP",
+ C.PI_MODES: C.PLUG_MODE_BOTH,
C.PI_PROTOCOLS: [],
- C.PI_DEPENDENCIES: ["XEP-0060", "INVITATION", "PUBSUB_INVITATION", "LIST_INTEREST"],
+ C.PI_DEPENDENCIES: [
+ "XEP-0060", "XEP-0080", "XEP-0447", "XEP-0470", # "INVITATION", "PUBSUB_INVITATION",
+ # "LIST_INTEREST"
+ ],
C.PI_RECOMMENDATIONS: ["XEP-0277", "EMAIL_INVITATION"],
C.PI_MAIN: "Events",
C.PI_HANDLER: "yes",
- C.PI_DESCRIPTION: _("""Experimental implementation of XMPP events management"""),
+ C.PI_DESCRIPTION: _("""XMPP Events Management"""),
}
NS_EVENT = "org.salut-a-toi.event:0"
+NS_EVENTS: Final = "urn:xmpp:events:0"
+NS_RSVP: Final = "urn:xmpp:events:rsvp:0"
+NS_EXTRA: Final = "urn:xmpp:events:extra:0"
-class Events(object):
- """Q&D module to handle event attendance answer, experimentation only"""
+class Events:
+ namespace = NS_EVENTS
def __init__(self, host):
- log.info(_("Event plugin initialization"))
+ log.info(_("Events plugin initialization"))
self.host = host
- self._p = self.host.plugins["XEP-0060"]
- self._i = self.host.plugins.get("EMAIL_INVITATION")
- self._b = self.host.plugins.get("XEP-0277")
- self.host.registerNamespace("event", NS_EVENT)
- self.host.plugins["PUBSUB_INVITATION"].register(NS_EVENT, self)
+ self._p = host.plugins["XEP-0060"]
+ self._g = host.plugins["XEP-0080"]
+ self._b = host.plugins.get("XEP-0277")
+ self._sfs = host.plugins["XEP-0447"]
+ self._a = host.plugins["XEP-0470"]
+ # self._i = host.plugins.get("EMAIL_INVITATION")
+ host.registerNamespace("events", NS_EVENTS)
+ self._a.registerAttachmentHandler("rsvp", NS_EVENTS, self.rsvp_get, self.rsvp_set)
+ # host.plugins["PUBSUB_INVITATION"].register(NS_EVENTS, self)
host.bridge.addMethod(
- "eventGet",
+ "eventsGet",
".plugin",
- in_sign="ssss",
- out_sign="(ia{ss})",
- method=self._eventGet,
+ in_sign="ssasss",
+ out_sign="s",
+ method=self._events_get,
async_=True,
)
host.bridge.addMethod(
"eventCreate",
".plugin",
- in_sign="ia{ss}ssss",
- out_sign="s",
- method=self._eventCreate,
+ in_sign="sssss",
+ out_sign="",
+ method=self._event_create,
async_=True,
)
host.bridge.addMethod(
"eventModify",
".plugin",
- in_sign="sssia{ss}s",
+ in_sign="sssss",
out_sign="",
- method=self._eventModify,
- async_=True,
- )
- host.bridge.addMethod(
- "eventsList",
- ".plugin",
- in_sign="sss",
- out_sign="aa{ss}",
- method=self._eventsList,
+ method=self._event_modify,
async_=True,
)
host.bridge.addMethod(
"eventInviteeGet",
".plugin",
- in_sign="ssss",
- out_sign="a{ss}",
- method=self._eventInviteeGet,
+ in_sign="sssasss",
+ out_sign="s",
+ method=self._event_invitee_get,
async_=True,
)
host.bridge.addMethod(
"eventInviteeSet",
".plugin",
- in_sign="ssa{ss}s",
+ in_sign="sssss",
out_sign="",
- method=self._eventInviteeSet,
+ method=self._event_invitee_set,
async_=True,
)
host.bridge.addMethod(
@@ -212,289 +223,771 @@ class Events(object):
data["creator"] = True
return timestamp, data
- async def getEventElement(self, client, service, node, id_):
- """Retrieve event element
+ def event_elt_2_event_data(self, event_elt: domish.Element) -> Dict[str, Any]:
+ """Convert element to event data
- @param service(jid.JID): pubsub service
- @param node(unicode): pubsub node
- @param id_(unicode, None): event id
- @return (domish.Element): event element
- @raise exceptions.NotFound: no event element found
+ @param event_elt: element
+ parent element can also be used
+ @raise exceptions.NotFound: can't find event payload
+ @raise ValueError: something is missing or badly formed
"""
- if not id_:
- id_ = NS_EVENT
- items, metadata = await self._p.getItems(client, service, node, item_ids=[id_])
+ if event_elt.name == "item":
+ try:
+ event_elt = next(event_elt.elements(NS_EVENTS, "event"))
+ except StopIteration:
+ raise exceptions.NotFound(" payload is missing")
+
+ event_data: Dict[str, Any] = {}
+
+ # id
+
+ parent_elt = event_elt.parent
+ if parent_elt is not None and parent_elt.hasAttribute("id"):
+ event_data["id"] = parent_elt["id"]
+
+ # name
+
+ name_data: Dict[str, str] = {}
+ event_data["name"] = name_data
+ for name_elt in event_elt.elements(NS_EVENTS, "name"):
+ lang = name_elt.getAttribute("xml:lang", "")
+ if lang in name_data:
+ raise ValueError(" elements don't have distinct xml:lang")
+ name_data[lang] = str(name_elt)
+
+ if not name_data:
+ raise exceptions.NotFound(" element is missing")
+
+ # start
+
try:
- event_elt = next(items[0].elements(NS_EVENT, "event"))
+ start_elt = next(event_elt.elements(NS_EVENTS, "start"))
except StopIteration:
- raise exceptions.NotFound(_("No event element has been found"))
- except IndexError:
- raise exceptions.NotFound(_("No event with this id has been found"))
- return event_elt
+ raise exceptions.NotFound(" element is missing")
+ event_data["start"] = utils.parse_xmpp_date(str(start_elt))
- def _eventGet(self, service, node, id_="", profile_key=C.PROF_KEY_NONE):
- service = jid.JID(service) if service else None
- node = node if node else NS_EVENT
- client = self.host.getClient(profile_key)
- return defer.ensureDeferred(
- self.eventGet(client, service, node, id_)
- )
+ # end
- async def eventGet(self, client, service, node, id_=NS_EVENT):
- """Retrieve event data
-
- @param service(unicode, None): PubSub service
- @param node(unicode): PubSub node of the event
- @param id_(unicode): id_ with even data
- @return (tuple[int, dict[unicode, unicode]): event data:
- - timestamp of the event
- - event metadata where key can be:
- location: location of the event
- image: URL of a picture to use to represent event
- background-image: URL of a picture to use in background
- """
- event_elt = await self.getEventElement(client, service, node, id_)
-
- return self._parseEventElt(event_elt)
-
- def _eventCreate(
- self, timestamp, data, service, node, id_="", profile_key=C.PROF_KEY_NONE
- ):
- service = jid.JID(service) if service else None
- node = node or None
- client = self.host.getClient(profile_key)
- data["register"] = C.bool(data.get("register", C.BOOL_FALSE))
- return defer.ensureDeferred(
- self.eventCreate(client, timestamp, data, service, node, id_ or NS_EVENT)
- )
-
- async def eventCreate(self, client, timestamp, data, service, node=None, event_id=NS_EVENT):
- """Create or replace an event
-
- @param service(jid.JID, None): PubSub service
- @param node(unicode, None): PubSub node of the event
- None will create instant node.
- @param event_id(unicode): ID of the item to create.
- @param timestamp(timestamp, None)
- @param data(dict[unicode, unicode]): data to update
- dict will be cleared, do a copy if data are still needed
- key can be:
- - name: name of the event
- - description: details
- - image: main picture of the event
- - background-image: image to use as background
- - register: bool, True if we want to register the event in our local list
- @return (unicode): created node
- """
- if not event_id:
- raise ValueError(_("event_id must be set"))
- if not service:
- service = client.jid.userhostJID()
- if not node:
- node = NS_EVENT + "__" + shortuuid.uuid()
- event_elt = domish.Element((NS_EVENT, "event"))
- if timestamp is not None and timestamp != -1:
- formatted_date = utils.xmpp_date(timestamp)
- event_elt.addElement((NS_EVENT, "date"), content=formatted_date)
- register = data.pop("register", False)
- for key in ("name",):
- if key in data:
- event_elt[key] = data.pop(key)
- for key in ("description",):
- if key in data:
- event_elt.addElement((NS_EVENT, key), content=data.pop(key))
- for key in ("image", "background-image"):
- if key in data:
- elt = event_elt.addElement((NS_EVENT, key))
- elt["src"] = data.pop(key)
-
- # we first create the invitees and blog nodes (if not specified in data)
- for uri_type in ("invitees", "blog"):
- key = uri_type + "_uri"
- for to_delete in ("service", "node"):
- k = uri_type + "_" + to_delete
- if k in data:
- del data[k]
- if key not in data:
- # FIXME: affiliate invitees
- uri_node = await self._p.createNode(client, service)
- await self._p.setConfiguration(
- client,
- service,
- uri_node,
- {self._p.OPT_ACCESS_MODEL: self._p.ACCESS_WHITELIST},
- )
- uri_service = service
- else:
- uri = data.pop(key)
- uri_data = xmpp_uri.parseXMPPUri(uri)
- if uri_data["type"] != "pubsub":
- raise ValueError(
- _("The given URI is not valid: {uri}").format(uri=uri)
- )
- uri_service = jid.JID(uri_data["path"])
- uri_node = uri_data["node"]
-
- elt = event_elt.addElement((NS_EVENT, uri_type))
- elt["uri"] = xmpp_uri.buildXMPPUri(
- "pubsub", path=uri_service.full(), node=uri_node
- )
-
- # remaining data are put in elements
- for key in list(data.keys()):
- elt = event_elt.addElement((NS_EVENT, "meta"), content=data.pop(key))
- elt["name"] = key
-
- item_elt = pubsub.Item(id=event_id, payload=event_elt)
try:
- # TODO: check auto-create, no need to create node first if available
- node = await self._p.createNode(client, service, nodeIdentifier=node)
- except error.StanzaError as e:
- if e.condition == "conflict":
- log.debug(_("requested node already exists"))
+ end_elt = next(event_elt.elements(NS_EVENTS, "end"))
+ except StopIteration:
+ raise exceptions.NotFound(" element is missing")
+ event_data["end"] = utils.parse_xmpp_date(str(end_elt))
- await self._p.publish(client, service, node, items=[item_elt])
+ # head-picture
- if register:
- extra = {}
- self.onInvitationPreflight(
- client, "", extra, service, node, event_id, item_elt
- )
- await self.host.plugins['LIST_INTEREST'].registerPubsub(
- client, NS_EVENT, service, node, event_id, True,
- extra.pop("name", ""), extra.pop("element"), extra
- )
- return node
+ head_pic_elt = next(event_elt.elements(NS_EVENTS, "head-picture"), None)
+ if head_pic_elt is not None:
+ event_data["head-picture"] = self._sfs.parse_file_sharing_elt(head_pic_elt)
- def _eventModify(self, service, node, id_, timestamp_update, data_update,
- profile_key=C.PROF_KEY_NONE):
- service = jid.JID(service) if service else None
- if not node:
- raise ValueError(_("missing node"))
+ # description
+
+ seen_desc = set()
+ for description_elt in event_elt.elements(NS_EVENTS, "description"):
+ lang = description_elt.getAttribute("xml:lang", "")
+ desc_type = description_elt.getAttribute("type", "text")
+ lang_type = (lang, desc_type)
+ if lang_type in seen_desc:
+ raise ValueError(
+ " elements don't have distinct xml:lang/type"
+ )
+ seen_desc.add(lang_type)
+ descriptions = event_data.setdefault("descriptions", [])
+ description_data = {"description": str(description_elt)}
+ if lang:
+ description_data["language"] = lang
+ if desc_type:
+ description_data["type"] = desc_type
+ descriptions.append(description_data)
+
+ # categories
+
+ for category_elt in event_elt.elements(NS_EVENTS, "category"):
+ try:
+ category_data = {
+ "term": category_elt["term"]
+ }
+ except KeyError:
+ log.warning(
+ " element is missing mandatory term: "
+ f"{category_elt.toXml()}"
+ )
+ continue
+ wd = category_elt.getAttribute("wd")
+ if wd:
+ category_data["wikidata_id"] = wd
+ lang = category_elt.getAttribute("xml:lang")
+ if lang:
+ category_data["language"] = lang
+ event_data.setdefault("categories", []).append(category_data)
+
+ # locations
+
+ seen_location_ids = set()
+ for location_elt in event_elt.elements(NS_EVENTS, "location"):
+ location_id = location_elt.getAttribute("id", "")
+ if location_id in seen_location_ids:
+ raise ValueError(" elements don't have distinct IDs")
+ seen_location_ids.add(location_id)
+ location_data = self._g.parse_geoloc_elt(location_elt)
+ if location_id:
+ location_data["id"] = location_id
+ lang = location_elt.getAttribute("xml:lang", "")
+ if lang:
+ location_data["language"] = lang
+ event_data.setdefault("locations", []).append(location_data)
+
+ # RSVPs
+
+ seen_rsvp_lang = set()
+ for rsvp_elt in event_elt.elements(NS_EVENTS, "rsvp"):
+ rsvp_lang = rsvp_elt.getAttribute("xml:lang", "")
+ if rsvp_lang in seen_rsvp_lang:
+ raise ValueError(" elements don't have distinct xml:lang")
+ seen_rsvp_lang.add(rsvp_lang)
+ rsvp_form = data_form.findForm(rsvp_elt, NS_RSVP)
+ if rsvp_form is None:
+ log.warning(f"RSVP form is missing: {rsvp_elt.toXml()}")
+ continue
+ rsvp_data = xml_tools.dataForm2dataDict(rsvp_form)
+ if rsvp_lang:
+ rsvp_data["language"] = rsvp_lang
+ event_data.setdefault("rsvp", []).append(rsvp_data)
+
+ # linked pubsub nodes
+
+ for name in ("invitees", "comments", "blog", "schedule"):
+ elt = next(event_elt.elements(NS_EVENTS, name), None)
+ if elt is not None:
+ try:
+ event_data[name] = {
+ "service": elt["jid"],
+ "node": elt["node"]
+ }
+ except KeyError:
+ log.warning(f"invalid {name} element: {elt.toXml()}")
+
+ # attachments
+
+ attachments_elt = next(event_elt.elements(NS_EVENTS, "attachments"), None)
+ if attachments_elt:
+ attachments = event_data["attachments"] = []
+ for file_sharing_elt in attachments_elt.elements(
+ self._sfs.namespace, "file-sharing"):
+ try:
+ file_sharing_data = self._sfs.parse_file_sharing_elt(file_sharing_elt)
+ except Exception as e:
+ log.warning(f"invalid attachment: {e}\n{file_sharing_elt.toXml()}")
+ continue
+ attachments.append(file_sharing_data)
+
+ # extra
+
+ extra_elt = next(event_elt.elements(NS_EVENTS, "extra"), None)
+ if extra_elt is not None:
+ extra_form = data_form.findForm(extra_elt, NS_EXTRA)
+ if extra_form is None:
+ log.warning(f"extra form is missing: {extra_elt.toXml()}")
+ else:
+ extra_data = event_data["extra"] = {}
+ for name, value in extra_form.items():
+ if name.startswith("accessibility:"):
+ extra_data.setdefault("accessibility", {})[name[14:]] = value
+ elif name == "accessibility":
+ log.warning(
+ 'ignoring "accessibility" key which is not standard: '
+ f"{extra_form.toElement().toXml()}"
+ )
+ else:
+ extra_data[name] = value
+
+ # external
+
+ external_elt = next(event_elt.elements(NS_EVENTS, "external"), None)
+ if external_elt:
+ try:
+ event_data["external"] = {
+ "jid": external_elt["jid"],
+ "node": external_elt["node"],
+ "item": external_elt["item"]
+ }
+ except KeyError:
+ log.warning(f"invalid element: {external_elt.toXml()}")
+
+ return event_data
+
+ def _events_get(
+ self, service: str, node: str, event_ids: List[str], extra: str, profile_key: str
+ ):
client = self.host.getClient(profile_key)
- return defer.ensureDeferred(
- self.eventModify(
- client, service, node, id_ or NS_EVENT, timestamp_update or None,
- data_update
+ d = defer.ensureDeferred(
+ self.events_get(
+ client,
+ jid.JID(service) if service else None,
+ node if node else NS_EVENTS,
+ event_ids,
+ data_format.deserialise(extra)
)
)
-
- async def eventModify(
- self, client, service, node, id_=NS_EVENT, timestamp_update=None, data_update=None
- ):
- """Update an event
-
- Similar as create instead that it update existing item instead of
- creating or replacing it. Params are the same as for [eventCreate].
- """
- event_timestamp, event_metadata = await self.eventGet(client, service, node, id_)
- new_timestamp = event_timestamp if timestamp_update is None else timestamp_update
- new_data = event_metadata
- if data_update:
- for k, v in data_update.items():
- new_data[k] = v
- await self.eventCreate(client, new_timestamp, new_data, service, node, id_)
-
- def _eventsListSerialise(self, events):
- for timestamp, data in events:
- data["date"] = str(timestamp)
- data["creator"] = C.boolConst(data.get("creator", False))
- return [e[1] for e in events]
-
- def _eventsList(self, service, node, profile):
- service = jid.JID(service) if service else None
- node = node or None
- client = self.host.getClient(profile)
- d = self.eventsList(client, service, node)
- d.addCallback(self._eventsListSerialise)
+ d.addCallback(data_format.serialise)
return d
- @defer.inlineCallbacks
- def eventsList(self, client, service, node=None):
- """Retrieve list of registered events
+ async def events_get(
+ self,
+ client: SatXMPPEntity,
+ service: Optional[jid.JID],
+ node: str = NS_EVENTS,
+ events_ids: Optional[List[str]] = None,
+ extra: Optional[dict] = None,
+ ) -> List[Dict[str, Any]]:
+ """Retrieve event data
- @return list(tuple(int, dict)): list of events (timestamp + metadata)
+ @param service: pubsub service
+ @param node: pubsub node
+ @param event_id: pubsub item ID
+ @return: event data:
"""
- items, metadata = yield self.host.plugins['LIST_INTEREST'].listInterests(
- client, service, node, namespace=NS_EVENT)
+ if service is None:
+ service = client.jid.userhostJID()
+ items, __ = await self._p.getItems(
+ client, service, node, item_ids=events_ids, extra=extra
+ )
events = []
for item in items:
try:
- event_elt = next(item.interest.pubsub.elements(NS_EVENT, "event"))
- except StopIteration:
+ events.append(self.event_elt_2_event_data((item)))
+ except (ValueError, exceptions.NotFound):
log.warning(
- _("No event found in item {item_id}, ignoring").format(
- item_id=item["id"])
+ f"Can't parse event for item {item['id']}: {item.toXml()}"
)
- else:
- timestamp, data = self._parseEventElt(event_elt)
- data["interest_id"] = item["id"]
- events.append((timestamp, data))
- defer.returnValue(events)
- def _eventInviteeGet(self, service, node, invitee_jid_s, profile_key):
- service = jid.JID(service) if service else None
- node = node if node else NS_EVENT
+ return events
+
+ def _event_create(
+ self,
+ data_s: str,
+ service: str,
+ node: str,
+ event_id: str = "",
+ profile_key: str = C.PROF_KEY_NONE
+ ):
client = self.host.getClient(profile_key)
- invitee_jid = jid.JID(invitee_jid_s) if invitee_jid_s else None
return defer.ensureDeferred(
- self.eventInviteeGet(client, service, node, invitee_jid)
+ self.event_create(
+ client,
+ data_format.deserialise(data_s),
+ jid.JID(service) if service else None,
+ node or None,
+ event_id or None
+ )
)
- async def eventInviteeGet(self, client, service, node, invitee_jid=None):
+ def event_data_2_event_elt(self, event_data: Dict[str, Any]) -> domish.Element:
+ """Convert Event Data to corresponding Element
+
+ @param event_data: data of the event with keys as follow:
+ name (dict)
+ map of language to name
+ empty string can be used as key if no language is specified
+ this key is mandatory
+ start (int|float)
+ starting time of the event
+ this key is mandatory
+ end (int|float)
+ ending time of the event
+ this key is mandatory
+ head-picture(dict)
+ file sharing data for the main picture to use to represent the event
+ description(list[dict])
+ list of descriptions. If there are several descriptions, they must have
+ distinct (language, type).
+ Description data is dict which following keys:
+ description(str)
+ the description itself, either in plain text or xhtml
+ this key is mandatory
+ language(str)
+ ISO-639 language code
+ type(str)
+ type of the description, either "text" (default) or "xhtml"
+ categories(list[dict])
+ each category is a dict with following keys:
+ term(str)
+ human readable short text of the category
+ this key is mandatory
+ wikidata_id(str)
+ Entity ID from WikiData
+ language(str)
+ ISO-639 language code
+ locations(list[dict])
+ list of location dict as used in plugin XEP-0080 [get_geoloc_elt].
+ If several locations are used, they must have distinct IDs
+ rsvp(list[dict])
+ RSVP data. The dict is a data dict as used in
+ sat.tools.xml_tools.dataDict2dataForm with some extra keys.
+ The "attending" key is automatically added if it's not already present,
+ except if the "no_default" key is present. Thus, an empty dict can be used
+ to use default RSVP.
+ If several dict are present in the list, they must have different "lang"
+ keys.
+ Following extra key can be used:
+ language(str)
+ ISO-639 code for language used in the form
+ no_default(bool)
+ if True, the "attending" field won't be automatically added
+ invitees(dict)
+ link to pubsub node holding invitees list.
+ Following keys are mandatory:
+ service(str)
+ pubsub service where the node is
+ node (str)
+ pubsub node to use
+ comments(dict)
+ link to pubsub node holding XEP-0277 comments on the event itself.
+ Following keys are mandatory:
+ service(str)
+ pubsub service where the node is
+ node (str)
+ pubsub node to use
+ blog(dict)
+ link to pubsub node holding a blog about the event.
+ Following keys are mandatory:
+ service(str)
+ pubsub service where the node is
+ node (str)
+ pubsub node to use
+ schedule(dict)
+ link to pubsub node holding an events node describing the schedule of this
+ event.
+ Following keys are mandatory:
+ service(str)
+ pubsub service where the node is
+ node (str)
+ pubsub node to use
+ attachments[list[dict]]
+ list of file sharing data about all kind of attachments of interest for
+ the event.
+ extra(dict)
+ extra information about the event.
+ Keys can be:
+ website(str)
+ main website about the event
+ status(str)
+ status of the event.
+ Can be one of "confirmed", "tentative" or "cancelled"
+ languages(list[str])
+ ISO-639 codes for languages which will be mainly spoken at the
+ event
+ accessibility(dict)
+ accessibility informations.
+ Keys can be:
+ wheelchair
+ tell if the event is accessible to wheelchair.
+ Value can be "full", "partial" or "no"
+ external(dict):
+ if present, this event is a link to an external one.
+ Keys (all mandatory) are:
+ jid: pubsub service
+ node: pubsub node
+ item: event id
+ @return: Event element
+ @raise ValueError: some expected data were missing or incorrect
+ """
+ event_elt = domish.Element((NS_EVENTS, "event"))
+ try:
+ for lang, name in event_data["name"].items():
+ name_elt = event_elt.addElement("name", content=name)
+ if lang:
+ name_elt["xml:lang"] = lang
+ except (KeyError, TypeError):
+ raise ValueError('"name" field is not a dict mapping language to event name')
+ try:
+ event_elt.addElement("start", content=utils.xmpp_date(event_data["start"]))
+ event_elt.addElement("end", content=utils.xmpp_date(event_data["end"]))
+ except (KeyError, TypeError, ValueError):
+ raise ValueError('"start" and "end" fields are mandatory')
+
+ if "head-picture" in event_data:
+ head_pic_data = event_data["head-picture"]
+ head_picture_elt = event_elt.addElement("head-picture")
+ head_picture_elt.addChild(self._sfs.get_file_sharing_elt(**head_pic_data))
+
+ seen_desc = set()
+ if "descriptions" in event_data:
+ for desc_data in event_data["descriptions"]:
+ desc_type = desc_data.get("type", "text")
+ lang = desc_data.get("language") or ""
+ lang_type = (lang, desc_type)
+ if lang_type in seen_desc:
+ raise ValueError(
+ '"xml:lang" and "type" is not unique among descriptions: '
+ f"{desc_data}"
+ )
+ seen_desc.add(lang_type)
+ try:
+ description = desc_data["description"]
+ except KeyError:
+ log.warning(f"description is missing in {desc_data!r}")
+ continue
+
+ if desc_type == "text":
+ description_elt = event_elt.addElement(
+ "description", content=description
+ )
+ elif desc_type == "xhtml":
+ description_elt = event_elt.addElement("description")
+ div_elt = xml_tools.parse(description, namespace=C.NS_XHTML)
+ description_elt.addChild(div_elt)
+ else:
+ log.warning(f"unknown description type {desc_type!r}")
+ continue
+ if lang:
+ description_elt["xml:lang"] = lang
+ for category_data in event_data.get("categories", []):
+ try:
+ category_term = category_data["term"]
+ except KeyError:
+ log.warning(f'"term" is missing categories data: {category_data}')
+ continue
+ category_elt = event_elt.addElement("category")
+ category_elt["term"] = category_term
+ category_wd = category_data.get("wikidata_id")
+ if category_wd:
+ category_elt["wd"] = category_wd
+ category_lang = category_data.get("language")
+ if category_lang:
+ category_elt["xml:lang"] = category_lang
+
+ seen_location_ids = set()
+ for location_data in event_data.get("locations", []):
+ location_id = location_data.get("id", "")
+ if location_id in seen_location_ids:
+ raise ValueError("locations must have distinct IDs")
+ seen_location_ids.add(location_id)
+ location_elt = event_elt.addElement("location")
+ location_elt.addChild(self._g.get_geoloc_elt(location_data))
+ if location_id:
+ location_elt["id"] = location_id
+
+ rsvp_data_list: Optional[List[dict]] = event_data.get("rsvp")
+ if rsvp_data_list is not None:
+ seen_lang = set()
+ for rsvp_data in rsvp_data_list:
+ if not rsvp_data:
+ # we use a minimum data if an empty dict is received. It will be later
+ # filled with defaut "attending" field.
+ rsvp_data = {"fields": []}
+ rsvp_elt = event_elt.addElement("rsvp")
+ lang = rsvp_data.get("language", "")
+ if lang in seen_lang:
+ raise ValueError(
+ "If several RSVP are specified, they must have distinct "
+ f"languages. {lang!r} language has been used several times."
+ )
+ seen_lang.add(lang)
+ if lang:
+ rsvp_elt["xml:lang"] = lang
+ if not rsvp_data.get("no_default", False):
+ try:
+ next(f for f in rsvp_data["fields"] if f["name"] == "attending")
+ except StopIteration:
+ rsvp_data["fields"].append({
+ "type": "list-single",
+ "name": "attending",
+ "label": "Attending",
+ "options": [
+ {"label": "maybe", "value": "maybe"},
+ {"label": "yes", "value": "yes"},
+ {"label": "no", "value": "no"}
+ ],
+ "required": True
+ })
+ rsvp_data["namespace"] = NS_RSVP
+ rsvp_form = xml_tools.dataDict2dataForm(rsvp_data)
+ rsvp_elt.addChild(rsvp_form.toElement())
+
+ for node_type in ("invitees", "comments", "blog", "schedule"):
+ node_data = event_data.get(node_type)
+ if not node_data:
+ continue
+ try:
+ service, node = node_data["service"], node_data["node"]
+ except KeyError:
+ log.warning(f"invalid node data for {node_type}: {node_data}")
+ else:
+ pub_node_elt = event_elt.addElement(node_type)
+ pub_node_elt["jid"] = service
+ pub_node_elt["node"] = node
+
+ attachments = event_data.get("attachments")
+ if attachments:
+ attachments_elt = event_elt.addElement("attachments")
+ for attachment_data in attachments:
+ attachments_elt.addChild(
+ self._sfs.get_file_sharing_elt(**attachment_data)
+ )
+
+ extra = event_data.get("extra")
+ if extra:
+ extra_form = data_form.Form(
+ "result",
+ formNamespace=NS_EXTRA
+ )
+ for node_type in ("website", "status"):
+ if node_type in extra:
+ extra_form.addField(
+ data_form.Field(var=node_type, value=extra[node_type])
+ )
+ if "languages" in extra:
+ extra_form.addField(
+ data_form.Field(
+ "list-multi", var="languages", values=extra["languages"]
+ )
+ )
+ for node_type, value in extra.get("accessibility", {}).items():
+ extra_form.addField(
+ data_form.Field(var=f"accessibility:{node_type}", value=value)
+ )
+
+ extra_elt = event_elt.addElement("extra")
+ extra_elt.addChild(extra_form.toElement())
+
+ if "external" in event_data:
+ external_data = event_data["external"]
+ external_elt = event_elt.addElement("external")
+ for node_type in ("jid", "node", "item"):
+ try:
+ value = external_data[node_type]
+ except KeyError:
+ raise ValueError(f"Invalid external data: {external_data}")
+ external_elt[node_type] = value
+
+ return event_elt
+
+ async def event_create(
+ self,
+ client: SatXMPPEntity,
+ event_data: Dict[str, Any],
+ service: Optional[jid.JID] = None,
+ node: Optional[str] = None,
+ event_id: Optional[str] = None,
+ ) -> None:
+ """Create or replace an event
+
+ @param event_data: data of the event (cf. [event_data_2_event_elt])
+ @param node: PubSub node of the event
+ None to use default node (default namespace for personal agenda)
+ @param service: PubSub service
+ None to use profile's PEP
+ @param event_id: ID of the item to create.
+ """
+ if not service:
+ service = client.jid.userhostJID()
+ if not node:
+ node = NS_EVENTS
+ if event_id is None:
+ event_id = shortuuid.uuid()
+ event_elt = self.event_data_2_event_elt(event_data)
+
+ item_elt = pubsub.Item(id=event_id, payload=event_elt)
+ options = {self._p.OPT_ACCESS_MODEL: self._p.ACCESS_WHITELIST}
+ await self._p.createIfNewNode(
+ client, service, nodeIdentifier=node, options=options
+ )
+ await self._p.publish(client, service, node, items=[item_elt])
+ if event_data.get("rsvp"):
+ await self._a.create_attachments_node(client, service, node, event_id)
+
+ def _event_modify(
+ self,
+ data_s: str,
+ event_id: str,
+ service: str,
+ node: str,
+ profile_key: str = C.PROF_KEY_NONE
+ ) -> None:
+ client = self.host.getClient(profile_key)
+ defer.ensureDeferred(
+ self.event_modify(
+ client,
+ data_format.deserialise(data_s),
+ event_id,
+ jid.JID(service) if service else None,
+ node or None,
+ )
+ )
+
+ async def event_modify(
+ self,
+ client: SatXMPPEntity,
+ event_data: Dict[str, Any],
+ event_id: str,
+ service: Optional[jid.JID] = None,
+ node: Optional[str] = None,
+ ) -> None:
+ """Update an event
+
+ Similar as create instead that it update existing item instead of
+ creating or replacing it. Params are the same as for [event_create].
+ """
+ if not service:
+ service = client.jid.userhostJID()
+ if not node:
+ node = NS_EVENTS
+ old_event = (await self.events_get(client, service, node, [event_id]))[0]
+ old_event.update(event_data)
+ event_data = old_event
+ await self.event_create(client, event_data, service, node, event_id)
+
+ def rsvp_get(
+ self,
+ client: SatXMPPEntity,
+ attachments_elt: domish.Element,
+ data: Dict[str, Any],
+ ) -> None:
+ """Get RSVP answers from attachments"""
+ try:
+ rsvp_elt = next(
+ attachments_elt.elements(NS_EVENTS, "rsvp")
+ )
+ except StopIteration:
+ pass
+ else:
+ rsvp_form = data_form.findForm(rsvp_elt, NS_RSVP)
+ if rsvp_form is not None:
+ data["rsvp"] = rsvp_data = dict(rsvp_form)
+ self._a.setTimestamp(rsvp_elt, rsvp_data)
+
+ def rsvp_set(
+ self,
+ client: SatXMPPEntity,
+ data: Dict[str, Any],
+ former_elt: Optional[domish.Element]
+ ) -> Optional[domish.Element]:
+ """update the attachment"""
+ rsvp_data = data["extra"].get("rsvp")
+ if rsvp_data is None:
+ return former_elt
+ elif rsvp_data:
+ rsvp_elt = domish.Element(
+ (NS_EVENTS, "rsvp"),
+ attribs = {
+ "timestamp": utils.xmpp_date()
+ }
+ )
+ rsvp_form = data_form.Form("submit", formNamespace=NS_RSVP)
+ rsvp_form.makeFields(rsvp_data)
+ rsvp_elt.addChild(rsvp_form.toElement())
+ return rsvp_elt
+ else:
+ return None
+
+ def _event_invitee_get(
+ self,
+ service: str,
+ node: str,
+ item: str,
+ invitees_s: List[str],
+ extra: str,
+ profile_key: str
+ ) -> defer.Deferred:
+ client = self.host.getClient(profile_key)
+ if invitees_s:
+ invitees = [jid.JID(i) for i in invitees_s]
+ else:
+ invitees = None
+ d = defer.ensureDeferred(
+ self.event_invitee_get(
+ client,
+ jid.JID(service) if service else None,
+ node or None,
+ item,
+ invitees,
+ data_format.deserialise(extra)
+ )
+ )
+ d.addCallback(lambda ret: data_format.serialise(ret))
+ return d
+
+ async def event_invitee_get(
+ self,
+ client: SatXMPPEntity,
+ service: Optional[jid.JID],
+ node: Optional[str],
+ item: str,
+ invitees: Optional[List[jid.JID]] = None,
+ extra: Optional[Dict[str, Any]] = None,
+ ) -> Dict[str, Dict[str, Any]]:
"""Retrieve attendance from event node
- @param service(unicode, None): PubSub service
- @param node(unicode): PubSub node of the event's invitees
- @param invitee_jid(jid.JId, None): jid of the invitee to retrieve (None to
- retrieve profile's invitation). The bare jid correspond to the PubSub item id.
- @return (dict): a dict with current attendance status,
+ @param service: PubSub service
+ @param node: PubSub node of the event
+ @param item: PubSub item of the event
+ @param invitees: if set, only retrieve RSVPs from those guests
+ @param extra: extra data used to retrieve items as for [getAttachments]
+ @return: mapping of invitee bare JID to their RSVP
an empty dict is returned if nothing has been answered yed
"""
- if invitee_jid is None:
- invitee_jid = client.jid
- try:
- items, metadata = await self._p.getItems(
- client, service, node, item_ids=[invitee_jid.userhost()]
- )
- event_elt = next(items[0].elements(NS_EVENT, "invitee"))
- except (exceptions.NotFound, IndexError):
- # no item found, event data are not set yet
- return {}
- data = {}
- for key in ("attend", "guests"):
+ if service is None:
+ service = client.jid.userhostJID()
+ if node is None:
+ node = NS_EVENTS
+ attachments, metadata = await self._a.getAttachments(
+ client, service, node, item, invitees, extra
+ )
+ ret = {}
+ for attachment in attachments:
try:
- data[key] = event_elt[key]
+ rsvp = attachment["rsvp"]
except KeyError:
continue
- return data
+ ret[attachment["from"]] = rsvp
- def _eventInviteeSet(self, service, node, event_data, profile_key):
- service = jid.JID(service) if service else None
- node = node if node else NS_EVENT
+ return ret
+
+ def _event_invitee_set(
+ self,
+ service: str,
+ node: str,
+ item: str,
+ rsvp_s: str,
+ profile_key: str
+ ):
client = self.host.getClient(profile_key)
return defer.ensureDeferred(
- self.eventInviteeSet(client, service, node, event_data)
+ self.event_invitee_set(
+ client,
+ jid.JID(service) if service else None,
+ node or None,
+ item,
+ data_format.deserialise(rsvp_s)
+ )
)
- async def eventInviteeSet(self, client, service, node, data):
+ async def event_invitee_set(
+ self,
+ client: SatXMPPEntity,
+ service: Optional[jid.JID],
+ node: Optional[str],
+ item: str,
+ rsvp: Dict[str, Any],
+ ) -> None:
"""Set or update attendance data in event node
- @param service(unicode, None): PubSub service
- @param node(unicode): PubSub node of the event
- @param data(dict[unicode, unicode]): data to update
- key can be:
- attend: one of "yes", "no", "maybe"
- guests: an int
+ @param service: PubSub service
+ @param node: PubSub node of the event
+ @param item: PubSub item of the event
+ @param rsvp: RSVP data (values to submit to the form)
"""
- event_elt = domish.Element((NS_EVENT, "invitee"))
- for key in ("attend", "guests"):
- try:
- event_elt[key] = data.pop(key)
- except KeyError:
- pass
- item_elt = pubsub.Item(id=client.jid.userhost(), payload=event_elt)
- return await self._p.publish(client, service, node, items=[item_elt])
+ if service is None:
+ service = client.jid.userhostJID()
+ if node is None:
+ node = NS_EVENTS
+ await self._a.setAttachments(client, {
+ "service": service.full(),
+ "node": node,
+ "id": item,
+ "extra": {"rsvp": rsvp}
+ })
def _eventInviteesList(self, service, node, profile_key):
service = jid.JID(service) if service else None
@@ -549,7 +1042,7 @@ class Events(object):
if item_id is None:
item_id = extra["default_item_id"] = NS_EVENT
- __, event_data = await self.eventGet(client, service, node, item_id)
+ __, event_data = await self.events_get(client, service, node, item_id)
log.debug(_("got event data"))
invitees_service = jid.JID(event_data["invitees_service"])
invitees_node = event_data["invitees_node"]
@@ -673,7 +1166,7 @@ class EventsHandler(XMPPHandler):
def getDiscoInfo(self, requestor, target, nodeIdentifier=""):
return [
- disco.DiscoFeature(NS_EVENT),
+ disco.DiscoFeature(NS_EVENTS),
]
def getDiscoItems(self, requestor, target, nodeIdentifier=""):