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 .
This commit is contained in:
Goffi 2021-11-05 18:11:18 +01:00
commit ecc6f96990

View file

@ -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 @@ class InvitationsPlugin(object):
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 @@ class InvitationsPlugin(object):
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 @@ class InvitationsPlugin(object):
## 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 @@ class InvitationsPlugin(object):
else:
format_args['name'] = name
profile = kwargs.pop('profile', None)
if profile is None:
format_args['profile'] = ''
else:
@ -344,8 +393,6 @@ class InvitationsPlugin(object):
if kwargs:
log.warning(_("Not all arguments have been consumed: {}").format(kwargs))
extra[KEY_JID] = jid_.full()
## extra data saving
self.invitations[id_] = extra