changeset 4027:26c3e1bc7fb7

plugin XEP-0471: renamed "events" plugin to XEP-0471 now that there is a XEP
author Goffi <goffi@goffi.org>
date Thu, 30 Mar 2023 16:47:41 +0200
parents fe4725bf42fb
children 883db2790b11
files sat/plugins/plugin_comp_ap_gateway/__init__.py sat/plugins/plugin_comp_ap_gateway/events.py sat/plugins/plugin_exp_events.py sat/plugins/plugin_exp_invitation_pubsub.py sat/plugins/plugin_misc_lists.py sat/plugins/plugin_xep_0471.py
diffstat 6 files changed, 1179 insertions(+), 1179 deletions(-) [+]
line wrap: on
line diff
--- a/sat/plugins/plugin_comp_ap_gateway/__init__.py	Fri Mar 24 10:29:48 2023 +0100
+++ b/sat/plugins/plugin_comp_ap_gateway/__init__.py	Thu Mar 30 16:47:41 2023 +0200
@@ -109,7 +109,7 @@
     C.PI_DEPENDENCIES: [
         "XEP-0050", "XEP-0054", "XEP-0060", "XEP-0084", "XEP-0106", "XEP-0277",
         "XEP-0292", "XEP-0329", "XEP-0372", "XEP-0424", "XEP-0465", "XEP-0470",
-        "XEP-0447", "PUBSUB_CACHE", "TEXT_SYNTAXES", "IDENTITY", "EVENTS"
+        "XEP-0447", "XEP-0471", "PUBSUB_CACHE", "TEXT_SYNTAXES", "IDENTITY"
     ],
     C.PI_RECOMMENDATIONS: [],
     C.PI_MAIN: "APGateway",
@@ -150,7 +150,7 @@
         self._c = host.plugins["PUBSUB_CACHE"]
         self._t = host.plugins["TEXT_SYNTAXES"]
         self._i = host.plugins["IDENTITY"]
