Mercurial > libervia-backend
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_ |