changeset 3700:cfc06915de15

plugin email invitations: re-use existing invitation for a given email: if an invitation is done on an email which has already received an invitation, it is re-used and no new XMPP account or Libervia profile is created (the name is modified though). This is currently done in an inefficient way due to `LazyPersistentBinaryDict` limitations (all invitations are loaded and checked to find the email). A more efficient check should be easier to implement with storage changes in 0.9 .
author Goffi <goffi@goffi.org>
date Fri, 05 Nov 2021 18:11:18 +0100
parents 2dab494e56fc
children 6d298323eed2
files sat/plugins/plugin_misc_email_invitation.py
diffstat 1 files changed, 107 insertions(+), 60 deletions(-) [+]
line wrap: on
line diff
--- a/sat/plugins/plugin_misc_email_invitation.py	Fri Nov 05 18:08:39 2021 +0100
+++ b/sat/plugins/plugin_misc_email_invitation.py	Fri Nov 05 18:11:18 2021 +0100
@@ -17,6 +17,7 @@
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 import shortuuid
+from typing import Optional
 from twisted.internet import defer
 from twisted.words.protocols.jabber import jid
 from twisted.words.protocols.jabber import error
@@ -131,6 +132,97 @@
                 kwargs[key] = str(value)
         return defer.ensureDeferred(self.create(**kwargs))
 
+    async def getExistingInvitation(self, email: Optional[str]) -> Optional[dict]:
+        """Retrieve existing invitation with given email
+
+        @param email: check if any invitation exist with this email
+        @return: first found invitation, or None if nothing found
+        """
+        # FIXME: This method is highly inefficient, it get all invitations and check them
+        # one by one, this is just a temporary way to avoid creating creating new accounts
+        # for an existing email. A better way will be available with Libervia 0.9.
+        # TODO: use a better way to check existing invitations
+
+        if email is None:
+            return None
+        all_invitations = await self.invitations.all()
+        for id_, invitation in all_invitations.items():
+            if invitation.get("email") == email:
+                invitation[KEY_ID] = id_
+                return invitation
+
+    async def _createAccountAndProfile(
+        self,
+        id_: str,
+        kwargs: dict,
+        extra: dict
+    ) -> None:
+        """Create XMPP account and Libervia profile for guest"""
+        ## XMPP account creation
+        password = kwargs.pop('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('jid_', None)
+        if not jid_:
+            domain = self.host.memory.getConfig(None, 'xmpp_domain')
+            if not domain:
+                # TODO: fallback to profile's domain
+                raise ValueError(_("You need to specify xmpp_domain in sat.conf"))
+            jid_ = "invitation-{uuid}@{domain}".format(uuid=shortuuid.uuid(),
+                                                        domain=domain)
+        jid_ = jid.JID(jid_)
+        extra[KEY_JID] = jid_.full()
+
+        if jid_.user:
+            # we don't register account if there is no user as anonymous login is then
+            # used
+            try:
+                await self.host.plugins['XEP-0077'].registerNewAccount(jid_, password)
+            except error.StanzaError as e:
+                prefix = jid_.user
+                idx = 0
+                while e.condition == 'conflict':
+                    if idx >= SUFFIX_MAX:
+                        raise exceptions.ConflictError(_("Can't create XMPP account"))
+                    jid_.user = prefix + '_' + str(idx)
+                    log.info(_("requested jid already exists, trying with {}".format(
+                        jid_.full())))
+                    try:
+                        await self.host.plugins['XEP-0077'].registerNewAccount(
+                            jid_,
+                            password
+                        )
+                    except error.StanzaError:
+                        idx += 1
+                    else:
+                        break
+                if e.condition != 'conflict':
+                    raise e
+
+            log.info(_("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
+        await self.host.memory.createProfile(guest_profile, password)
+        await self.host.memory.startSession(password, guest_profile)
+        await self.host.memory.setParam("JabberID", jid_.full(), "Connection",
+                                        profile_key=guest_profile)
+        await self.host.memory.setParam("Password", password, "Connection",
+                                        profile_key=guest_profile)
+
     async def create(self, **kwargs):
         r"""Create an invitation
 
@@ -201,6 +293,13 @@
         self.checkExtra(extra)
 
         email = kwargs.pop('email', None)
+
+        existing = await self.getExistingInvitation(email)
+        if existing is not None:
+            log.info(f"There is already an invitation for {email!r}")
+            extra.update(existing)
+            del extra[KEY_ID]
+
         emails_extra = kwargs.pop('emails_extra', [])
         if not email and emails_extra:
             raise ValueError(
@@ -214,67 +313,18 @@
 
         ## uuid
         log.info(_("creating an invitation"))
-        id_ = str(shortuuid.uuid())
+        id_ = existing[KEY_ID] if existing else str(shortuuid.uuid())
 
-        ## XMPP account creation
-        password = kwargs.pop('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
+        if existing is None:
+            await self._createAccountAndProfile(id_, kwargs, extra)
 
-        jid_ = kwargs.pop('jid_', None)
-        if not jid_:
-            domain = self.host.memory.getConfig(None, 'xmpp_domain')
-            if not domain:
-                # TODO: fallback to profile's domain
-                raise ValueError(_("You need to specify xmpp_domain in sat.conf"))
-            jid_ = "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:
-                await self.host.plugins['XEP-0077'].registerNewAccount(jid_, password)
-            except error.StanzaError as e:
-                prefix = jid_.user
-                idx = 0
-                while e.condition == 'conflict':
-                    if idx >= SUFFIX_MAX:
-                        raise exceptions.ConflictError(_("Can't create XMPP account"))
-                    jid_.user = prefix + '_' + str(idx)
-                    log.info(_("requested jid already exists, trying with {}".format(
-                        jid_.full())))
-                    try:
-                        await self.host.plugins['XEP-0077'].registerNewAccount(jid_,
-                                                                               password)
-                    except error.StanzaError:
-                        idx += 1
-                    else:
-                        break
-                if e.condition != 'conflict':
-                    raise e
+        profile = kwargs.pop('profile', None)
+        guest_profile = extra[KEY_GUEST_PROFILE]
+        jid_ = jid.JID(extra[KEY_JID])
 
-            log.info(_("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
-        await self.host.memory.createProfile(guest_profile, password)
-        await self.host.memory.startSession(password, guest_profile)
-        await self.host.memory.setParam("JabberID", jid_.full(), "Connection",
-                                        profile_key=guest_profile)
-        await self.host.memory.setParam("Password", password, "Connection",
-                                        profile_key=guest_profile)
+        ## identity
         name = kwargs.pop('name', None)
+        password = extra[KEY_PASSWORD]
         if name is not None:
             extra['name'] = name
             try:
@@ -306,7 +356,6 @@
             else:
                 format_args['name'] = name
 
-            profile = kwargs.pop('profile', None)
             if profile is None:
                 format_args['profile'] = ''
             else:
@@ -344,8 +393,6 @@
         if kwargs:
             log.warning(_("Not all arguments have been consumed: {}").format(kwargs))
 
-        extra[KEY_JID] = jid_.full()
-
         ## extra data saving
         self.invitations[id_] = extra