-        self._events = host.plugins["EVENTS"]
+        self._events = host.plugins["XEP-0471"]
         self._p.addManagedNode(
             "",
             items_cb=self._itemsReceived,
--- a/sat/plugins/plugin_comp_ap_gateway/events.py	Fri Mar 24 10:29:48 2023 +0100
+++ b/sat/plugins/plugin_comp_ap_gateway/events.py	Thu Mar 30 16:47:41 2023 +0200
@@ -96,7 +96,7 @@
     def __init__(self, apg):
         self.host = apg.host
         self.apg = apg
-        self._events = self.host.plugins["EVENTS"]
+        self._events = self.host.plugins["XEP-0471"]
 
     async def event_data_2_ap_item(
         self, event_data: dict, author_jid: jid.JID, is_new: bool=True
--- a/sat/plugins/plugin_exp_events.py	Fri Mar 24 10:29:48 2023 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,1173 +0,0 @@
-#!/usr/bin/env python3
-
-
-# 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
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU Affero General Public License for more details.
-
-# You should have received a copy of the GNU Affero General Public License
-# along with this program.  If not, see <http://www.gnu.org/licenses/>.
-
-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 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, data_form
-
-log = getLogger(__name__)
-
-
-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", "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: _("""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:
-    namespace = NS_EVENTS
-
-    def __init__(self, host):
-        log.info(_("Events plugin initialization"))
-        self.host = host
-        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.register_attachment_handler("rsvp", NS_EVENTS, self.rsvp_get, self.rsvp_set)
-        # host.plugins["PUBSUB_INVITATION"].register(NS_EVENTS, self)
-        host.bridge.addMethod(
-            "eventsGet",
-            ".plugin",
-            in_sign="ssasss",
-            out_sign="s",
-            method=self._events_get,
-            async_=True,
-        )
-        host.bridge.addMethod(
-            "eventCreate",
-            ".plugin",
-            in_sign="sssss",
-            out_sign="",
-            method=self._event_create,
-            async_=True,
-        )
-        host.bridge.addMethod(
-            "eventModify",
-            ".plugin",
-            in_sign="sssss",
-            out_sign="",
-            method=self._event_modify,
-            async_=True,
-        )
-        host.bridge.addMethod(
-            "eventInviteeGet",
-            ".plugin",
-            in_sign="sssasss",
-            out_sign="s",
-            method=self._event_invitee_get,
-            async_=True,
-        )
-        host.bridge.addMethod(
-            "eventInviteeSet",
-            ".plugin",
-            in_sign="sssss",
-            out_sign="",
-            method=self._event_invitee_set,
-            async_=True,
-        )
-        host.bridge.addMethod(
-            "eventInviteesList",
-            ".plugin",
-            in_sign="sss",
-            out_sign="a{sa{ss}}",
-            method=self._eventInviteesList,
-            async_=True,
-        ),
-        host.bridge.addMethod(
-            "eventInvite",
-            ".plugin",
-            in_sign="sssss",
-            out_sign="",
-            method=self._invite,
-            async_=True,
-        )
-        host.bridge.addMethod(
-            "eventInviteByEmail",
-            ".plugin",
-            in_sign="ssssassssssss",
-            out_sign="",
-            method=self._inviteByEmail,
-            async_=True,
-        )
-
-    def getHandler(self, client):
-        return EventsHandler(self)
-
-    def _parseEventElt(self, event_elt):
-        """Helper method to parse event element
-
-        @param (domish.Element): event_elt
-        @return (tuple[int, dict[unicode, unicode]): timestamp, event_data
-        """
-        try:
-            timestamp = date_utils.date_parse(next(event_elt.elements(NS_EVENT, "date")))
-        except StopIteration:
-            timestamp = -1
-
-        data = {}
-
-        for key in ("name",):
-            try:
-                data[key] = event_elt[key]
-            except KeyError:
-                continue
-
-        for elt_name in ("description",):
-            try:
-                elt = next(event_elt.elements(NS_EVENT, elt_name))
-            except StopIteration:
-                continue
-            else:
-                data[elt_name] = str(elt)
-
-        for elt_name in ("image", "background-image"):
-            try:
-                image_elt = next(event_elt.elements(NS_EVENT, elt_name))
-                data[elt_name] = image_elt["src"]
-            except StopIteration:
-                continue
-            except KeyError:
-                log.warning(_("no src found for image"))
-
-        for uri_type in ("invitees", "blog"):
-            try:
-                elt = next(event_elt.elements(NS_EVENT, uri_type))
-                uri = data[uri_type + "_uri"] = elt["uri"]
-                uri_data = xmpp_uri.parseXMPPUri(uri)
-                if uri_data["type"] != "pubsub":
-                    raise ValueError
-            except StopIteration:
-                log.warning(_("no {uri_type} element found!").format(uri_type=uri_type))
-            except KeyError:
-                log.warning(_("incomplete {uri_type} element").format(uri_type=uri_type))
-            except ValueError:
-                log.warning(_("bad {uri_type} element").format(uri_type=uri_type))
-            else:
-                data[uri_type + "_service"] = uri_data["path"]
-                data[uri_type + "_node"] = uri_data["node"]
-
-        for meta_elt in event_elt.elements(NS_EVENT, "meta"):
-            key = meta_elt["name"]
-            if key in data:
-                log.warning(
-                    "Ignoring conflicting meta element: {xml}".format(
-                        xml=meta_elt.toXml()
-                    )
-                )
-                continue
-            data[key] = str(meta_elt)
-        if event_elt.link:
-            link_elt = event_elt.link
-            data["service"] = link_elt["service"]
-            data["node"] = link_elt["node"]
-            data["item"] = link_elt["item"]
-        if event_elt.getAttribute("creator") == "true":
-            data["creator"] = True
-        return timestamp, data
-
-    def event_elt_2_event_data(self, event_elt: domish.Element) -> Dict[str, Any]:
-        """Convert <event/> element to event data
-
-        @param event_elt: <event/> element
-            parent <item/> element can also be used
-        @raise exceptions.NotFound: can't find event payload
-        @raise ValueError: something is missing or badly formed
-        """
-        if event_elt.name == "item":
-            try:
-                event_elt = next(event_elt.elements(NS_EVENTS, "event"))
-            except StopIteration:
-                raise exceptions.NotFound("<event/> 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("<name/> elements don't have distinct xml:lang")
-            name_data[lang] = str(name_elt)
-
-        if not name_data:
-            raise exceptions.NotFound("<name/> element is missing")
-
-        # start
-
-        try:
-            start_elt = next(event_elt.elements(NS_EVENTS, "start"))
-        except StopIteration:
-            raise exceptions.NotFound("<start/> element is missing")
-        event_data["start"] = utils.parse_xmpp_date(str(start_elt))
-
-        # end
-
-        try:
-            end_elt = next(event_elt.elements(NS_EVENTS, "end"))
-        except StopIteration:
-            raise exceptions.NotFound("<end/> element is missing")
-        event_data["end"] = utils.parse_xmpp_date(str(end_elt))
-
-        # head-picture
-
-        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)
-
-        # 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(
-                    "<description/> 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(
-                    "<category/> 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("<location/> 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("<rsvp/> 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 <external/> 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)
-        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)
-            )
-        )
-        d.addCallback(data_format.serialise)
-        return d
-
-    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
-
-        @param service: pubsub service
-        @param node: pubsub node
-        @param event_id: pubsub item ID
-        @return: event data:
-        """
-        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:
-                events.append(self.event_elt_2_event_data((item)))
-            except (ValueError, exceptions.NotFound):
-                log.warning(
-                    f"Can't parse event for item {item['id']}: {item.toXml()}"
-                )
-
-        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)
-        return defer.ensureDeferred(
-            self.event_create(
-                client,
-                data_format.deserialise(data_s),
-                jid.JID(service) if service else None,
-                node or None,
-                event_id or 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 <reaction> 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: 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 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:
-                rsvp = attachment["rsvp"]
-            except KeyError:
-                continue
-            ret[attachment["from"]] = rsvp
-
-        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.event_invitee_set(
-                client,
-                jid.JID(service) if service else None,
-                node or None,
-                item,
-                data_format.deserialise(rsvp_s)
-            )
-        )
-
-    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: 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)
-        """
-        if service is None:
-            service = client.jid.userhostJID()
-        if node is None:
-            node = NS_EVENTS
-        await self._a.set_attachements(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
-        node = node if node else NS_EVENT
-        client = self.host.getClient(profile_key)
-        return defer.ensureDeferred(
-            self.eventInviteesList(client, service, node)
-        )
-
-    async def eventInviteesList(self, client, service, node):
-        """Retrieve attendance from event node
-
-        @param service(unicode, None): PubSub service
-        @param node(unicode): PubSub node of the event
-        @return (dict): a dict with current attendance status,
-            an empty dict is returned if nothing has been answered yed
-        """
-        items, metadata = await self._p.getItems(client, service, node)
-        invitees = {}
-        for item in items:
-            try:
-                event_elt = next(item.elements(NS_EVENT, "invitee"))
-            except StopIteration:
-                # no item found, event data are not set yet
-                log.warning(_(
-                    "no data found for {item_id} (service: {service}, node: {node})"
-                    .format(item_id=item["id"], service=service, node=node)))
-            else:
-                data = {}
-                for key in ("attend", "guests"):
-                    try:
-                        data[key] = event_elt[key]
-                    except KeyError:
-                        continue
-                invitees[item["id"]] = data
-        return invitees
-
-    async def invitePreflight(
-        self,
-        client: SatXMPPEntity,
-        invitee_jid: jid.JID,
-        service: jid.JID,
-        node: str,
-        item_id: Optional[str] = None,
-        name: str = '',
-        extra: Optional[dict] = None,
-    ) -> None:
-        if self._b is None:
-            raise exceptions.FeatureNotFound(
-                _('"XEP-0277" (blog) plugin is needed for this feature')
-            )
-        if item_id is None:
-            item_id = extra["default_item_id"] = NS_EVENT
-
-        __, 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"]
-        blog_service = jid.JID(event_data["blog_service"])
-        blog_node = event_data["blog_node"]
-        await self._p.setNodeAffiliations(
-            client, invitees_service, invitees_node, {invitee_jid: "publisher"}
-        )
-        log.debug(
-            f"affiliation set on invitee node (jid: {invitees_service}, "
-            f"node: {invitees_node!r})"
-        )
-        await self._p.setNodeAffiliations(
-            client, blog_service, blog_node, {invitee_jid: "member"}
-        )
-        blog_items, __ = await self._b.mbGet(client, blog_service, blog_node, None)
-
-        for item in blog_items:
-            try:
-                comments_service = jid.JID(item["comments_service"])
-                comments_node = item["comments_node"]
-            except KeyError:
-                log.debug(
-                    "no comment service set for item {item_id}".format(
-                        item_id=item["id"]
-                    )
-                )
-            else:
-                await self._p.setNodeAffiliations(
-                    client, comments_service, comments_node, {invitee_jid: "publisher"}
-                )
-        log.debug(_("affiliation set on blog and comments nodes"))
-
-    def _invite(self, invitee_jid, service, node, item_id, profile):
-        return self.host.plugins["PUBSUB_INVITATION"]._sendPubsubInvitation(
-            invitee_jid, service, node, item_id or NS_EVENT, profile_key=profile
-        )
-
-    def _inviteByEmail(self, service, node, id_=NS_EVENT, email="", emails_extra=None,
-                       name="", host_name="", language="", url_template="",
-                       message_subject="", message_body="",
-                       profile_key=C.PROF_KEY_NONE):
-        client = self.host.getClient(profile_key)
-        kwargs = {
-            "profile": client.profile,
-            "emails_extra": [str(e) for e in emails_extra],
-        }
-        for key in (
-            "email",
-            "name",
-            "host_name",
-            "language",
-            "url_template",
-            "message_subject",
-            "message_body",
-        ):
-            value = locals()[key]
-            kwargs[key] = str(value)
-        return defer.ensureDeferred(self.inviteByEmail(
-            client, jid.JID(service) if service else None, node, id_ or NS_EVENT, **kwargs
-        ))
-
-    async def inviteByEmail(self, client, service, node, id_=NS_EVENT, **kwargs):
-        """High level method to create an email invitation to an event
-
-        @param service(unicode, None): PubSub service
-        @param node(unicode): PubSub node of the event
-        @param id_(unicode): id_ with even data
-        """
-        if self._i is None:
-            raise exceptions.FeatureNotFound(
-                _('"Invitations" plugin is needed for this feature')
-            )
-        if self._b is None:
-            raise exceptions.FeatureNotFound(
-                _('"XEP-0277" (blog) plugin is needed for this feature')
-            )
-        service = service or client.jid.userhostJID()
-        event_uri = xmpp_uri.buildXMPPUri(
-            "pubsub", path=service.full(), node=node, item=id_
-        )
-        kwargs["extra"] = {"event_uri": event_uri}
-        invitation_data = await self._i.create(**kwargs)
-        invitee_jid = invitation_data["jid"]
-        log.debug(_("invitation created"))
-        # now that we have a jid, we can send normal invitation
-        await self.invite(client, invitee_jid, service, node, id_)
-
-    def onInvitationPreflight(
-        self,
-        client: SatXMPPEntity,
-        name: str,
-        extra: dict,
-        service: jid.JID,
-        node: str,
-        item_id: Optional[str],
-        item_elt: domish.Element
-    ) -> None:
-        event_elt = item_elt.event
-        link_elt = event_elt.addElement("link")
-        link_elt["service"] = service.full()
-        link_elt["node"] = node
-        link_elt["item"] = item_id
-        __, event_data = self._parseEventElt(event_elt)
-        try:
-            name = event_data["name"]
-        except KeyError:
-            pass
-        else:
-            extra["name"] = name
-        if 'image' in event_data:
-            extra["thumb_url"] = event_data['image']
-        extra["element"] = event_elt
-
-
-@implementer(iwokkel.IDisco)
-class EventsHandler(XMPPHandler):
-
-    def __init__(self, plugin_parent):
-        self.plugin_parent = plugin_parent
-
-    def getDiscoInfo(self, requestor, target, nodeIdentifier=""):
-        return [
-            disco.DiscoFeature(NS_EVENTS),
-        ]
-
-    def getDiscoItems(self, requestor, target, nodeIdentifier=""):
-        return []
--- a/sat/plugins/plugin_exp_invitation_pubsub.py	Fri Mar 24 10:29:48 2023 +0100
+++ b/sat/plugins/plugin_exp_invitation_pubsub.py	Thu Mar 30 16:47:41 2023 +0200
@@ -147,11 +147,11 @@
             extra = {}
         try:
             handler = self._ns_handler[namespace]
-            preflight = handler.onInvitationPreflight
+            preflight = handler.on_invitation_preflight
         except KeyError:
             pass
         except AttributeError:
-            log.debug(f"no onInvitationPreflight method found for {namespace!r}")
+            log.debug(f"no on_invitation_preflight method found for {namespace!r}")
         else:
             await utils.asDeferred(
                 preflight,
--- a/sat/plugins/plugin_misc_lists.py	Fri Mar 24 10:29:48 2023 +0100
+++ b/sat/plugins/plugin_misc_lists.py	Thu Mar 30 16:47:41 2023 +0200
@@ -297,7 +297,7 @@
             async_=True,
         )
 
-    async def onInvitationPreflight(
+    async def on_invitation_preflight(
         self,
         client: SatXMPPEntity,
         namespace: str,
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/sat/plugins/plugin_xep_0471.py	Thu Mar 30 16:47:41 2023 +0200
@@ -0,0 +1,1173 @@
+#!/usr/bin/env python3
+
+
+# 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
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+
+# You should have received a copy of the GNU Affero General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+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 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, data_form
+
+log = getLogger(__name__)
+
+
+PLUGIN_INFO = {
+    C.PI_NAME: "Events",
+    C.PI_IMPORT_NAME: "XEP-0471",
+    C.PI_TYPE: "XEP",
+    C.PI_MODES: C.PLUG_MODE_BOTH,
+    C.PI_PROTOCOLS: [],
+    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: "XEP_0471",
+    C.PI_HANDLER: "yes",
+    C.PI_DESCRIPTION: _("""Calendar Events"""),
+}
+
+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 XEP_0471:
+    namespace = NS_EVENTS
+
+    def __init__(self, host):
+        log.info(_("Events plugin initialization"))
+        self.host = host
+        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.register_attachment_handler("rsvp", NS_EVENTS, self.rsvp_get, self.rsvp_set)
+        # host.plugins["PUBSUB_INVITATION"].register(NS_EVENTS, self)
+        host.bridge.addMethod(
+            "eventsGet",
+            ".plugin",
+            in_sign="ssasss",
+            out_sign="s",
+            method=self._events_get,
+            async_=True,
+        )
+        host.bridge.addMethod(
+            "eventCreate",
+            ".plugin",
+            in_sign="sssss",
+            out_sign="",
+            method=self._event_create,
+            async_=True,
+        )
+        host.bridge.addMethod(
+            "eventModify",
+            ".plugin",
+            in_sign="sssss",
+            out_sign="",
+            method=self._event_modify,
+            async_=True,
+        )
+        host.bridge.addMethod(
+            "eventInviteeGet",
+            ".plugin",
+            in_sign="sssasss",
+            out_sign="s",
+            method=self._event_invitee_get,
+            async_=True,
+        )
+        host.bridge.addMethod(
+            "eventInviteeSet",
+            ".plugin",
+            in_sign="sssss",
+            out_sign="",
+            method=self._event_invitee_set,
+            async_=True,
+        )
+        host.bridge.addMethod(
+            "eventInviteesList",
+            ".plugin",
+            in_sign="sss",
+            out_sign="a{sa{ss}}",
+            method=self._eventInviteesList,
+            async_=True,
+        ),
+        host.bridge.addMethod(
+            "eventInvite",
+            ".plugin",
+            in_sign="sssss",
+            out_sign="",
+            method=self._invite,
+            async_=True,
+        )
+        host.bridge.addMethod(
+            "eventInviteByEmail",
+            ".plugin",
+            in_sign="ssssassssssss",
+            out_sign="",
+            method=self._invite_by_email,
+            async_=True,
+        )
+
+    def getHandler(self, client):
+        return EventsHandler(self)
+
+    def _parseEventElt(self, event_elt):
+        """Helper method to parse event element
+
+        @param (domish.Element): event_elt
+        @return (tuple[int, dict[unicode, unicode]): timestamp, event_data
+        """
+        try:
+            timestamp = date_utils.date_parse(next(event_elt.elements(NS_EVENT, "date")))
+        except StopIteration:
+            timestamp = -1
+
+        data = {}
+
+        for key in ("name",):
+            try:
+                data[key] = event_elt[key]
+            except KeyError:
+                continue
+
+        for elt_name in ("description",):
+            try:
+                elt = next(event_elt.elements(NS_EVENT, elt_name))
+            except StopIteration:
+                continue
+            else:
+                data[elt_name] = str(elt)
+
+        for elt_name in ("image", "background-image"):
+            try:
+                image_elt = next(event_elt.elements(NS_EVENT, elt_name))
+                data[elt_name] = image_elt["src"]
+            except StopIteration:
+                continue
+            except KeyError:
+                log.warning(_("no src found for image"))
+
+        for uri_type in ("invitees", "blog"):
+            try:
+                elt = next(event_elt.elements(NS_EVENT, uri_type))
+                uri = data[uri_type + "_uri"] = elt["uri"]
+                uri_data = xmpp_uri.parseXMPPUri(uri)
+                if uri_data["type"] != "pubsub":
+                    raise ValueError
+            except StopIteration:
+                log.warning(_("no {uri_type} element found!").format(uri_type=uri_type))
+            except KeyError:
+                log.warning(_("incomplete {uri_type} element").format(uri_type=uri_type))
+            except ValueError:
+                log.warning(_("bad {uri_type} element").format(uri_type=uri_type))
+            else:
+                data[uri_type + "_service"] = uri_data["path"]
+                data[uri_type + "_node"] = uri_data["node"]
+
+        for meta_elt in event_elt.elements(NS_EVENT, "meta"):
+            key = meta_elt["name"]
+            if key in data:
+                log.warning(
+                    "Ignoring conflicting meta element: {xml}".format(
+                        xml=meta_elt.toXml()
+                    )
+                )
+                continue
+            data[key] = str(meta_elt)
+        if event_elt.link:
+            link_elt = event_elt.link
+            data["service"] = link_elt["service"]
+            data["node"] = link_elt["node"]
+            data["item"] = link_elt["item"]
+        if event_elt.getAttribute("creator") == "true":
+            data["creator"] = True
+        return timestamp, data
+
+    def event_elt_2_event_data(self, event_elt: domish.Element) -> Dict[str, Any]:
+        """Convert <event/> element to event data
+
+        @param event_elt: <event/> element
+            parent <item/> element can also be used
+        @raise exceptions.NotFound: can't find event payload
+        @raise ValueError: something is missing or badly formed
+        """
+        if event_elt.name == "item":
+            try:
+                event_elt = next(event_elt.elements(NS_EVENTS, "event"))
+            except StopIteration:
+                raise exceptions.NotFound("<event/> 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("<name/> elements don't have distinct xml:lang")
+            name_data[lang] = str(name_elt)
+
+        if not name_data:
+            raise exceptions.NotFound("<name/> element is missing")
+
+        # start
+
+        try:
+            start_elt = next(event_elt.elements(NS_EVENTS, "start"))
+        except StopIteration:
+            raise exceptions.NotFound("<start/> element is missing")
+        event_data["start"] = utils.parse_xmpp_date(str(start_elt))
+
+        # end
+
+        try:
+            end_elt = next(event_elt.elements(NS_EVENTS, "end"))
+        except StopIteration:
+            raise exceptions.NotFound("<end/> element is missing")
+        event_data["end"] = utils.parse_xmpp_date(str(end_elt))
+
+        # head-picture
+
+        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)
+
+        # 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(
+                    "<description/> 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(
+                    "<category/> 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("<location/> 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("<rsvp/> 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 <external/> 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)
+        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)
+            )
+        )
+        d.addCallback(data_format.serialise)
+        return d
+
+    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
+
+        @param service: pubsub service
+        @param node: pubsub node
+        @param event_id: pubsub item ID
+        @return: event data:
+        """
+        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:
+                events.append(self.event_elt_2_event_data((item)))
+            except (ValueError, exceptions.NotFound):
+                log.warning(
+                    f"Can't parse event for item {item['id']}: {item.toXml()}"
+                )
+
+        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)
+        return defer.ensureDeferred(
+            self.event_create(
+                client,
+                data_format.deserialise(data_s),
+                jid.JID(service) if service else None,
+                node or None,
+                event_id or 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 <reaction> 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: 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 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:
+                rsvp = attachment["rsvp"]
+            except KeyError:
+                continue
+            ret[attachment["from"]] = rsvp
+
+        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.event_invitee_set(
+                client,
+                jid.JID(service) if service else None,
+                node or None,
+                item,
+                data_format.deserialise(rsvp_s)
+            )
+        )
+
+    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: 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)
+        """
+        if service is None:
+            service = client.jid.userhostJID()
+        if node is None:
+            node = NS_EVENTS
+        await self._a.set_attachements(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
+        node = node if node else NS_EVENT
+        client = self.host.getClient(profile_key)
+        return defer.ensureDeferred(
+            self.eventInviteesList(client, service, node)
+        )
+
+    async def eventInviteesList(self, client, service, node):
+        """Retrieve attendance from event node
+
+        @param service(unicode, None): PubSub service
+        @param node(unicode): PubSub node of the event
+        @return (dict): a dict with current attendance status,
+            an empty dict is returned if nothing has been answered yed
+        """
+        items, metadata = await self._p.getItems(client, service, node)
+        invitees = {}
+        for item in items:
+            try:
+                event_elt = next(item.elements(NS_EVENT, "invitee"))
+            except StopIteration:
+                # no item found, event data are not set yet
+                log.warning(_(
+                    "no data found for {item_id} (service: {service}, node: {node})"
+                    .format(item_id=item["id"], service=service, node=node)))
+            else:
+                data = {}
+                for key in ("attend", "guests"):
+                    try:
+                        data[key] = event_elt[key]
+                    except KeyError:
+                        continue
+                invitees[item["id"]] = data
+        return invitees
+
+    async def invitePreflight(
+        self,
+        client: SatXMPPEntity,
+        invitee_jid: jid.JID,
+        service: jid.JID,
+        node: str,
+        item_id: Optional[str] = None,
+        name: str = '',
+        extra: Optional[dict] = None,
+    ) -> None:
+        if self._b is None:
+            raise exceptions.FeatureNotFound(
+                _('"XEP-0277" (blog) plugin is needed for this feature')
+            )
+        if item_id is None:
+            item_id = extra["default_item_id"] = NS_EVENT
+
+        __, 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"]
+        blog_service = jid.JID(event_data["blog_service"])
+        blog_node = event_data["blog_node"]
+        await self._p.setNodeAffiliations(
+            client, invitees_service, invitees_node, {invitee_jid: "publisher"}
+        )
+        log.debug(
+            f"affiliation set on invitee node (jid: {invitees_service}, "
+            f"node: {invitees_node!r})"
+        )
+        await self._p.setNodeAffiliations(
+            client, blog_service, blog_node, {invitee_jid: "member"}
+        )
+        blog_items, __ = await self._b.mbGet(client, blog_service, blog_node, None)
+
+        for item in blog_items:
+            try:
+                comments_service = jid.JID(item["comments_service"])
+                comments_node = item["comments_node"]
+            except KeyError:
+                log.debug(
+                    "no comment service set for item {item_id}".format(
+                        item_id=item["id"]
+                    )
+                )
+            else:
+                await self._p.setNodeAffiliations(
+                    client, comments_service, comments_node, {invitee_jid: "publisher"}
+                )
+        log.debug(_("affiliation set on blog and comments nodes"))
+
+    def _invite(self, invitee_jid, service, node, item_id, profile):
+        return self.host.plugins["PUBSUB_INVITATION"]._sendPubsubInvitation(
+            invitee_jid, service, node, item_id or NS_EVENT, profile_key=profile
+        )
+
+    def _invite_by_email(self, service, node, id_=NS_EVENT, email="", emails_extra=None,
+                       name="", host_name="", language="", url_template="",
+                       message_subject="", message_body="",
+                       profile_key=C.PROF_KEY_NONE):
+        client = self.host.getClient(profile_key)
+        kwargs = {
+            "profile": client.profile,
+            "emails_extra": [str(e) for e in emails_extra],
+        }
+        for key in (
+            "email",
+            "name",
+            "host_name",
+            "language",
+            "url_template",
+            "message_subject",
+            "message_body",
+        ):
+            value = locals()[key]
+            kwargs[key] = str(value)
+        return defer.ensureDeferred(self.invite_by_email(
+            client, jid.JID(service) if service else None, node, id_ or NS_EVENT, **kwargs
+        ))
+
+    async def invite_by_email(self, client, service, node, id_=NS_EVENT, **kwargs):
+        """High level method to create an email invitation to an event
+
+        @param service(unicode, None): PubSub service
+        @param node(unicode): PubSub node of the event
+        @param id_(unicode): id_ with even data
+        """
+        if self._i is None:
+            raise exceptions.FeatureNotFound(
+                _('"Invitations" plugin is needed for this feature')
+            )
+        if self._b is None:
+            raise exceptions.FeatureNotFound(
+                _('"XEP-0277" (blog) plugin is needed for this feature')
+            )
+        service = service or client.jid.userhostJID()
+        event_uri = xmpp_uri.buildXMPPUri(
+            "pubsub", path=service.full(), node=node, item=id_
+        )
+        kwargs["extra"] = {"event_uri": event_uri}
+        invitation_data = await self._i.create(**kwargs)
+        invitee_jid = invitation_data["jid"]
+        log.debug(_("invitation created"))
+        # now that we have a jid, we can send normal invitation
+        await self.invite(client, invitee_jid, service, node, id_)
+
+    def on_invitation_preflight(
+        self,
+        client: SatXMPPEntity,
+        name: str,
+        extra: dict,
+        service: jid.JID,
+        node: str,
+        item_id: Optional[str],
+        item_elt: domish.Element
+    ) -> None:
+        event_elt = item_elt.event
+        link_elt = event_elt.addElement("link")
+        link_elt["service"] = service.full()
+        link_elt["node"] = node
+        link_elt["item"] = item_id
+        __, event_data = self._parseEventElt(event_elt)
+        try:
+            name = event_data["name"]
+        except KeyError:
+            pass
+        else:
+            extra["name"] = name
+        if 'image' in event_data:
+            extra["thumb_url"] = event_data['image']
+        extra["element"] = event_elt
+
+
+@implementer(iwokkel.IDisco)
+class EventsHandler(XMPPHandler):
+
+    def __init__(self, plugin_parent):
+        self.plugin_parent = plugin_parent
+
+    def getDiscoInfo(self, requestor, target, nodeIdentifier=""):
+        return [
+            disco.DiscoFeature(NS_EVENTS),
+        ]
+
+    def getDiscoItems(self, requestor, target, nodeIdentifier=""):
+        return []