comparison sat/plugins/plugin_misc_account.py @ 2562:26edcf3a30eb

core, setup: huge cleaning: - moved directories from src and frontends/src to sat and sat_frontends, which is the recommanded naming convention - move twisted directory to root - removed all hacks from setup.py, and added missing dependencies, it is now clean - use https URL for website in setup.py - removed "Environment :: X11 Applications :: GTK", as wix is deprecated and removed - renamed sat.sh to sat and fixed its installation - added python_requires to specify Python version needed - replaced glib2reactor which use deprecated code by gtk3reactor sat can now be installed directly from virtualenv without using --system-site-packages anymore \o/
author Goffi <goffi@goffi.org>
date Mon, 02 Apr 2018 19:44:50 +0200
parents src/plugins/plugin_misc_account.py@0046283a285d
children 56f94936df1e
comparison
equal deleted inserted replaced
2561:bd30dc3ffe5a 2562:26edcf3a30eb
1 #!/usr/bin/env python2
2 # -*- coding: utf-8 -*-
3
4 # SAT plugin for account creation (experimental)
5 # Copyright (C) 2009-2018 Jérôme Poisson (goffi@goffi.org)
6
7 # This program is free software: you can redistribute it and/or modify
8 # it under the terms of the GNU Affero General Public License as published by
9 # the Free Software Foundation, either version 3 of the License, or
10 # (at your option) any later version.
11
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU Affero General Public License for more details.
16
17 # You should have received a copy of the GNU Affero General Public License
18 # along with this program. If not, see <http://www.gnu.org/licenses/>.
19
20 from sat.core.i18n import _, D_
21 from sat.core.log import getLogger
22 log = getLogger(__name__)
23 from sat.core import exceptions
24 from sat.tools import xml_tools
25 from sat.memory.memory import Sessions
26 from sat.memory.crypto import PasswordHasher
27 from sat.core.constants import Const as C
28 import ConfigParser
29 from twisted.internet import defer
30 from twisted.python.failure import Failure
31 from twisted.words.protocols.jabber import jid
32 from sat.tools import email as sat_email
33
34 # FIXME: this plugin code is old and need a cleaning
35 # TODO: account deletion/password change need testing
36
37
38 PLUGIN_INFO = {
39 C.PI_NAME: "Account Plugin",
40 C.PI_IMPORT_NAME: "MISC-ACCOUNT",
41 C.PI_TYPE: "MISC",
42 C.PI_PROTOCOLS: [],
43 C.PI_DEPENDENCIES: ["XEP-0077"],
44 C.PI_RECOMMENDATIONS: ['GROUPBLOG'],
45 C.PI_MAIN: "MiscAccount",
46 C.PI_HANDLER: "no",
47 C.PI_DESCRIPTION: _(u"""SàT account creation""")
48 }
49
50 CONFIG_SECTION = "plugin account"
51
52 # You need do adapt the following consts to your server
53 # all theses values (key=option name, value=default) can (and should) be overriden in sat.conf
54 # in section CONFIG_SECTION
55
56 default_conf = {"email_from": "NOREPLY@example.net",
57 "email_server": "localhost",
58 "email_sender_domain": "",
59 "email_port": 25,
60 "email_username": "",
61 "email_password": "",
62 "email_starttls": "false",
63 "email_auth": "false",
64 "email_admins_list": [],
65 "admin_email": "",
66 "new_account_server": "localhost",
67 "new_account_domain": "", # use xmpp_domain if not found
68 "reserved_list": ['libervia'] # profiles which can't be used
69 }
70
71 WELCOME_MSG = D_(u"""Welcome to Libervia, the web interface of Salut à Toi.
72
73 Your account on {domain} has been successfully created.
74 This is a demonstration version to show you the current status of the project.
75 It is still under development, please keep it in mind!
76
77 Here is your connection information:
78
79 Login on {domain}: {profile}
80 Jabber ID (JID): {jid}
81 Your password has been chosen by yourself during registration.
82
83 In the beginning, you have nobody to talk to. To find some contacts, you may use the users' directory:
84 - make yourself visible in "Service / Directory subscription".
85 - search for people with "Contacts" / Search directory".
86
87 Any feedback welcome. Thank you!
88
89 Salut à Toi association
90 https://www.salut-a-toi.org
91 """)
92
93 DEFAULT_DOMAIN = u"example.net"
94
95
96 class MiscAccount(object):
97 """Account plugin: create a SàT + XMPP account, used by Libervia"""
98 # XXX: This plugin was initialy a Q&D one used for the demo.
99 # TODO: cleaning, separate email handling, more configuration/tests, fixes
100
101
102 def __init__(self, host):
103 log.info(_(u"Plugin Account initialization"))
104 self.host = host
105 host.bridge.addMethod("registerSatAccount", ".plugin", in_sign='sss', out_sign='', method=self._registerAccount, async=True)
106 host.bridge.addMethod("getNewAccountDomain", ".plugin", in_sign='', out_sign='s', method=self.getNewAccountDomain, async=False)
107 host.bridge.addMethod("getAccountDialogUI", ".plugin", in_sign='s', out_sign='s', method=self._getAccountDialogUI, async=False)
108 host.bridge.addMethod("asyncConnectWithXMPPCredentials", ".plugin", in_sign='ss', out_sign='b', method=self.asyncConnectWithXMPPCredentials, async=True)
109
110 self.fixEmailAdmins()
111 self._sessions = Sessions()
112
113 self.__account_cb_id = host.registerCallback(self._accountDialogCb, with_data=True)
114 self.__change_password_id = host.registerCallback(self.__changePasswordCb, with_data=True)
115
116 def deleteBlogCallback(posts, comments):
117 return lambda data, profile: self.__deleteBlogPostsCb(posts, comments, data, profile)
118
119 self.__delete_posts_id = host.registerCallback(deleteBlogCallback(True, False), with_data=True)
120 self.__delete_comments_id = host.registerCallback(deleteBlogCallback(False, True), with_data=True)
121 self.__delete_posts_comments_id = host.registerCallback(deleteBlogCallback(True, True), with_data=True)
122
123 self.__delete_account_id = host.registerCallback(self.__deleteAccountCb, with_data=True)
124
125
126 # FIXME: remove this after some time, when the deprecated parameter is really abandoned
127 def fixEmailAdmins(self):
128 """Handle deprecated config option "admin_email" to fix the admin emails list"""
129 admin_email = self.getConfig('admin_email')
130 if not admin_email:
131 return
132 log.warning(u"admin_email parameter is deprecated, please use email_admins_list instead")
133 param_name = "email_admins_list"
134 try:
135 section = ""
136 value = self.host.memory.getConfig(section, param_name, Exception)
137 except (ConfigParser.NoOptionError, ConfigParser.NoSectionError):
138 section = CONFIG_SECTION
139 value = self.host.memory.getConfig(section, param_name, default_conf[param_name])
140
141 value = set(value)
142 value.add(admin_email)
143 self.host.memory.config.set(section, param_name, ",".join(value))
144
145 def getConfig(self, name, section=CONFIG_SECTION):
146 if name.startswith("email_"):
147 # XXX: email_ parameters were first in [plugin account] section
148 # but as it make more sense to have them in common with other plugins,
149 # they can now be in [DEFAULT] section
150 try:
151 value = self.host.memory.getConfig(None, name, Exception)
152 except (ConfigParser.NoOptionError, ConfigParser.NoSectionError):
153 pass
154 else:
155 return value
156
157 if section == CONFIG_SECTION:
158 default = default_conf[name]
159 else:
160 default = None
161 return self.host.memory.getConfig(section, name, default)
162
163 def _registerAccount(self, email, password, profile):
164 return self.registerAccount(email, password, None, profile)
165
166 def registerAccount(self, email, password, jid_s, profile):
167 """Register a new profile, its associated XMPP account, send the confirmation emails.
168
169 @param email (unicode): where to send to confirmation email to
170 @param password (unicode): password chosen by the user
171 while be used for profile *and* XMPP account
172 @param jid_s (unicode): JID to re-use or to register:
173 - non empty value: bind this JID to the new sat profile
174 - None or "": register a new JID on the local XMPP server
175 @param profile
176 @return Deferred
177 """
178 d = self.createProfile(password, jid_s, profile)
179 d.addCallback(lambda dummy: self.sendEmails(email, profile))
180 return d
181
182 def createProfile(self, password, jid_s, profile):
183 """Register a new profile and its associated XMPP account.
184
185 @param password (unicode): password chosen by the user
186 while be used for profile *and* XMPP account
187 @param jid_s (unicode): JID to re-use or to register:
188 - non empty value: bind this JID to the new sat profile
189 - None or "": register a new JID on the local XMPP server
190 @param profile
191 @return Deferred
192 """
193 if not password or not profile:
194 raise exceptions.DataError
195
196 if profile.lower() in self.getConfig('reserved_list'):
197 return defer.fail(Failure(exceptions.ConflictError))
198
199 d = self.host.memory.createProfile(profile, password)
200 d.addCallback(lambda dummy: self.profileCreated(password, jid_s, profile))
201 return d
202
203 def profileCreated(self, password, jid_s, profile):
204 """Create the XMPP account and set the profile connection parameters.
205
206 @param password (unicode): password chosen by the user
207 @param jid_s (unicode): JID to re-use or to register:
208 - non empty value: bind this JID to the new sat profile
209 - None or empty: register a new JID on the local XMPP server
210 @param profile
211 @return: Deferred
212 """
213 if jid_s:
214 d = defer.succeed(None)
215 jid_ = jid.JID(jid_s)
216 else:
217 jid_s = profile + u"@" + self.getNewAccountDomain()
218 jid_ = jid.JID(jid_s)
219 d = self.host.plugins['XEP-0077'].registerNewAccount(jid_, password)
220
221 def setParams(dummy):
222 self.host.memory.setParam("JabberID", jid_s, "Connection", profile_key=profile)
223 d = self.host.memory.setParam("Password", password, "Connection", profile_key=profile)
224 return d
225
226 def removeProfile(failure):
227 self.host.memory.asyncDeleteProfile(profile)
228 return failure
229
230 d.addCallback(lambda dummy: self.host.memory.startSession(password, profile))
231 d.addCallback(setParams)
232 d.addCallback(lambda dummy: self.host.memory.stopSession(profile))
233 d.addErrback(removeProfile)
234 return d
235
236 def _sendEmailEb(self, failure_, email):
237 # TODO: return error code to user
238 log.error(_(u"Failed to send account creation confirmation to {email}: {msg}").format(
239 email = email,
240 msg = failure_))
241
242 def sendEmails(self, email, profile):
243 # time to send the email
244
245 domain = self.getNewAccountDomain()
246
247 # email to the administrators
248 admins_emails = self.getConfig('email_admins_list')
249 if not admins_emails:
250 log.warning(u"No known admin email, we can't send email to administrator(s).\nPlease fill email_admins_list parameter")
251 d_admin = defer.fail(exceptions.DataError("no admin email"))
252 else:
253 subject = _(u'New Libervia account created')
254 body = (u"""New account created: {profile} [{email}]""".format(
255 profile = profile,
256 # there is no email when an existing XMPP account is used
257 email = email or u"<no email>"))
258 d_admin = sat_email.sendEmail(self.host, admins_emails, subject, body)
259
260 admins_emails_txt = u', '.join([u'<' + addr + u'>' for addr in admins_emails])
261 d_admin.addCallbacks(lambda dummy: log.debug(u"Account creation notification sent to admin(s) {}".format(admins_emails_txt)),
262 lambda dummy: log.error(u"Failed to send account creation notification to admin {}".format(admins_emails_txt)))
263 if not email:
264 # TODO: if use register with an existing account, an XMPP message should be sent
265 return d_admin
266
267 jid_s = self.host.memory.getParamA(u"JabberID", u"Connection", profile_key=profile)
268 subject = _(u'Your Libervia account has been created')
269 body = (_(WELCOME_MSG).format(profile=profile, jid=jid_s, domain=domain))
270
271 # XXX: this will not fail when the email address doesn't exist
272 # FIXME: check email reception to validate email given by the user
273 # FIXME: delete the profile if the email could not been sent?
274 d_user = sat_email.sendEmail(self.host, [email], subject, body)
275 d_user.addCallbacks(lambda dummy: log.debug(u"Account creation confirmation sent to <{}>".format(email)),
276 self._sendEmailEb)
277 return defer.DeferredList([d_user, d_admin])
278
279 def getNewAccountDomain(self):
280 """get the domain that will be set to new account"""
281
282 domain = self.getConfig('new_account_domain') or self.getConfig('xmpp_domain', None)
283 if not domain:
284 log.warning(_(u'xmpp_domain needs to be set in sat.conf. Using "{default}" meanwhile').format(default=DEFAULT_DOMAIN))
285 return DEFAULT_DOMAIN
286 return domain
287
288 def _getAccountDialogUI(self, profile):
289 """Get the main dialog to manage your account
290 @param menu_data
291 @param profile: %(doc_profile)s
292 @return: XML of the dialog
293 """
294 form_ui = xml_tools.XMLUI("form", "tabs", title=D_("Manage your account"), submit_id=self.__account_cb_id)
295 tab_container = form_ui.current_container
296
297 tab_container.addTab("update", D_("Change your password"), container=xml_tools.PairsContainer)
298 form_ui.addLabel(D_("Current profile password"))
299 form_ui.addPassword("current_passwd", value="")
300 form_ui.addLabel(D_("New password"))
301 form_ui.addPassword("new_passwd1", value="")
302 form_ui.addLabel(D_("New password (again)"))
303 form_ui.addPassword("new_passwd2", value="")
304
305 # FIXME: uncomment and fix these features
306 """
307 if 'GROUPBLOG' in self.host.plugins:
308 tab_container.addTab("delete_posts", D_("Delete your posts"), container=xml_tools.PairsContainer)
309 form_ui.addLabel(D_("Current profile password"))
310 form_ui.addPassword("delete_posts_passwd", value="")
311 form_ui.addLabel(D_("Delete all your posts and their comments"))
312 form_ui.addBool("delete_posts_checkbox", "false")
313 form_ui.addLabel(D_("Delete all your comments on other's posts"))
314 form_ui.addBool("delete_comments_checkbox", "false")
315
316 tab_container.addTab("delete", D_("Delete your account"), container=xml_tools.PairsContainer)
317 form_ui.addLabel(D_("Current profile password"))
318 form_ui.addPassword("delete_passwd", value="")
319 form_ui.addLabel(D_("Delete your account"))
320 form_ui.addBool("delete_checkbox", "false")
321 """
322
323 return form_ui.toXml()
324
325 @defer.inlineCallbacks
326 def _accountDialogCb(self, data, profile):
327 """Called when the user submits the main account dialog
328 @param data
329 @param profile
330 """
331 sat_cipher = yield self.host.memory.asyncGetParamA(C.PROFILE_PASS_PATH[1], C.PROFILE_PASS_PATH[0], profile_key=profile)
332
333 @defer.inlineCallbacks
334 def verify(attempt):
335 auth = yield PasswordHasher.verify(attempt, sat_cipher)
336 defer.returnValue(auth)
337
338 def error_ui(message=None):
339 if not message:
340 message = D_("The provided profile password doesn't match.")
341 error_ui = xml_tools.XMLUI("popup", title=D_("Attempt failure"))
342 error_ui.addText(message)
343 return {'xmlui': error_ui.toXml()}
344
345 # check for account deletion
346 # FIXME: uncomment and fix these features
347 """
348 delete_passwd = data[xml_tools.SAT_FORM_PREFIX + 'delete_passwd']
349 delete_checkbox = data[xml_tools.SAT_FORM_PREFIX + 'delete_checkbox']
350 if delete_checkbox == 'true':
351 verified = yield verify(delete_passwd)
352 assert isinstance(verified, bool)
353 if verified:
354 defer.returnValue(self.__deleteAccount(profile))
355 defer.returnValue(error_ui())
356
357 # check for blog posts deletion
358 if 'GROUPBLOG' in self.host.plugins:
359 delete_posts_passwd = data[xml_tools.SAT_FORM_PREFIX + 'delete_posts_passwd']
360 delete_posts_checkbox = data[xml_tools.SAT_FORM_PREFIX + 'delete_posts_checkbox']
361 delete_comments_checkbox = data[xml_tools.SAT_FORM_PREFIX + 'delete_comments_checkbox']
362 posts = delete_posts_checkbox == 'true'
363 comments = delete_comments_checkbox == 'true'
364 if posts or comments:
365 verified = yield verify(delete_posts_passwd)
366 assert isinstance(verified, bool)
367 if verified:
368 defer.returnValue(self.__deleteBlogPosts(posts, comments, profile))
369 defer.returnValue(error_ui())
370 """
371
372 # check for password modification
373 current_passwd = data[xml_tools.SAT_FORM_PREFIX + 'current_passwd']
374 new_passwd1 = data[xml_tools.SAT_FORM_PREFIX + 'new_passwd1']
375 new_passwd2 = data[xml_tools.SAT_FORM_PREFIX + 'new_passwd2']
376 if new_passwd1 or new_passwd2:
377 verified = yield verify(current_passwd)
378 assert isinstance(verified, bool)
379 if verified:
380 if new_passwd1 == new_passwd2:
381 data = yield self.__changePassword(new_passwd1, profile=profile)
382 defer.returnValue(data)
383 else:
384 defer.returnValue(error_ui(D_("The values entered for the new password are not equal.")))
385 defer.returnValue(error_ui())
386
387 defer.returnValue({})
388
389 def __changePassword(self, password, profile):
390 """Ask for a confirmation before changing the XMPP account and SàT profile passwords.
391
392 @param password (str): the new password
393 @param profile (str): %(doc_profile)s
394 """
395 session_id, dummy = self._sessions.newSession({'new_password': password}, profile=profile)
396 form_ui = xml_tools.XMLUI("form", title=D_("Change your password?"), submit_id=self.__change_password_id, session_id=session_id)
397 form_ui.addText(D_("Note for advanced users: this will actually change both your SàT profile password AND your XMPP account password."))
398 form_ui.addText(D_("Continue with changing the password?"))
399 return {'xmlui': form_ui.toXml()}
400
401 def __changePasswordCb(self, data, profile):
402 """Actually change the user XMPP account and SàT profile password
403 @param data (dict)
404 @profile (str): %(doc_profile)s
405 """
406 client = self.host.getClient(profile)
407 password = self._sessions.profileGet(data['session_id'], profile)['new_password']
408 del self._sessions[data['session_id']]
409
410 def passwordChanged(dummy):
411 d = self.host.memory.setParam(C.PROFILE_PASS_PATH[1], password, C.PROFILE_PASS_PATH[0], profile_key=profile)
412 d.addCallback(lambda dummy: self.host.memory.setParam("Password", password, "Connection", profile_key=profile))
413 confirm_ui = xml_tools.XMLUI("popup", title=D_("Confirmation"))
414 confirm_ui.addText(D_("Your password has been changed."))
415 return defer.succeed({'xmlui': confirm_ui.toXml()})
416
417 def errback(failure):
418 error_ui = xml_tools.XMLUI("popup", title=D_("Error"))
419 error_ui.addText(D_("Your password could not be changed: %s") % failure.getErrorMessage())
420 return defer.succeed({'xmlui': error_ui.toXml()})
421
422 d = self.host.plugins['XEP-0077'].changePassword(client, password)
423 d.addCallbacks(passwordChanged, errback)
424 return d
425
426 def __deleteAccount(self, profile):
427 """Ask for a confirmation before deleting the XMPP account and SàT profile
428 @param profile
429 """
430 form_ui = xml_tools.XMLUI("form", title=D_("Delete your account?"), submit_id=self.__delete_account_id)
431 form_ui.addText(D_("If you confirm this dialog, you will be disconnected and then your XMPP account AND your SàT profile will both be DELETED."))
432 target = D_('contact list, messages history, blog posts and comments' if 'GROUPBLOG' in self.host.plugins else D_('contact list and messages history'))
433 form_ui.addText(D_("All your data stored on %(server)s, including your %(target)s will be erased.") % {'server': self.getNewAccountDomain(), 'target': target})
434 form_ui.addText(D_("There is no other confirmation dialog, this is the very last one! Are you sure?"))
435 return {'xmlui': form_ui.toXml()}
436
437 def __deleteAccountCb(self, data, profile):
438 """Actually delete the XMPP account and SàT profile
439
440 @param data
441 @param profile
442 """
443 client = self.host.getClient(profile)
444 def userDeleted(dummy):
445
446 # FIXME: client should be disconnected at this point, so 2 next loop should be removed (to be confirmed)
447 for jid_ in client.roster._jids: # empty roster
448 client.presence.unsubscribe(jid_)
449
450 for jid_ in self.host.memory.getWaitingSub(profile): # delete waiting subscriptions
451 self.host.memory.delWaitingSub(jid_)
452
453 delete_profile = lambda: self.host.memory.asyncDeleteProfile(profile, force=True)
454 if 'GROUPBLOG' in self.host.plugins:
455 d = self.host.plugins['GROUPBLOG'].deleteAllGroupBlogsAndComments(profile_key=profile)
456 d.addCallback(lambda dummy: delete_profile())
457 else:
458 delete_profile()
459
460 return defer.succeed({})
461
462 def errback(failure):
463 error_ui = xml_tools.XMLUI("popup", title=D_("Error"))
464 error_ui.addText(D_("Your XMPP account could not be deleted: %s") % failure.getErrorMessage())
465 return defer.succeed({'xmlui': error_ui.toXml()})
466
467 d = self.host.plugins['XEP-0077'].unregister(client, jid.JID(client.jid.host))
468 d.addCallbacks(userDeleted, errback)
469 return d
470
471 def __deleteBlogPosts(self, posts, comments, profile):
472 """Ask for a confirmation before deleting the blog posts
473 @param posts: delete all posts of the user (and their comments)
474 @param comments: delete all the comments of the user on other's posts
475 @param data
476 @param profile
477 """
478 if posts:
479 if comments: # delete everything
480 form_ui = xml_tools.XMLUI("form", title=D_("Delete all your (micro-)blog posts and comments?"), submit_id=self.__delete_posts_comments_id)
481 form_ui.addText(D_("If you confirm this dialog, all the (micro-)blog data you submitted will be erased."))
482 form_ui.addText(D_("These are the public and private posts and comments you sent to any group."))
483 form_ui.addText(D_("There is no other confirmation dialog, this is the very last one! Are you sure?"))
484 else: # delete only the posts
485 form_ui = xml_tools.XMLUI("form", title=D_("Delete all your (micro-)blog posts?"), submit_id=self.__delete_posts_id)
486 form_ui.addText(D_("If you confirm this dialog, all the public and private posts you sent to any group will be erased."))
487 form_ui.addText(D_("There is no other confirmation dialog, this is the very last one! Are you sure?"))
488 elif comments: # delete only the comments
489 form_ui = xml_tools.XMLUI("form", title=D_("Delete all your (micro-)blog comments?"), submit_id=self.__delete_comments_id)
490 form_ui.addText(D_("If you confirm this dialog, all the public and private comments you made on other people's posts will be erased."))
491 form_ui.addText(D_("There is no other confirmation dialog, this is the very last one! Are you sure?"))
492
493 return {'xmlui': form_ui.toXml()}
494
495 def __deleteBlogPostsCb(self, posts, comments, data, profile):
496 """Actually delete the XMPP account and SàT profile
497 @param posts: delete all posts of the user (and their comments)
498 @param comments: delete all the comments of the user on other's posts
499 @param profile
500 """
501 if posts:
502 if comments:
503 target = D_('blog posts and comments')
504 d = self.host.plugins['GROUPBLOG'].deleteAllGroupBlogsAndComments(profile_key=profile)
505 else:
506 target = D_('blog posts')
507 d = self.host.plugins['GROUPBLOG'].deleteAllGroupBlogs(profile_key=profile)
508 elif comments:
509 target = D_('comments')
510 d = self.host.plugins['GROUPBLOG'].deleteAllGroupBlogsComments(profile_key=profile)
511
512 def deleted(result):
513 ui = xml_tools.XMLUI("popup", title=D_("Deletion confirmation"))
514 # TODO: change the message when delete/retract notifications are done with XEP-0060
515 ui.addText(D_("Your %(target)s have been deleted.") % {'target': target})
516 ui.addText(D_("Known issue of the demo version: you need to refresh the page to make the deleted posts actually disappear."))
517 return defer.succeed({'xmlui': ui.toXml()})
518
519 def errback(failure):
520 error_ui = xml_tools.XMLUI("popup", title=D_("Error"))
521 error_ui.addText(D_("Your %(target)s could not be deleted: %(message)s") % {'target': target, 'message': failure.getErrorMessage()})
522 return defer.succeed({'xmlui': error_ui.toXml()})
523
524 d.addCallbacks(deleted, errback)
525 return d
526
527 def asyncConnectWithXMPPCredentials(self, jid_s, password):
528 """Create and connect a new SàT profile using the given XMPP credentials.
529
530 Re-use given JID and XMPP password for the profile name and profile password.
531 @param jid_s (unicode): JID
532 @param password (unicode): XMPP password
533 @return Deferred(bool)
534 @raise exceptions.PasswordError, exceptions.ConflictError
535 """
536 try: # be sure that the profile doesn't exist yet
537 self.host.memory.getProfileName(jid_s)
538 except exceptions.ProfileUnknownError:
539 pass
540 else:
541 raise exceptions.ConflictError
542
543 d = self.createProfile(password, jid_s, jid_s)
544 d.addCallback(lambda dummy: self.host.memory.getProfileName(jid_s)) # checks if the profile has been successfuly created
545 d.addCallback(self.host.connect, password, {}, 0)
546
547
548 def connected(result):
549 self.sendEmails(None, profile=jid_s)
550 return result
551
552 def removeProfile(failure): # profile has been successfully created but the XMPP credentials are wrong!
553 log.debug("Removing previously auto-created profile: %s" % failure.getErrorMessage())
554 self.host.memory.asyncDeleteProfile(jid_s)
555 raise failure
556
557 # FIXME: we don't catch the case where the JID host is not an XMPP server, and the user
558 # has to wait until the DBUS timeout ; as a consequence, emails are sent to the admins
559 # and the profile is not deleted. When the host exists, removeProfile is well called.
560 d.addCallbacks(connected, removeProfile)
561 return d