changeset 2912:a3faf1c86596

plugin events: refactored invitation and personal lists logic: - invitation logic has been moved to a new generic "plugin_exp_invitation" plugin - plugin_misc_invitations has be rename "plugin_exp_email_invitation" to avoid confusion - personal event list has be refactored to use a new experimental "list of interest", which regroup all interestings items, events or other ones
author Goffi <goffi@goffi.org>
date Sun, 14 Apr 2019 08:21:51 +0200
parents cd391ea847cb
children 672e6be3290f
files sat/plugins/plugin_exp_events.py sat/plugins/plugin_exp_invitation.py sat/plugins/plugin_exp_list_of_interest.py sat/plugins/plugin_misc_email_invitation.py sat/plugins/plugin_misc_invitations.py
diffstat 5 files changed, 793 insertions(+), 541 deletions(-) [+]
line wrap: on
line diff
--- a/sat/plugins/plugin_exp_events.py	Sun Apr 14 08:21:51 2019 +0200
+++ b/sat/plugins/plugin_exp_events.py	Sun Apr 14 08:21:51 2019 +0200
@@ -17,12 +17,11 @@
 # 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/>.
 
+import shortuuid
 from sat.core.i18n import _
 from sat.core import exceptions
 from sat.core.constants import Const as C
 from sat.core.log import getLogger
-
-log = getLogger(__name__)
 from sat.tools import utils
 from sat.tools.common import uri as xmpp_uri
 from sat.tools.common import date_utils
@@ -32,29 +31,24 @@
 from wokkel import disco, iwokkel
 from zope.interface import implements
 from twisted.words.protocols.jabber.xmlstream import XMPPHandler
+from wokkel import pubsub
 
-from wokkel import pubsub
-import shortuuid
+log = getLogger(__name__)
 
 
 PLUGIN_INFO = {
-    C.PI_NAME: "Event plugin",
+    C.PI_NAME: "Events",
     C.PI_IMPORT_NAME: "EVENTS",
     C.PI_TYPE: "EXP",
     C.PI_PROTOCOLS: [],
-    C.PI_DEPENDENCIES: ["XEP-0060"],
-    C.PI_RECOMMENDATIONS: ["INVITATIONS", "XEP-0277"],
+    C.PI_DEPENDENCIES: [u"XEP-0060", u"INVITATION", u"LIST_INTEREST"],
+    C.PI_RECOMMENDATIONS: ["XEP-0277", "EMAIL_INVITATION"],
     C.PI_MAIN: "Events",
     C.PI_HANDLER: "yes",
-    C.PI_DESCRIPTION: _("""Experimental implementation of XMPP events management"""),
+    C.PI_DESCRIPTION: _(u"""Experimental implementation of XMPP events management"""),
 }
 
 NS_EVENT = "org.salut-a-toi.event:0"
-NS_EVENT_LIST = NS_EVENT + "#list"
-NS_EVENT_INVIT = NS_EVENT + "#invitation"
-INVITATION = '/message[@type="chat"]/invitation[@xmlns="{ns_invit}"]'.format(
-    ns_invit=NS_EVENT_INVIT
-)
 
 
 class Events(object):
@@ -64,8 +58,10 @@
         log.info(_(u"Event plugin initialization"))
         self.host = host
         self._p = self.host.plugins["XEP-0060"]
-        self._i = self.host.plugins.get("INVITATIONS")
+        self._i = self.host.plugins.get("EMAIL_INVITATION")
         self._b = self.host.plugins.get("XEP-0277")
