# HG changeset patch # User Goffi # Date 1636132278 -3600 # Node ID cfc06915de1543cb358c77ed89c0f5a66c9cba17 # Parent 2dab494e56fc7b04445d6841ee7c23a1158cec70 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 . diff -r 2dab494e56fc -r cfc06915de15 sat/plugins/plugin_misc_email_invitation.py --- 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 . 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