comparison sat/plugins/plugin_misc_email_invitation.py @ 3709:09f5ac48ffe3

merge bookmark @
author Goffi <goffi@goffi.org>
date Fri, 12 Nov 2021 17:21:24 +0100
parents cfc06915de15
children 524856bd7b19
comparison
equal deleted inserted replaced
3684:8353cc3b8db9 3709:09f5ac48ffe3
15 15
16 # You should have received a copy of the GNU Affero General Public License 16 # You should have received a copy of the GNU Affero General Public License
17 # along with this program. If not, see <http://www.gnu.org/licenses/>. 17 # along with this program. If not, see <http://www.gnu.org/licenses/>.
18 18
19 import shortuuid 19 import shortuuid
20 from typing import Optional
20 from twisted.internet import defer 21 from twisted.internet import defer
21 from twisted.words.protocols.jabber import jid 22 from twisted.words.protocols.jabber import jid
22 from twisted.words.protocols.jabber import error 23 from twisted.words.protocols.jabber import error
23 from twisted.words.protocols.jabber import sasl 24 from twisted.words.protocols.jabber import sasl
24 from sat.core.i18n import _, D_ 25 from sat.core.i18n import _, D_
128 "url_template", "message_subject", "message_body", "profile"): 129 "url_template", "message_subject", "message_body", "profile"):
129 value = locals()[key] 130 value = locals()[key]
130 if value: 131 if value:
131 kwargs[key] = str(value) 132 kwargs[key] = str(value)
132 return defer.ensureDeferred(self.create(**kwargs)) 133 return defer.ensureDeferred(self.create(**kwargs))
134
135 async def getExistingInvitation(self, email: Optional[str]) -> Optional[dict]:
136 """Retrieve existing invitation with given email
137
138 @param email: check if any invitation exist with this email
139 @return: first found invitation, or None if nothing found
140 """
141 # FIXME: This method is highly inefficient, it get all invitations and check them
142 # one by one, this is just a temporary way to avoid creating creating new accounts
143 # for an existing email. A better way will be available with Libervia 0.9.
144 # TODO: use a better way to check existing invitations
145
146 if email is None:
147 return None
148 all_invitations = await self.invitations.all()
149 for id_, invitation in all_invitations.items():
150 if invitation.get("email") == email:
151 invitation[KEY_ID] = id_
152 return invitation
153
154 async def _createAccountAndProfile(
155 self,
156 id_: str,
157 kwargs: dict,
158 extra: dict
159 ) -> None:
160 """Create XMPP account and Libervia profile for guest"""
161 ## XMPP account creation
162 password = kwargs.pop('password', None)
163 if password is None:
164 password = utils.generatePassword()
165 assert password
166 # XXX: password is here saved in clear in database
167 # it is needed for invitation as the same password is used for profile
168 # and SàT need to be able to automatically open the profile with the uuid
169 # FIXME: we could add an extra encryption key which would be used with the
170 # uuid when the invitee is connecting (e.g. with URL). This key would
171 # not be saved and could be used to encrypt profile password.
172 extra[KEY_PASSWORD] = password
173
174 jid_ = kwargs.pop('jid_', None)
175 if not jid_:
176 domain = self.host.memory.getConfig(None, 'xmpp_domain')
177 if not domain:
178 # TODO: fallback to profile's domain
179 raise ValueError(_("You need to specify xmpp_domain in sat.conf"))
180 jid_ = "invitation-{uuid}@{domain}".format(uuid=shortuuid.uuid(),
181 domain=domain)
182 jid_ = jid.JID(jid_)
183 extra[KEY_JID] = jid_.full()
184
185 if jid_.user:
186 # we don't register account if there is no user as anonymous login is then
187 # used
188 try:
189 await self.host.plugins['XEP-0077'].registerNewAccount(jid_, password)
190 except error.StanzaError as e:
191 prefix = jid_.user
192 idx = 0
193 while e.condition == 'conflict':
194 if idx >= SUFFIX_MAX:
195 raise exceptions.ConflictError(_("Can't create XMPP account"))
196 jid_.user = prefix + '_' + str(idx)
197 log.info(_("requested jid already exists, trying with {}".format(
198 jid_.full())))
199 try:
200 await self.host.plugins['XEP-0077'].registerNewAccount(
201 jid_,
202 password
203 )
204 except error.StanzaError:
205 idx += 1
206 else:
207 break
208 if e.condition != 'conflict':
209 raise e
210
211 log.info(_("account {jid_} created").format(jid_=jid_.full()))
212
213 ## profile creation
214
215 extra[KEY_GUEST_PROFILE] = guest_profile = INVITEE_PROFILE_TPL.format(
216 uuid=id_
217 )
218 # profile creation should not fail as we generate unique name ourselves
219 await self.host.memory.createProfile(guest_profile, password)
220 await self.host.memory.startSession(password, guest_profile)
221 await self.host.memory.setParam("JabberID", jid_.full(), "Connection",
222 profile_key=guest_profile)
223 await self.host.memory.setParam("Password", password, "Connection",
224 profile_key=guest_profile)
133 225
134 async def create(self, **kwargs): 226 async def create(self, **kwargs):
135 r"""Create an invitation 227 r"""Create an invitation
136 228
137 This will create an XMPP account and a profile, and use a UUID to retrieve them. 229 This will create an XMPP account and a profile, and use a UUID to retrieve them.
199 ', '.join(set(kwargs).intersection(extra)))) 291 ', '.join(set(kwargs).intersection(extra))))
200 292
201 self.checkExtra(extra) 293 self.checkExtra(extra)
202 294
203 email = kwargs.pop('email', None) 295 email = kwargs.pop('email', None)
296
297 existing = await self.getExistingInvitation(email)
298 if existing is not None:
299 log.info(f"There is already an invitation for {email!r}")
300 extra.update(existing)
301 del extra[KEY_ID]
302
204 emails_extra = kwargs.pop('emails_extra', []) 303 emails_extra = kwargs.pop('emails_extra', [])
205 if not email and emails_extra: 304 if not email and emails_extra:
206 raise ValueError( 305 raise ValueError(
207 _('You need to provide a main email address before using emails_extra')) 306 _('You need to provide a main email address before using emails_extra'))
208 307
212 raise ValueError( 311 raise ValueError(
213 _("You need to provide url_template if you use default message body")) 312 _("You need to provide url_template if you use default message body"))
214 313
215 ## uuid 314 ## uuid
216 log.info(_("creating an invitation")) 315 log.info(_("creating an invitation"))
217 id_ = str(shortuuid.uuid()) 316 id_ = existing[KEY_ID] if existing else str(shortuuid.uuid())
218 317
219 ## XMPP account creation 318 if existing is None:
220 password = kwargs.pop('password', None) 319 await self._createAccountAndProfile(id_, kwargs, extra)
221 if password is None: 320
222 password = utils.generatePassword() 321 profile = kwargs.pop('profile', None)
223 assert password 322 guest_profile = extra[KEY_GUEST_PROFILE]
224 # XXX: password is here saved in clear in database 323 jid_ = jid.JID(extra[KEY_JID])
225 # it is needed for invitation as the same password is used for profile 324
226 # and SàT need to be able to automatically open the profile with the uuid 325 ## identity
227 # FIXME: we could add an extra encryption key which would be used with the uuid
228 # when the invitee is connecting (e.g. with URL). This key would not be
229 # saved and could be used to encrypt profile password.
230 extra[KEY_PASSWORD] = password
231
232 jid_ = kwargs.pop('jid_', None)
233 if not jid_:
234 domain = self.host.memory.getConfig(None, 'xmpp_domain')
235 if not domain:
236 # TODO: fallback to profile's domain
237 raise ValueError(_("You need to specify xmpp_domain in sat.conf"))
238 jid_ = "invitation-{uuid}@{domain}".format(uuid=shortuuid.uuid(),
239 domain=domain)
240 jid_ = jid.JID(jid_)
241 if jid_.user:
242 # we don't register account if there is no user as anonymous login is then
243 # used
244 try:
245 await self.host.plugins['XEP-0077'].registerNewAccount(jid_, password)
246 except error.StanzaError as e:
247 prefix = jid_.user
248 idx = 0
249 while e.condition == 'conflict':
250 if idx >= SUFFIX_MAX:
251 raise exceptions.ConflictError(_("Can't create XMPP account"))
252 jid_.user = prefix + '_' + str(idx)
253 log.info(_("requested jid already exists, trying with {}".format(
254 jid_.full())))
255 try:
256 await self.host.plugins['XEP-0077'].registerNewAccount(jid_,
257 password)
258 except error.StanzaError:
259 idx += 1
260 else:
261 break
262 if e.condition != 'conflict':
263 raise e
264
265 log.info(_("account {jid_} created").format(jid_=jid_.full()))
266
267 ## profile creation
268
269 extra[KEY_GUEST_PROFILE] = guest_profile = INVITEE_PROFILE_TPL.format(uuid=id_)
270 # profile creation should not fail as we generate unique name ourselves
271 await self.host.memory.createProfile(guest_profile, password)
272 await self.host.memory.startSession(password, guest_profile)
273 await self.host.memory.setParam("JabberID", jid_.full(), "Connection",
274 profile_key=guest_profile)
275 await self.host.memory.setParam("Password", password, "Connection",
276 profile_key=guest_profile)
277 name = kwargs.pop('name', None) 326 name = kwargs.pop('name', None)
327 password = extra[KEY_PASSWORD]
278 if name is not None: 328 if name is not None:
279 extra['name'] = name 329 extra['name'] = name
280 try: 330 try:
281 id_plugin = self.host.plugins['IDENTITY'] 331 id_plugin = self.host.plugins['IDENTITY']
282 except KeyError: 332 except KeyError:
304 if name is None: 354 if name is None:
305 format_args['name'] = email 355 format_args['name'] = email
306 else: 356 else:
307 format_args['name'] = name 357 format_args['name'] = name
308 358
309 profile = kwargs.pop('profile', None)
310 if profile is None: 359 if profile is None:
311 format_args['profile'] = '' 360 format_args['profile'] = ''
312 else: 361 else:
313 format_args['profile'] = extra['profile'] = profile 362 format_args['profile'] = extra['profile'] = profile
314 363
341 else: 390 else:
342 await self.host.updateContact(client, jid_, name, ['guests']) 391 await self.host.updateContact(client, jid_, name, ['guests'])
343 392
344 if kwargs: 393 if kwargs:
345 log.warning(_("Not all arguments have been consumed: {}").format(kwargs)) 394 log.warning(_("Not all arguments have been consumed: {}").format(kwargs))
346
347 extra[KEY_JID] = jid_.full()
348 395
349 ## extra data saving 396 ## extra data saving
350 self.invitations[id_] = extra 397 self.invitations[id_] = extra
351 398
352 extra[KEY_ID] = id_ 399 extra[KEY_ID] = id_