+        self.host.plugins[u"INVITATION"].registerNamespace(NS_EVENT,
+                                                           self.register)
         host.bridge.addMethod(
             "eventGet",
             ".plugin",
@@ -229,11 +225,12 @@
         items, metadata = yield self._p.getItems(client, service, node, item_ids=[id_])
         try:
             event_elt = next(items[0].elements(NS_EVENT, u"event"))
+        except StopIteration:
+            raise exceptions.NotFound(_(u"No event element has been found"))
         except IndexError:
             raise exceptions.NotFound(_(u"No event with this id has been found"))
         defer.returnValue(event_elt)
 
-    @defer.inlineCallbacks
     def register(self, client, service, node, event_id, event_elt, creator=False):
         """register evenement in personal events list
 
@@ -244,32 +241,13 @@
             note that this element will be modified in place
         @param creator(bool): True if client's profile is the creator of the node
         """
-        # we save a link to the event in our local list
-        try:
-            # TODO: check auto-create, no need to create node first if available
-            options = {self._p.OPT_ACCESS_MODEL: self._p.ACCESS_WHITELIST}
-            yield self._p.createNode(
-                client,
-                client.jid.userhostJID(),
-                nodeIdentifier=NS_EVENT_LIST,
-                options=options,
-            )
-        except error.StanzaError as e:
-            if e.condition == u"conflict":
-                log.debug(_(u"requested node already exists"))
-        link_elt = event_elt.addElement((NS_EVENT_LIST, "link"))
+        link_elt = event_elt.addElement("link")
         link_elt["service"] = service.full()
         link_elt["node"] = node
         link_elt["item"] = event_id
-        item_id = xmpp_uri.buildXMPPUri(
-            u"pubsub", path=service.full(), node=node, item=event_id
-        )
-        if creator:
-            event_elt["creator"] = "true"
-        item_elt = pubsub.Item(id=item_id, payload=event_elt)
-        yield self._p.publish(
-            client, client.jid.userhostJID(), NS_EVENT_LIST, items=[item_elt]
-        )
+        return self.host.plugins[u'LIST_INTEREST'].registerPubsub(
+            client, NS_EVENT, service, node, event_id, creator,
+            element=event_elt)
 
     def _eventGet(self, service, node, id_=u"", profile_key=C.PROF_KEY_NONE):
         service = jid.JID(service) if service else None
@@ -396,17 +374,11 @@
             yield self.register(client, service, node, event_id, event_elt, creator=True)
         defer.returnValue(node)
 
-    def _eventModify(
-        self,
-        service,
-        node,
-        id_,
-        timestamp_update,
-        data_update,
-        profile_key=C.PROF_KEY_NONE,
-    ):
+    def _eventModify(self, service, node, id_, timestamp_update, data_update,
+                     profile_key=C.PROF_KEY_NONE):
         service = jid.JID(service) if service else None
-        node = node if node else NS_EVENT
+        if not node:
+            raise ValueError(_(u"missing node"))
         client = self.host.getClient(profile_key)
         return self.eventModify(
             client, service, node, id_ or NS_EVENT, timestamp_update or None, data_update
@@ -444,24 +416,24 @@
         return d
 
     @defer.inlineCallbacks
-    def eventsList(self, client, service, node):
+    def eventsList(self, client, service, node=None):
         """Retrieve list of registered events
 
         @return list(tuple(int, dict)): list of events (timestamp + metadata)
         """
-        if not node:
-            node = NS_EVENT_LIST
-        items = yield self._p.getItems(client, service, node)
+        items, metadata = yield self.host.plugins[u'LIST_INTEREST'].listInterests(
+            client, service, node, namespace=NS_EVENT)
         events = []
-        for item in items[0]:
+        for item in items:
             try:
-                event_elt = next(item.elements(NS_EVENT, u"event"))
+                event_elt = next(item.interest.pubsub.elements(NS_EVENT, u"event"))
             except IndexError:
-                log.error(
+                log.warning(
                     _(u"No event found in item {item_id}").format(item_id=item["id"])
                 )
-            timestamp, data = self._parseEventElt(event_elt)
-            events.append((timestamp, data))
+            else:
+                timestamp, data = self._parseEventElt(event_elt)
+                events.append((timestamp, data))
         defer.returnValue(events)
 
     def _eventInviteeGet(self, service, node, profile_key):
@@ -479,12 +451,12 @@
         @return (dict): a dict with current attendance status,
             an empty dict is returned if nothing has been answered yed
         """
-        items, metadata = yield self._p.getItems(
-            client, service, node, item_ids=[client.jid.userhost()]
-        )
         try:
+            items, metadata = yield self._p.getItems(
+                client, service, node, item_ids=[client.jid.userhost()]
+            )
             event_elt = next(items[0].elements(NS_EVENT, u"invitee"))
-        except IndexError:
+        except (exceptions.NotFound, IndexError):
             # no item found, event data are not set yet
             defer.returnValue({})
         data = {}
@@ -542,13 +514,9 @@
                 event_elt = next(item.elements(NS_EVENT, u"invitee"))
             except StopIteration:
                 # no item found, event data are not set yet
-                log.warning(
-                    _(
-                        u"no data found for {item_id} (service: {service}, node: {node})".format(
-                            item_id=item["id"], service=service, node=node
-                        )
-                    )
-                )
+                log.warning(_(
+                    u"no data found for {item_id} (service: {service}, node: {node})"
+                    .format(item_id=item["id"], service=service, node=node)))
             else:
                 data = {}
                 for key in (u"attend", u"guests"):
@@ -559,30 +527,6 @@
                 invitees[item["id"]] = data
         defer.returnValue(invitees)
 
-    def sendMessageInvitation(self, client, invitee_jid, service, node, item_id):
-        """Send an invitation in a <message> stanza
-
-        @param invitee_jid(jid.JID): entitee to send invitation to
-        @param service(jid.JID): pubsub service of the event
-        @param node(unicode): node of the event
-        @param item_id(unicode): id of the event
-        """
-        mess_data = {
-            "from": client.jid,
-            "to": invitee_jid,
-            "uid": "",
-            "message": {},
-            "type": C.MESS_TYPE_CHAT,
-            "subject": {},
-            "extra": {},
-        }
-        client.generateMessageXML(mess_data)
-        event_elt = mess_data["xml"].addElement("invitation", NS_EVENT_INVIT)
-        event_elt["service"] = service.full()
-        event_elt["node"] = node
-        event_elt["item"] = item_id
-        client.send(mess_data["xml"])
-
     def _invite(self, invitee_jid, service, node, item_id, profile):
         client = self.host.getClient(profile)
         service = jid.JID(service) if service else None
@@ -644,23 +588,14 @@
         log.debug(_(u"affiliation set on blog and comments nodes"))
 
         # now we send the invitation
-        self.sendMessageInvitation(client, invitee_jid, service, node, item_id)
+        pubsub_invitation = self.host.plugins[u'PUBSUB_INVITATION']
+        pubsub_invitation.sendPubsubInvitation(client, invitee_jid, service, node,
+                                                item_id)
 
-    def _inviteByEmail(
-        self,
-        service,
-        node,
-        id_=NS_EVENT,
-        email=u"",
-        emails_extra=None,
-        name=u"",
-        host_name=u"",
-        language=u"",
-        url_template=u"",
-        message_subject=u"",
-        message_body=u"",
-        profile_key=C.PROF_KEY_NONE,
-    ):
+    def _inviteByEmail(self, service, node, id_=NS_EVENT, email=u"", emails_extra=None,
+                       name=u"", host_name=u"", language=u"", url_template=u"",
+                       message_subject=u"", message_body=u"",
+                       profile_key=C.PROF_KEY_NONE):
         client = self.host.getClient(profile_key)
         kwargs = {
             u"profile": client.profile,
@@ -708,19 +643,6 @@
         # now that we have a jid, we can send normal invitation
         yield self.invite(client, invitee_jid, service, node, id_)
 
-    @defer.inlineCallbacks
-    def onInvitation(self, message_elt, client):
-        invitation_elt = message_elt.invitation
-        try:
-            service = jid.JID(invitation_elt["service"])
-            node = invitation_elt["node"]
-            event_id = invitation_elt["item"]
-        except (RuntimeError, KeyError):
-            log.warning(_(u"Bad invitation: {xml}").format(xml=message_elt.toXml()))
-
-        event_elt = yield self.getEventElement(client, service, node, event_id)
-        yield self.register(client, service, node, event_id, event_elt, creator=False)
-
 
 class EventsHandler(XMPPHandler):
     implements(iwokkel.IDisco)
@@ -728,20 +650,9 @@
     def __init__(self, plugin_parent):
         self.plugin_parent = plugin_parent
 
-    @property
-    def host(self):
-        return self.plugin_parent.host
-
-    def connectionInitialized(self):
-        self.xmlstream.addObserver(
-            INVITATION, self.plugin_parent.onInvitation, client=self.parent
-        )
-
     def getDiscoInfo(self, requestor, target, nodeIdentifier=""):
         return [
             disco.DiscoFeature(NS_EVENT),
-            disco.DiscoFeature(NS_EVENT_LIST),
-            disco.DiscoFeature(NS_EVENT_INVIT),
         ]
 
     def getDiscoItems(self, requestor, target, nodeIdentifier=""):
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/sat/plugins/plugin_exp_invitation.py	Sun Apr 14 08:21:51 2019 +0200
@@ -0,0 +1,187 @@
+#!/usr/bin/env python2
+# -*- coding: utf-8 -*-
+
+# SAT plugin to detect language (experimental)
+# Copyright (C) 2009-2019 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 sat.core.i18n import _
+from sat.core import exceptions
+from sat.core.constants import Const as C
+from sat.core.log import getLogger
+from twisted.internet import defer
+from twisted.words.protocols.jabber import jid
+from wokkel import disco, iwokkel
+from zope.interface import implements
+from twisted.words.protocols.jabber.xmlstream import XMPPHandler
+
+log = getLogger(__name__)
+
+
+PLUGIN_INFO = {
+    C.PI_NAME: "Invitation",
+    C.PI_IMPORT_NAME: "INVITATION",
+    C.PI_TYPE: "EXP",
+    C.PI_PROTOCOLS: [],
+    C.PI_DEPENDENCIES: ["XEP-0060"],
+    C.PI_RECOMMENDATIONS: [],
+    C.PI_MAIN: "Invitation",
+    C.PI_HANDLER: "yes",
+    C.PI_DESCRIPTION: _(u"Experimental handling of invitations"),
+}
+
+NS_INVITATION = u"https://salut-a-toi/protocol/invitation:0"
+INVITATION = '/message/invitation[@xmlns="{ns_invit}"]'.format(
+    ns_invit=NS_INVITATION
+)
+NS_INVITATION_LIST = NS_INVITATION + u"#list"
+
+
+class Invitation(object):
+
+    def __init__(self, host):
+        log.info(_(u"Invitation plugin initialization"))
+        self.host = host
+        self._p = self.host.plugins["XEP-0060"]
+        # map from namespace of the invitation to callback handling it
+        self._ns_cb = {}
+
+    def getHandler(self, client):
+        return PubsubInvitationHandler(self)
+
+    def registerNamespace(self, namespace, callback):
+        """Register a callback for a namespace
+
+        @param namespace(unicode): namespace handled
+        @param callback(callbable): method handling the invitation
+            For pubsub invitation, it will be called with following arguments:
+                - client
+                - service(jid.JID): pubsub service jid
+                - node(unicode): pubsub node
+                - item_id(unicode, None): pubsub item id
+                - item_elt(domish.Element): item of the invitation
+        @raise exceptions.ConflictError: this namespace is already registered
+        """
+        if namespace in self._ns_cb:
+            raise exceptions.ConflictError(
+                u"invitation namespace {namespace} is already register with {callback}"
+                .format(namespace=namespace, callback=self._ns_cb[namespace]))
+        self._ns_cb[namespace] = callback
+
+    def sendPubsubInvitation(self, client, invitee_jid, service, node,
+                              item_id):
+        """Send an invitation in a <message> stanza
+
+        @param invitee_jid(jid.JID): entitee to send invitation to
+        @param service(jid.JID): pubsub service
+        @param node(unicode): pubsub node
+        @param item_id(unicode): pubsub id
+        """
+        mess_data = {
+            "from": client.jid,
+            "to": invitee_jid,
+            "uid": "",
+            "message": {},
+            "type": C.MESS_TYPE_CHAT,
+            "subject": {},
+            "extra": {},
+        }
+        client.generateMessageXML(mess_data)
+        invitation_elt = mess_data["xml"].addElement("invitation", NS_INVITATION)
+        pubsub_elt = invitation_elt.addElement(u"pubsub")
+        pubsub_elt[u"service"] = service.full()
+        pubsub_elt[u"node"] = node
+        pubsub_elt[u"item"] = item_id
+        client.send(mess_data[u"xml"])
+
+    @defer.inlineCallbacks
+    def _parsePubsubElt(self, client, pubsub_elt):
+        try:
+            service = jid.JID(pubsub_elt["service"])
+            node = pubsub_elt["node"]
+            item_id = pubsub_elt.getAttribute("item")
+        except (RuntimeError, KeyError):
+            log.warning(_(u"Bad invitation, ignoring"))
+            raise exceptions.DataError
+
+        try:
+            items, metadata = yield self._p.getItems(client, service, node,
+                                                     item_ids=[item_id])
+        except Exception as e:
+            log.warning(_(u"Can't get item linked with invitation: {reason}").format(
+                        reason=e))
+        try:
+            item_elt = items[0]
+        except IndexError:
+            log.warning(_(u"Invitation was linking to a non existing item"))
+            raise exceptions.DataError
+
+        try:
+            namespace = item_elt.firstChildElement().uri
+        except Exception as e:
+            log.warning(_(u"Can't retrieve namespace of invitation: {reason}").format(
+                reason = e))
+            raise exceptions.DataError
+
+        args = [service, node, item_id, item_elt]
+        defer.returnValue((namespace, args))
+
+    @defer.inlineCallbacks
+    def onInvitation(self, message_elt, client):
+        invitation_elt = message_elt.invitation
+        for elt in invitation_elt.elements():
+            if elt.uri != NS_INVITATION:
+                log.warning(u"unexpected element: {xml}".format(xml=elt.toXml()))
+                continue
+            if elt.name == u"pubsub":
+                method = self._parsePubsubElt
+            else:
+                log.warning(u"not implemented invitation element: {xml}".format(
+                    xml = elt.toXml()))
+                continue
+            try:
+                namespace, args = yield method(client, elt)
+            except exceptions.DataError:
+                log.warning(u"Can't parse invitation element: {xml}".format(
+                            xml = elt.toXml()))
+                continue
+
+            try:
+                cb = self._ns_cb[namespace]
+            except KeyError:
+                log.warning(_(u'No handler for namespace "{namespace}", invitation ignored')
+                    .format(namespace=namespace))
+            else:
+                cb(client, *args)
+
+
+class PubsubInvitationHandler(XMPPHandler):
+    implements(iwokkel.IDisco)
+
+    def __init__(self, plugin_parent):
+        self.plugin_parent = plugin_parent
+
+    def connectionInitialized(self):
+        self.xmlstream.addObserver(
+            INVITATION, self.plugin_parent.onInvitation, client=self.parent
+        )
+
+    def getDiscoInfo(self, requestor, target, nodeIdentifier=""):
+        return [
+            disco.DiscoFeature(NS_INVITATION),
+        ]
+
+    def getDiscoItems(self, requestor, target, nodeIdentifier=""):
+        return []
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/sat/plugins/plugin_exp_list_of_interest.py	Sun Apr 14 08:21:51 2019 +0200
@@ -0,0 +1,154 @@
+#!/usr/bin/env python2
+# -*- coding: utf-8 -*-
+
+# SAT plugin to detect language (experimental)
+# Copyright (C) 2009-2019 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 sat.core.i18n import _
+from sat.core.constants import Const as C
+from sat.core.log import getLogger
+from wokkel import disco, iwokkel, pubsub
+from zope.interface import implements
+from twisted.internet import defer
+from twisted.words.protocols.jabber import error as jabber_error
+from twisted.words.protocols.jabber.xmlstream import XMPPHandler
+from twisted.words.xish import domish
+
+log = getLogger(__name__)
+
+
+PLUGIN_INFO = {
+    C.PI_NAME: "List of Interest",
+    C.PI_IMPORT_NAME: "LIST_INTEREST",
+    C.PI_TYPE: "EXP",
+    C.PI_PROTOCOLS: [],
+    C.PI_DEPENDENCIES: ["XEP-0060"],
+    C.PI_RECOMMENDATIONS: [],
+    C.PI_MAIN: "ListInterest",
+    C.PI_HANDLER: "yes",
+    C.PI_DESCRIPTION: _(u"Experimental handling of interesting XMPP locations"),
+}
+
+NS_LIST_INTEREST = "https://salut-a-toi/protocol/list-interest:0"
+
+
+class ListInterest(object):
+    namespace = NS_LIST_INTEREST
+
+    def __init__(self, host):
+        log.info(_(u"List of Interest plugin initialization"))
+        self.host = host
+        self._p = self.host.plugins["XEP-0060"]
+
+    def getHandler(self, client):
+        return ListInterestHandler(self)
+
+    @defer.inlineCallbacks
+    def createNode(self, client):
+        try:
+            # TODO: check auto-create, no need to create node first if available
+            options = {self._p.OPT_ACCESS_MODEL: self._p.ACCESS_WHITELIST}
+            yield self._p.createNode(
+                client,
+                client.jid.userhostJID(),
+                nodeIdentifier=NS_LIST_INTEREST,
+                options=options,
+            )
+        except jabber_error.StanzaError as e:
+            if e.condition == u"conflict":
+                log.debug(_(u"requested node already exists"))
+
+    @defer.inlineCallbacks
+    def registerPubsub(self, client, namespace, service, node, item_id=None,
+                       creator=False, name=None, element=None):
+        """Register an interesting element in personal list
+
+        @param namespace(unicode): namespace of the interest
+            this is used as a cache, to avoid the need to retrieve the item only to get
+            its namespace
+        @param service(jid.JID): pubsub service of the
+        @param node(unicode): target pubsub node
+        @param item_id(unicode, None): target pubsub id
+        @param creator(bool): True if client's profile is the creator of the node
+            This is used a cache, to avoid the need to retrieve affiliations
+        @param name(unicode, None): name of the interest
+        @param element(domish.Element, None): element to attach
+            may be used to cache some extra data
+        """
+        yield self.createNode(client)
+        interest_elt = domish.Element((NS_LIST_INTEREST, u"interest"))
+        interest_elt[u"namespace"] = namespace
+        if name is not None:
+            interest_elt[u'name'] = name
+        pubsub_elt = interest_elt.addElement(u"pubsub")
+        pubsub_elt[u"service"] = service.full()
+        pubsub_elt[u"node"] = node
+        if item_id is not None:
+            pubsub_elt[u"item"] = item_id
+        if creator:
+            pubsub_elt[u"creator"] = C.BOOL_TRUE
+        if element is not None:
+            pubsub_elt.addChild(element)
+        item_elt = pubsub.Item(payload=interest_elt)
+        yield self._p.publish(
+            client, client.jid.userhostJID(), NS_LIST_INTEREST, items=[item_elt]
+        )
+
+    @defer.inlineCallbacks
+    def listInterests(self, client, service=None, node=None, namespace=None):
+        """Retrieve list of interests
+
+        @param service(jid.JID, None): service to use
+            None to use own PEP
+        @param node(unicode, None): node to use
+            None to use default node
+        @param namespace(unicode, None): filter interests of this namespace
+            None to retrieve all interests
+        @return: same as [XEP_0060.getItems]
+        """
+        # TODO: if a MAM filter were available, it would improve performances
+        if not node:
+            node = NS_LIST_INTEREST
+        items, metadata = yield self._p.getItems(client, service, node)
+        if namespace is not None:
+            filtered_items = []
+            for item in items:
+                try:
+                    interest_elt = next(item.elements(NS_LIST_INTEREST, u"interest"))
+                except StopIteration:
+                    log.warning(_(u"Missing interest element: {xml}").format(
+                        xml=interest_elt.toXml()))
+                    continue
+                if interest_elt.getAttribute(u"namespace") == namespace:
+                    filtered_items.append(item)
+            items = filtered_items
+
+        defer.returnValue((items, metadata))
+
+
+class ListInterestHandler(XMPPHandler):
+    implements(iwokkel.IDisco)
+
+    def __init__(self, plugin_parent):
+        self.plugin_parent = plugin_parent
+
+    def getDiscoInfo(self, requestor, target, nodeIdentifier=""):
+        return [
+            disco.DiscoFeature(NS_LIST_INTEREST),
+        ]
+
+    def getDiscoItems(self, requestor, target, nodeIdentifier=""):
+        return []
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/sat/plugins/plugin_misc_email_invitation.py	Sun Apr 14 08:21:51 2019 +0200
@@ -0,0 +1,409 @@
+#!/usr/bin/env python2
+# -*- coding: utf-8 -*-
+
+# SAT plugin for file tansfer
+# Copyright (C) 2009-2019 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/>.
+
+import shortuuid
+from twisted.internet import defer
+from twisted.words.protocols.jabber import jid
+from twisted.words.protocols.jabber import error
+from sat.core.i18n import _, D_
+from sat.core.constants import Const as C
+from sat.core import exceptions
+from sat.core.log import getLogger
+from sat.tools import utils
+from sat.tools.common import data_format
+from sat.memory import persistent
+from sat.tools import email as sat_email
+
+log = getLogger(__name__)
+
+
+PLUGIN_INFO = {
+    C.PI_NAME: "Invitations",
+    C.PI_IMPORT_NAME: "EMAIL_INVITATION",
+    C.PI_TYPE: C.PLUG_TYPE_MISC,
+    C.PI_DEPENDENCIES: ['XEP-0077'],
+    C.PI_RECOMMENDATIONS: ["IDENTITY"],
+    C.PI_MAIN: "InvitationsPlugin",
+    C.PI_HANDLER: "no",
+    C.PI_DESCRIPTION: _(u"""invitation of people without XMPP account""")
+}
+
+
+SUFFIX_MAX = 5
+INVITEE_PROFILE_TPL = u"guest@@{uuid}"
+KEY_ID = u'id'
+KEY_JID = u'jid'
+KEY_CREATED = u'created'
+KEY_LAST_CONNECTION = u'last_connection'
+KEY_GUEST_PROFILE = u'guest_profile'
+KEY_PASSWORD = u'password'
+KEY_EMAILS_EXTRA = u'emails_extra'
+EXTRA_RESERVED = {KEY_ID, KEY_JID, KEY_CREATED, u'jid_', u'jid', KEY_LAST_CONNECTION,
+                  KEY_GUEST_PROFILE, KEY_PASSWORD, KEY_EMAILS_EXTRA}
+DEFAULT_SUBJECT = D_(u"You have been invited by {host_name} to {app_name}")
+DEFAULT_BODY = D_(u"""Hello {name}!
+
+You have received an invitation from {host_name} to participate to "{app_name}".
+To join, you just have to click on the following URL:
+{url}
+
+Please note that this URL should not be shared with anybody!
+If you want more details on {app_name}, you can check {app_url}.
+
+Welcome!
+""")
+
+
+class InvitationsPlugin(object):
+
+    def __init__(self, host):
+        log.info(_(u"plugin Invitations initialization"))
+        self.host = host
+        self.invitations = persistent.LazyPersistentBinaryDict(u'invitations')
+        host.bridge.addMethod("invitationCreate", ".plugin", in_sign='sasssssssssa{ss}s',
+                              out_sign='a{ss}',
+                              method=self._create,
+                              async=True)
+        host.bridge.addMethod("invitationGet", ".plugin", in_sign='s', out_sign='a{ss}',
+                              method=self.get,
+                              async=True)
+        host.bridge.addMethod("invitationModify", ".plugin", in_sign='sa{ss}b',
+                              out_sign='',
+                              method=self._modify,
+                              async=True)
+        host.bridge.addMethod("invitationList", ".plugin", in_sign='s',
+                              out_sign='a{sa{ss}}',
+                              method=self._list,
+                              async=True)
+
+    def checkExtra(self, extra):
+        if EXTRA_RESERVED.intersection(extra):
+            raise ValueError(
+                _(u"You can't use following key(s) in extra, they are reserved: {}")
+                .format(u', '.join(EXTRA_RESERVED.intersection(extra))))
+
+    def _create(self, email=u'', emails_extra=None, jid_=u'', password=u'', name=u'',
+                host_name=u'', language=u'', url_template=u'', message_subject=u'',
+                message_body=u'', extra=None, profile=u''):
+        # XXX: we don't use **kwargs here to keep arguments name for introspection with
+        #      D-Bus bridge
+        if emails_extra is None:
+            emails_extra = []
+
+        if extra is None:
+            extra = {}
+        else:
+            extra = {unicode(k): unicode(v) for k,v in extra.iteritems()}
+
+        kwargs = {"extra": extra,
+                  KEY_EMAILS_EXTRA: [unicode(e) for e in emails_extra]
+                  }
+
+        # we need to be sure that values are unicode, else they won't be pickled correctly
+        # with D-Bus
+        for key in ("jid_", "password", "name", "host_name", "email", "language",
+                    "url_template", "message_subject", "message_body", "profile"):
+            value = locals()[key]
+            if value:
+                kwargs[key] = unicode(value)
+        d = self.create(**kwargs)
+        def serialize(data):
+            data[KEY_JID] = data[KEY_JID].full()
+            return data
+        d.addCallback(serialize)
+        return d
+
+    @defer.inlineCallbacks
+    def create(self, **kwargs):
+        ur"""Create an invitation
+
+        This will create an XMPP account and a profile, and use a UUID to retrieve them.
+        The profile is automatically generated in the form guest@@[UUID], this way they
+            can be retrieved easily
+        **kwargs: keywords arguments which can have the following keys, unset values are
+                  equivalent to None:
+            jid_(jid.JID, None): jid to use for invitation, the jid will be created using
+                                 XEP-0077
+                if the jid has no user part, an anonymous account will be used (no XMPP
+                    account created in this case)
+                if None, automatically generate an account name (in the form
+                    "invitation-[random UUID]@domain.tld") (note that this UUID is not the
+                    same as the invitation one, as jid can be used publicly (leaking the
+                    UUID), and invitation UUID give access to account.
+                in case of conflict, a suffix number is added to the account until a free
+                    one if found (with a failure if SUFFIX_MAX is reached)
+            password(unicode, None): password to use (will be used for XMPP account and
+                                     profile)
+                None to automatically generate one
+            name(unicode, None): name of the invitee
+                will be set as profile identity if present
+            host_name(unicode, None): name of the host
+            email(unicode, None): email to send the invitation to
+                if None, no invitation email is sent, you can still associate email using
+                    extra
+                if email is used, extra can't have "email" key
+            language(unicode): language of the invitee (used notabily to translate the
+                               invitation)
+                TODO: not used yet
+            url_template(unicode, None): template to use to construct the invitation URL
+                use {uuid} as a placeholder for identifier
+                use None if you don't want to include URL (or if it is already specified
+                    in custom message)
+                /!\ you must put full URL, don't forget https://
+                /!\ the URL will give access to the invitee account, you should warn in
+                    message to not publish it publicly
+            message_subject(unicode, None): customised message body for the invitation
+                                            email
+                None to use default subject
+                uses the same substitution as for message_body
+            message_body(unicode, None): customised message body for the invitation email
+                None to use default body
+                use {name} as a place holder for invitee name
+                use {url} as a placeholder for the invitation url
+                use {uuid} as a placeholder for the identifier
+                use {app_name} as a placeholder for this software name
+                use {app_url} as a placeholder for this software official website
+                use {profile} as a placeholder for host's profile
+                use {host_name} as a placeholder for host's name
+            extra(dict, None): extra data to associate with the invitee
+                some keys are reserved:
+                    - created (creation date)
+                if email argument is used, "email" key can't be used
+            profile(unicode, None): profile of the host (person who is inviting)
+        @return (dict[unicode, unicode]): dictionary with:
+            - UUID associated with the invitee (key: id)
+            - filled extra dictionary, as saved in the databae
+        """
+        ## initial checks
+        extra = kwargs.pop('extra', {})
+        if set(kwargs).intersection(extra):
+            raise ValueError(
+                _(u"You can't use following key(s) in both args and extra: {}").format(
+                u', '.join(set(kwargs).intersection(extra))))
+
+        self.checkExtra(extra)
+
+        email = kwargs.pop(u'email', None)
+        emails_extra = kwargs.pop(u'emails_extra', [])
+        if not email and emails_extra:
+            raise ValueError(
+                _(u'You need to provide a main email address before using emails_extra'))
+
+        if (email is not None
+            and not 'url_template' in kwargs
+            and not 'message_body' in kwargs):
+            raise ValueError(
+                _(u"You need to provide url_template if you use default message body"))
+
+        ## uuid
+        log.info(_(u"creating an invitation"))
+        id_ = unicode(shortuuid.uuid())
+
+        ## XMPP account creation
+        password = kwargs.pop(u'password', None)
+        if password is None:
+           password = utils.generatePassword()
+        assert password
+        # XXX: password is here saved in clear in database
+        #      it is needed for invitation as the same password is used for profile
+        #      and SàT need to be able to automatically open the profile with the uuid
+        # FIXME: we could add an extra encryption key which would be used with the uuid
+        #        when the invitee is connecting (e.g. with URL). This key would not be
+        #        saved and could be used to encrypt profile password.
+        extra[KEY_PASSWORD] = password
+
+        jid_ = kwargs.pop(u'jid_', None)
+        if not jid_:
+            domain = self.host.memory.getConfig(None, 'xmpp_domain')
+            if not domain:
+                # TODO: fallback to profile's domain
+                raise ValueError(_(u"You need to specify xmpp_domain in sat.conf"))
+            jid_ = u"invitation-{uuid}@{domain}".format(uuid=shortuuid.uuid(),
+                                                        domain=domain)
+        jid_ = jid.JID(jid_)
+        if jid_.user:
+            # we don't register account if there is no user as anonymous login is then
+            # used
+            try:
+                yield self.host.plugins['XEP-0077'].registerNewAccount(jid_, password)
+            except error.StanzaError as e:
+                prefix = jid_.user
+                idx = 0
+                while e.condition == u'conflict':
+                    if idx >= SUFFIX_MAX:
+                        raise exceptions.ConflictError(_(u"Can't create XMPP account"))
+                    jid_.user = prefix + '_' + unicode(idx)
+                    log.info(_(u"requested jid already exists, trying with {}".format(
+                        jid_.full())))
+                    try:
+                        yield self.host.plugins['XEP-0077'].registerNewAccount(jid_,
+                                                                               password)
+                    except error.StanzaError as e:
+                        idx += 1
+                    else:
+                        break
+                if e.condition != u'conflict':
+                    raise e
+
+            log.info(_(u"account {jid_} created").format(jid_=jid_.full()))
+
+        ## profile creation
+
+        extra[KEY_GUEST_PROFILE] = guest_profile = INVITEE_PROFILE_TPL.format(uuid=id_)
+        # profile creation should not fail as we generate unique name ourselves
+        yield self.host.memory.createProfile(guest_profile, password)
+        yield self.host.memory.startSession(password, guest_profile)
+        yield self.host.memory.setParam("JabberID", jid_.full(), "Connection",
+                                        profile_key=guest_profile)
+        yield self.host.memory.setParam("Password", password, "Connection",
+                                        profile_key=guest_profile)
+        name = kwargs.pop(u'name', None)
+        if name is not None:
+            extra[u'name'] = name
+            try:
+                id_plugin = self.host.plugins[u'IDENTITY']
+            except KeyError:
+                pass
+            else:
+                yield self.host.connect(guest_profile, password)
+                guest_client = self.host.getClient(guest_profile)
+                yield id_plugin.setIdentity(guest_client, {u'nick': name})
+                yield self.host.disconnect(guest_profile)
+
+        ## email
+        language = kwargs.pop(u'language', None)
+        if language is not None:
+            extra[u'language'] = language.strip()
+
+        if email is not None:
+            extra[u'email'] = email
+            data_format.iter2dict(KEY_EMAILS_EXTRA, extra)
+            url_template = kwargs.pop(u'url_template', '')
+            format_args = {
+                u'uuid': id_,
+                u'app_name': C.APP_NAME,
+                u'app_url': C.APP_URL}
+
+            if name is None:
+                format_args[u'name'] = email
+            else:
+                format_args[u'name'] = name
+
+            profile = kwargs.pop(u'profile', None)
+            if profile is None:
+                format_args[u'profile'] = u''
+            else:
+                format_args[u'profile'] = extra[u'profile'] = profile
+
+            host_name = kwargs.pop(u'host_name', None)
+            if host_name is None:
+                format_args[u'host_name'] = profile or _(u"somebody")
+            else:
+                format_args[u'host_name'] = extra[u'host_name'] = host_name
+
+            invite_url = url_template.format(**format_args)
+            format_args[u'url'] = invite_url
+
+            yield sat_email.sendEmail(
+                self.host,
+                [email] + emails_extra,
+                (kwargs.pop(u'message_subject', None) or DEFAULT_SUBJECT).format(
+                    **format_args),
+                (kwargs.pop(u'message_body', None) or DEFAULT_BODY).format(**format_args),
+            )
+
+        ## extra data saving
+        self.invitations[id_] = extra
+
+        if kwargs:
+            log.warning(_(u"Not all arguments have been consumed: {}").format(kwargs))
+
+        extra[KEY_ID] = id_
+        extra[KEY_JID] = jid_
+        defer.returnValue(extra)
+
+    def get(self, id_):
+        """Retrieve invitation linked to uuid if it exists
+
+        @param id_(unicode): UUID linked to an invitation
+        @return (dict[unicode, unicode]): data associated to the invitation
+        @raise KeyError: there is not invitation with this id_
+        """
+        return self.invitations[id_]
+
+    def _modify(self, id_, new_extra, replace):
+        return self.modify(id_, {unicode(k): unicode(v) for k,v in new_extra.iteritems()},
+                           replace)
+
+    def modify(self, id_, new_extra, replace=False):
+        """Modify invitation data
+
+        @param id_(unicode): UUID linked to an invitation
+        @param new_extra(dict[unicode, unicode]): data to update
+            empty values will be deleted if replace is True
+        @param replace(bool): if True replace the data
+            else update them
+        @raise KeyError: there is not invitation with this id_
+        """
+        self.checkExtra(new_extra)
+        def gotCurrentData(current_data):
+            if replace:
+                new_data = new_extra
+                for k in EXTRA_RESERVED:
+                    try:
+                        new_data[k] = current_data[k]
+                    except KeyError:
+                        continue
+            else:
+                new_data = current_data
+                for k,v in new_extra.iteritems():
+                    if k in EXTRA_RESERVED:
+                        log.warning(_(u"Skipping reserved key {key}".format(k)))
+                        continue
+                    if v:
+                        new_data[k] = v
+                    else:
+                        try:
+                            del new_data[k]
+                        except KeyError:
+                            pass
+
+            self.invitations[id_] = new_data
+
+        d = self.invitations[id_]
+        d.addCallback(gotCurrentData)
+        return d
+
+    def _list(self, profile=C.PROF_KEY_NONE):
+        return self.list(profile)
+
+    @defer.inlineCallbacks
+    def list(self, profile=C.PROF_KEY_NONE):
+        """List invitations
+
+        @param profile(unicode): return invitation linked to this profile only
+            C.PROF_KEY_NONE: don't filter invitations
+        @return list(unicode): invitations uids
+        """
+        invitations = yield self.invitations.items()
+        if profile != C.PROF_KEY_NONE:
+            invitations = {id_:data for id_, data in invitations.iteritems()
+                           if data.get(u'profile') == profile}
+
+        defer.returnValue(invitations)
--- a/sat/plugins/plugin_misc_invitations.py	Sun Apr 14 08:21:51 2019 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,409 +0,0 @@
-#!/usr/bin/env python2
-# -*- coding: utf-8 -*-
-
-# SAT plugin for file tansfer
-# Copyright (C) 2009-2019 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/>.
-
-import shortuuid
-from twisted.internet import defer
-from twisted.words.protocols.jabber import jid
-from twisted.words.protocols.jabber import error
-from sat.core.i18n import _, D_
-from sat.core.constants import Const as C
-from sat.core import exceptions
-from sat.core.log import getLogger
-from sat.tools import utils
-from sat.tools.common import data_format
-from sat.memory import persistent
-from sat.tools import email as sat_email
-
-log = getLogger(__name__)
-
-
-PLUGIN_INFO = {
-    C.PI_NAME: "Invitations",
-    C.PI_IMPORT_NAME: "INVITATIONS",
-    C.PI_TYPE: C.PLUG_TYPE_MISC,
-    C.PI_DEPENDENCIES: ['XEP-0077'],
-    C.PI_RECOMMENDATIONS: ["IDENTITY"],
-    C.PI_MAIN: "InvitationsPlugin",
-    C.PI_HANDLER: "no",
-    C.PI_DESCRIPTION: _(u"""invitation of people without XMPP account""")
-}
-
-
-SUFFIX_MAX = 5
-INVITEE_PROFILE_TPL = u"guest@@{uuid}"
-KEY_ID = u'id'
-KEY_JID = u'jid'
-KEY_CREATED = u'created'
-KEY_LAST_CONNECTION = u'last_connection'
-KEY_GUEST_PROFILE = u'guest_profile'
-KEY_PASSWORD = u'password'
-KEY_EMAILS_EXTRA = u'emails_extra'
-EXTRA_RESERVED = {KEY_ID, KEY_JID, KEY_CREATED, u'jid_', u'jid', KEY_LAST_CONNECTION,
-                  KEY_GUEST_PROFILE, KEY_PASSWORD, KEY_EMAILS_EXTRA}
-DEFAULT_SUBJECT = D_(u"You have been invited by {host_name} to {app_name}")
-DEFAULT_BODY = D_(u"""Hello {name}!
-
-You have received an invitation from {host_name} to participate to "{app_name}".
-To join, you just have to click on the following URL:
-{url}
-
-Please note that this URL should not be shared with anybody!
-If you want more details on {app_name}, you can check {app_url}.
-
-Welcome!
-""")
-
-
-class InvitationsPlugin(object):
-
-    def __init__(self, host):
-        log.info(_(u"plugin Invitations initialization"))
-        self.host = host
-        self.invitations = persistent.LazyPersistentBinaryDict(u'invitations')
-        host.bridge.addMethod("invitationCreate", ".plugin", in_sign='sasssssssssa{ss}s',
-                              out_sign='a{ss}',
-                              method=self._create,
-                              async=True)
-        host.bridge.addMethod("invitationGet", ".plugin", in_sign='s', out_sign='a{ss}',
-                              method=self.get,
-                              async=True)
-        host.bridge.addMethod("invitationModify", ".plugin", in_sign='sa{ss}b',
-                              out_sign='',
-                              method=self._modify,
-                              async=True)
-        host.bridge.addMethod("invitationList", ".plugin", in_sign='s',
-                              out_sign='a{sa{ss}}',
-                              method=self._list,
-                              async=True)
-
-    def checkExtra(self, extra):
-        if EXTRA_RESERVED.intersection(extra):
-            raise ValueError(
-                _(u"You can't use following key(s) in extra, they are reserved: {}")
-                .format(u', '.join(EXTRA_RESERVED.intersection(extra))))
-
-    def _create(self, email=u'', emails_extra=None, jid_=u'', password=u'', name=u'',
-                host_name=u'', language=u'', url_template=u'', message_subject=u'',
-                message_body=u'', extra=None, profile=u''):
-        # XXX: we don't use **kwargs here to keep arguments name for introspection with
-        #      D-Bus bridge
-        if emails_extra is None:
-            emails_extra = []
-
-        if extra is None:
-            extra = {}
-        else:
-            extra = {unicode(k): unicode(v) for k,v in extra.iteritems()}
-
-        kwargs = {"extra": extra,
-                  KEY_EMAILS_EXTRA: [unicode(e) for e in emails_extra]
-                  }
-
-        # we need to be sure that values are unicode, else they won't be pickled correctly
-        # with D-Bus
-        for key in ("jid_", "password", "name", "host_name", "email", "language",
-                    "url_template", "message_subject", "message_body", "profile"):
-            value = locals()[key]
-            if value:
-                kwargs[key] = unicode(value)
-        d = self.create(**kwargs)
-        def serialize(data):
-            data[KEY_JID] = data[KEY_JID].full()
-            return data
-        d.addCallback(serialize)
-        return d
-
-    @defer.inlineCallbacks
-    def create(self, **kwargs):
-        ur"""Create an invitation
-
-        This will create an XMPP account and a profile, and use a UUID to retrieve them.
-        The profile is automatically generated in the form guest@@[UUID], this way they
-            can be retrieved easily
-        **kwargs: keywords arguments which can have the following keys, unset values are
-                  equivalent to None:
-            jid_(jid.JID, None): jid to use for invitation, the jid will be created using
-                                 XEP-0077
-                if the jid has no user part, an anonymous account will be used (no XMPP
-                    account created in this case)
-                if None, automatically generate an account name (in the form
-                    "invitation-[random UUID]@domain.tld") (note that this UUID is not the
-                    same as the invitation one, as jid can be used publicly (leaking the
-                    UUID), and invitation UUID give access to account.
-                in case of conflict, a suffix number is added to the account until a free
-                    one if found (with a failure if SUFFIX_MAX is reached)
-            password(unicode, None): password to use (will be used for XMPP account and
-                                     profile)
-                None to automatically generate one
-            name(unicode, None): name of the invitee
-                will be set as profile identity if present
-            host_name(unicode, None): name of the host
-            email(unicode, None): email to send the invitation to
-                if None, no invitation email is sent, you can still associate email using
-                    extra
-                if email is used, extra can't have "email" key
-            language(unicode): language of the invitee (used notabily to translate the
-                               invitation)
-                TODO: not used yet
-            url_template(unicode, None): template to use to construct the invitation URL
-                use {uuid} as a placeholder for identifier
-                use None if you don't want to include URL (or if it is already specified
-                    in custom message)
-                /!\ you must put full URL, don't forget https://
-                /!\ the URL will give access to the invitee account, you should warn in
-                    message to not publish it publicly
-            message_subject(unicode, None): customised message body for the invitation
-                                            email
-                None to use default subject
-                uses the same substitution as for message_body
-            message_body(unicode, None): customised message body for the invitation email
-                None to use default body
-                use {name} as a place holder for invitee name
-                use {url} as a placeholder for the invitation url
-                use {uuid} as a placeholder for the identifier
-                use {app_name} as a placeholder for this software name
-                use {app_url} as a placeholder for this software official website
-                use {profile} as a placeholder for host's profile
-                use {host_name} as a placeholder for host's name
-            extra(dict, None): extra data to associate with the invitee
-                some keys are reserved:
-                    - created (creation date)
-                if email argument is used, "email" key can't be used
-            profile(unicode, None): profile of the host (person who is inviting)
-        @return (dict[unicode, unicode]): dictionary with:
-            - UUID associated with the invitee (key: id)
-            - filled extra dictionary, as saved in the databae
-        """
-        ## initial checks
-        extra = kwargs.pop('extra', {})
-        if set(kwargs).intersection(extra):
-            raise ValueError(
-                _(u"You can't use following key(s) in both args and extra: {}").format(
-                u', '.join(set(kwargs).intersection(extra))))
-
-        self.checkExtra(extra)
-
-        email = kwargs.pop(u'email', None)
-        emails_extra = kwargs.pop(u'emails_extra', [])
-        if not email and emails_extra:
-            raise ValueError(
-                _(u'You need to provide a main email address before using emails_extra'))
-
-        if (email is not None
-            and not 'url_template' in kwargs
-            and not 'message_body' in kwargs):
-            raise ValueError(
-                _(u"You need to provide url_template if you use default message body"))
-
-        ## uuid
-        log.info(_(u"creating an invitation"))
-        id_ = unicode(shortuuid.uuid())
-
-        ## XMPP account creation
-        password = kwargs.pop(u'password', None)
-        if password is None:
-           password = utils.generatePassword()
-        assert password
-        # XXX: password is here saved in clear in database
-        #      it is needed for invitation as the same password is used for profile
-        #      and SàT need to be able to automatically open the profile with the uuid
-        # FIXME: we could add an extra encryption key which would be used with the uuid
-        #        when the invitee is connecting (e.g. with URL). This key would not be
-        #        saved and could be used to encrypt profile password.
-        extra[KEY_PASSWORD] = password
-
-        jid_ = kwargs.pop(u'jid_', None)
-        if not jid_:
-            domain = self.host.memory.getConfig(None, 'xmpp_domain')
-            if not domain:
-                # TODO: fallback to profile's domain
-                raise ValueError(_(u"You need to specify xmpp_domain in sat.conf"))
-            jid_ = u"invitation-{uuid}@{domain}".format(uuid=shortuuid.uuid(),
-                                                        domain=domain)
-        jid_ = jid.JID(jid_)
-        if jid_.user:
-            # we don't register account if there is no user as anonymous login is then
-            # used
-            try:
-                yield self.host.plugins['XEP-0077'].registerNewAccount(jid_, password)
-            except error.StanzaError as e:
-                prefix = jid_.user
-                idx = 0
-                while e.condition == u'conflict':
-                    if idx >= SUFFIX_MAX:
-                        raise exceptions.ConflictError(_(u"Can't create XMPP account"))
-                    jid_.user = prefix + '_' + unicode(idx)
-                    log.info(_(u"requested jid already exists, trying with {}".format(
-                        jid_.full())))
-                    try:
-                        yield self.host.plugins['XEP-0077'].registerNewAccount(jid_,
-                                                                               password)
-                    except error.StanzaError as e:
-                        idx += 1
-                    else:
-                        break
-                if e.condition != u'conflict':
-                    raise e
-
-            log.info(_(u"account {jid_} created").format(jid_=jid_.full()))
-
-        ## profile creation
-
-        extra[KEY_GUEST_PROFILE] = guest_profile = INVITEE_PROFILE_TPL.format(uuid=id_)
-        # profile creation should not fail as we generate unique name ourselves
-        yield self.host.memory.createProfile(guest_profile, password)
-        yield self.host.memory.startSession(password, guest_profile)
-        yield self.host.memory.setParam("JabberID", jid_.full(), "Connection",
-                                        profile_key=guest_profile)
-        yield self.host.memory.setParam("Password", password, "Connection",
-                                        profile_key=guest_profile)
-        name = kwargs.pop(u'name', None)
-        if name is not None:
-            extra[u'name'] = name
-            try:
-                id_plugin = self.host.plugins[u'IDENTITY']
-            except KeyError:
-                pass
-            else:
-                yield self.host.connect(guest_profile, password)
-                guest_client = self.host.getClient(guest_profile)
-                yield id_plugin.setIdentity(guest_client, {u'nick': name})
-                yield self.host.disconnect(guest_profile)
-
-        ## email
-        language = kwargs.pop(u'language', None)
-        if language is not None:
-            extra[u'language'] = language.strip()
-
-        if email is not None:
-            extra[u'email'] = email
-            data_format.iter2dict(KEY_EMAILS_EXTRA, extra)
-            url_template = kwargs.pop(u'url_template', '')
-            format_args = {
-                u'uuid': id_,
-                u'app_name': C.APP_NAME,
-                u'app_url': C.APP_URL}
-
-            if name is None:
-                format_args[u'name'] = email
-            else:
-                format_args[u'name'] = name
-
-            profile = kwargs.pop(u'profile', None)
-            if profile is None:
-                format_args[u'profile'] = u''
-            else:
-                format_args[u'profile'] = extra[u'profile'] = profile
-
-            host_name = kwargs.pop(u'host_name', None)
-            if host_name is None:
-                format_args[u'host_name'] = profile or _(u"somebody")
-            else:
-                format_args[u'host_name'] = extra[u'host_name'] = host_name
-
-            invite_url = url_template.format(**format_args)
-            format_args[u'url'] = invite_url
-
-            yield sat_email.sendEmail(
-                self.host,
-                [email] + emails_extra,
-                (kwargs.pop(u'message_subject', None) or DEFAULT_SUBJECT).format(
-                    **format_args),
-                (kwargs.pop(u'message_body', None) or DEFAULT_BODY).format(**format_args),
-            )
-
-        ## extra data saving
-        self.invitations[id_] = extra
-
-        if kwargs:
-            log.warning(_(u"Not all arguments have been consumed: {}").format(kwargs))
-
-        extra[KEY_ID] = id_
-        extra[KEY_JID] = jid_
-        defer.returnValue(extra)
-
-    def get(self, id_):
-        """Retrieve invitation linked to uuid if it exists
-
-        @param id_(unicode): UUID linked to an invitation
-        @return (dict[unicode, unicode]): data associated to the invitation
-        @raise KeyError: there is not invitation with this id_
-        """
-        return self.invitations[id_]
-
-    def _modify(self, id_, new_extra, replace):
-        return self.modify(id_, {unicode(k): unicode(v) for k,v in new_extra.iteritems()},
-                           replace)
-
-    def modify(self, id_, new_extra, replace=False):
-        """Modify invitation data
-
-        @param id_(unicode): UUID linked to an invitation
-        @param new_extra(dict[unicode, unicode]): data to update
-            empty values will be deleted if replace is True
-        @param replace(bool): if True replace the data
-            else update them
-        @raise KeyError: there is not invitation with this id_
-        """
-        self.checkExtra(new_extra)
-        def gotCurrentData(current_data):
-            if replace:
-                new_data = new_extra
-                for k in EXTRA_RESERVED:
-                    try:
-                        new_data[k] = current_data[k]
-                    except KeyError:
-                        continue
-            else:
-                new_data = current_data
-                for k,v in new_extra.iteritems():
-                    if k in EXTRA_RESERVED:
-                        log.warning(_(u"Skipping reserved key {key}".format(k)))
-                        continue
-                    if v:
-                        new_data[k] = v
-                    else:
-                        try:
-                            del new_data[k]
-                        except KeyError:
-                            pass
-
-            self.invitations[id_] = new_data
-
-        d = self.invitations[id_]
-        d.addCallback(gotCurrentData)
-        return d
-
-    def _list(self, profile=C.PROF_KEY_NONE):
-        return self.list(profile)
-
-    @defer.inlineCallbacks
-    def list(self, profile=C.PROF_KEY_NONE):
-        """List invitations
-
-        @param profile(unicode): return invitation linked to this profile only
-            C.PROF_KEY_NONE: don't filter invitations
-        @return list(unicode): invitations uids
-        """
-        invitations = yield self.invitations.items()
-        if profile != C.PROF_KEY_NONE:
-            invitations = {id_:data for id_, data in invitations.iteritems()
-                           if data.get(u'profile') == profile}
-
-        defer.returnValue(invitations)