diff --git a/deluge/core/authmanager.py b/deluge/core/authmanager.py index 7c2d5c1d2..bed8ec0c2 100644 --- a/deluge/core/authmanager.py +++ b/deluge/core/authmanager.py @@ -2,6 +2,7 @@ # authmanager.py # # Copyright (C) 2009 Andrew Resch +# Copyright (C) 2011 Pedro Algarvio # # Deluge is free software. # @@ -40,7 +41,7 @@ import logging import deluge.component as component import deluge.configmanager as configmanager -from deluge.error import BadLoginError, AuthenticationRequired +from deluge.error import AuthManagerError, AuthenticationRequired, BadLoginError log = logging.getLogger(__name__) @@ -51,21 +52,35 @@ AUTH_LEVEL_ADMIN = 10 AUTH_LEVEL_DEFAULT = AUTH_LEVEL_NORMAL +AUTH_LEVELS_MAPPING = { + 'NONE': AUTH_LEVEL_NONE, + 'READONLY': AUTH_LEVEL_READONLY, + 'DEFAULT': AUTH_LEVEL_NORMAL, + 'NORMAL': AUTH_LEVEL_DEFAULT, + 'ADMIN': AUTH_LEVEL_ADMIN +} + +AUTH_LEVELS_MAPPING_REVERSE = {} +for key, value in AUTH_LEVELS_MAPPING.iteritems(): + AUTH_LEVELS_MAPPING_REVERSE[value] = key + class Account(object): - __slots__ = ('username', 'password', 'auth_level') - def __init__(self, username, password, auth_level): + __slots__ = ('username', 'password', 'authlevel') + def __init__(self, username, password, authlevel): self.username = username self.password = password - self.auth_level = auth_level + self.authlevel = authlevel - def data(self, include_private=True): - rv = self.__dict__.copy() - if not include_private: - rv['password'] = '' - return rv + def data(self): + return { + 'username': self.username, + 'password': self.password, + 'authlevel': AUTH_LEVELS_MAPPING_REVERSE[self.authlevel], + 'authlevel_int': self.authlevel + } def __repr__(self): - return ('' % + return ('' % self.__dict__) @@ -104,52 +119,71 @@ class AuthManager(component.Component): "Username and Password are required.", username ) - self.__test_existing_account(username) + if username not in self.__auth: + # Let's try to re-load the file.. Maybe it's been updated + self.__load_auth_file() + if username not in self.__auth: + raise BadLoginError("Username does not exist", username) if self.__auth[username].password == password: # Return the users auth level - return self.__auth[username].auth_level + return self.__auth[username].authlevel elif not password and self.__auth[username].password: raise AuthenticationRequired("Password is required", username) else: - raise BadLoginError("Password does not match") + raise BadLoginError("Password does not match", username) - def get_known_accounts(self, include_private_data=False): + def get_known_accounts(self): """ Returns a list of known deluge usernames. """ self.__load_auth_file() - rv = {} - for account in self.__auth.items(): - rv[account.username] = account.data(include_private_data) - return rv + return [account.data() for account in self.__auth.values()] - def create_account(self, username, password='', auth_level=AUTH_LEVEL_DEFAULT): + def create_account(self, username, password, authlevel): if username in self.__auth: - raise Something() - self.__create_account(username, password, auth_level) + raise AuthManagerError("Username in use.", username) + try: + self.__auth[username] = Account(username, password, + AUTH_LEVELS_MAPPING[authlevel]) + self.write_auth_file() + return True + except Exception, err: + log.exception(err) + raise err - def update_account(self, username, password='', auth_level=AUTH_LEVEL_DEFAULT): - if username in self.__auth: - raise Something() - self.__create_account(username, password, auth_level) + def update_account(self, username, password, authlevel): + if username not in self.__auth: + raise AuthManagerError("Username not known", username) + try: + self.__auth[username].username = username + self.__auth[username].password = password + self.__auth[username].authlevel = AUTH_LEVELS_MAPPING[authlevel] + self.write_auth_file() + return True + except Exception, err: + log.exception(err) + raise err def remove_account(self, username): - if username in self.__auth: - raise Something() + if username not in self.__auth: + raise AuthManagerError("Username not known", username) + elif username == component.get("RPCServer").get_session_user(): + raise AuthManagerError( + "You cannot delete your own account while logged in!", username + ) + del self.__auth[username] self.write_auth_file() - if component.get("RPCServer").get_session_user() == username: - # Force a client logout by the server - component.get("RPCServer").logout_current_session() + return True def write_auth_file(self): old_auth_file = configmanager.get_config_dir("auth") new_auth_file = old_auth_file + '.new' fd = open(new_auth_file, "w") - for account in self.__auth.items(): + for account in self.__auth.values(): fd.write( - "%(username)s:%(password)s:%(auth_level)s\n" % account.__dict__ + "%(username)s:%(password)s:%(authlevel_int)s\n" % account.data() ) fd.flush() os.fsync(fd.fileno()) @@ -157,18 +191,6 @@ class AuthManager(component.Component): os.rename(new_auth_file, old_auth_file) self.__load_auth_file() - def __add_account(self, username, password, auth_level): - self.__auth[username] = Account(username, password, auth_level) - self.write_auth_file() - - def __test_existing_account(self, username): - if username not in self.__auth: - # Let's try to re-load the file.. Maybe it's been updated - self.__load_auth_file() - if username not in self.__auth: - raise BadLoginError("Username does not exist") - return True - def __create_localclient_account(self): """ Returns the string. @@ -217,23 +239,27 @@ class AuthManager(component.Component): log.warning("Your auth entry for %s contains no auth level, " "using AUTH_LEVEL_DEFAULT(%s)..", username, AUTH_LEVEL_DEFAULT) - auth_level = AUTH_LEVEL_DEFAULT + authlevel = AUTH_LEVEL_DEFAULT elif len(lsplit) == 3: - username, password, auth_level = lsplit + username, password, authlevel = lsplit else: - log.error("Your auth file is malformed: Incorrect number of fields!") + log.error("Your auth file is malformed: " + "Incorrect number of fields!") continue username = username.strip() password = password.strip() try: - auth_level = int(auth_level) + authlevel = int(authlevel) except ValueError: - log.error("Your auth file is malformed: %r is not a valid auth " - "level" % auth_level) + try: + authlevel = AUTH_LEVELS_MAPPING[authlevel] + except KeyError: + log.error("Your auth file is malformed: %r is not a valid auth " + "level" % authlevel) continue - self.__auth[username] = Account(username, password, auth_level) + self.__auth[username] = Account(username, password, authlevel) if "localclient" not in self.__auth: self.__create_localclient_account() diff --git a/deluge/core/core.py b/deluge/core/core.py index 225979730..6586fff05 100644 --- a/deluge/core/core.py +++ b/deluge/core/core.py @@ -2,6 +2,7 @@ # core.py # # Copyright (C) 2007-2009 Andrew Resch +# Copyright (C) 2011 Pedro Algarvio # # Deluge is free software. # @@ -43,8 +44,6 @@ import threading import tempfile from urlparse import urljoin -from twisted.internet import reactor, defer -from twisted.internet.task import LoopingCall import twisted.web.client import twisted.web.error @@ -55,7 +54,8 @@ import deluge.common import deluge.component as component from deluge.event import * from deluge.error import * -from deluge.core.authmanager import AUTH_LEVEL_ADMIN, AUTH_LEVEL_DEFAULT +from deluge.core.authmanager import AUTH_LEVEL_ADMIN, AUTH_LEVEL_NONE +from deluge.core.authmanager import AUTH_LEVELS_MAPPING, AUTH_LEVELS_MAPPING_REVERSE from deluge.core.torrentmanager import TorrentManager from deluge.core.pluginmanager import PluginManager from deluge.core.alertmanager import AlertManager @@ -77,7 +77,8 @@ class Core(component.Component): log.info("Starting libtorrent %s session..", lt.version) # Create the client fingerprint - version = [int(value.split("-")[0]) for value in deluge.common.get_version().split(".")] + version = [int(value.split("-")[0]) for value in + deluge.common.get_version().split(".")] while len(version) < 4: version.append(0) @@ -147,16 +148,16 @@ class Core(component.Component): def __save_session_state(self): """Saves the libtorrent session state""" try: - open(deluge.configmanager.get_config_dir("session.state"), "wb").write( - lt.bencode(self.session.state())) + session_state = deluge.configmanager.get_config_dir("session.state") + open(session_state, "wb").write(lt.bencode(self.session.state())) except Exception, e: log.warning("Failed to save lt state: %s", e) def __load_session_state(self): """Loads the libtorrent session state""" try: - self.session.load_state(lt.bdecode( - open(deluge.configmanager.get_config_dir("session.state"), "rb").read())) + session_state = deluge.configmanager.get_config_dir("session.state") + self.session.load_state(lt.bdecode(open(session_state, "rb").read())) except Exception, e: log.warning("Failed to load lt state: %s", e) @@ -212,7 +213,9 @@ class Core(component.Component): log.exception(e) try: - torrent_id = self.torrentmanager.add(filedump=filedump, options=options, filename=filename) + torrent_id = self.torrentmanager.add( + filedump=filedump, options=options, filename=filename + ) except Exception, e: log.error("There was an error adding the torrent file %s", filename) log.exception(e) @@ -245,16 +248,23 @@ class Core(component.Component): os.remove(filename) except Exception, e: log.warning("Couldn't remove temp file: %s", e) - return self.add_torrent_file(filename, base64.encodestring(data), options) + return self.add_torrent_file( + filename, base64.encodestring(data), options + ) def on_download_fail(failure): if failure.check(twisted.web.error.PageRedirect): new_url = urljoin(url, failure.getErrorMessage().split(" to ")[1]) - result = download_file(new_url, tempfile.mkstemp()[1], headers=headers, force_filename=True) + result = download_file( + new_url, tempfile.mkstemp()[1], headers=headers, + force_filename=True + ) result.addCallbacks(on_download_success, on_download_fail) elif failure.check(twisted.web.client.PartialDownloadError): - result = download_file(url, tempfile.mkstemp()[1], headers=headers, force_filename=True, - allow_compression=False) + result = download_file( + url, tempfile.mkstemp()[1], headers=headers, + force_filename=True, allow_compression=False + ) result.addCallbacks(on_download_success, on_download_fail) else: # Log the error and pass the failure onto the client @@ -263,7 +273,9 @@ class Core(component.Component): result = failure return result - d = download_file(url, tempfile.mkstemp()[1], headers=headers, force_filename=True) + d = download_file( + url, tempfile.mkstemp()[1], headers=headers, force_filename=True + ) d.addCallbacks(on_download_success, on_download_fail) return d @@ -829,9 +841,22 @@ class Core(component.Component): """ return lt.version - @export(AUTH_LEVEL_DEFAULT) + @export(AUTH_LEVEL_ADMIN) def get_known_accounts(self): - auth_level = component.get("RPCServer").get_session_auth_level() - return self.authmanager.get_known_accounts( - include_private_data=(auth_level==AUTH_LEVEL_ADMIN) - ) + return self.authmanager.get_known_accounts() + + @export(AUTH_LEVEL_NONE) + def get_auth_levels_mappings(self): + return (AUTH_LEVELS_MAPPING, AUTH_LEVELS_MAPPING_REVERSE) + + @export(AUTH_LEVEL_ADMIN) + def create_account(self, username, password, authlevel): + return self.authmanager.create_account(username, password, authlevel) + + @export(AUTH_LEVEL_ADMIN) + def update_account(self, username, password, authlevel): + return self.authmanager.update_account(username, password, authlevel) + + @export(AUTH_LEVEL_ADMIN) + def remove_account(self, username): + return self.authmanager.remove_account(username) diff --git a/deluge/error.py b/deluge/error.py index 055d6b338..cdb67091a 100644 --- a/deluge/error.py +++ b/deluge/error.py @@ -2,6 +2,7 @@ # error.py # # Copyright (C) 2008 Andrew Resch +# Copyright (C) 2011 Pedro Algarvio # # Deluge is free software. # @@ -52,10 +53,9 @@ class InvalidPathError(DelugeError): class NotAuthorizedError(DelugeError): pass -class BadLoginError(DelugeError): - pass -class AuthenticationRequired(BadLoginError): +class _UsernameBasedException(DelugeError): + def _get_message(self): return self._message def _set_message(self, message): @@ -70,9 +70,17 @@ class AuthenticationRequired(BadLoginError): username = property(_get_username, _set_username) del _get_username, _set_username - def __init__(self, message, username): - super(AuthenticationRequired, self).__init__(message) + super(_UsernameBasedException, self).__init__(message) self.message = message self.username = username + +class BadLoginError(_UsernameBasedException): + pass + +class AuthenticationRequired(BadLoginError): + pass + +class AuthManagerError(_UsernameBasedException): + pass diff --git a/deluge/ui/client.py b/deluge/ui/client.py index 26cef631f..049595383 100644 --- a/deluge/ui/client.py +++ b/deluge/ui/client.py @@ -2,6 +2,7 @@ # client.py # # Copyright (C) 2009 Andrew Resch +# Copyright (C) 2011 Pedro Algarvio # # Deluge is free software. # @@ -76,6 +77,20 @@ class DelugeRPCError(object): self.exception_msg = exception_msg self.traceback = traceback + def logable(self): + # Create a delugerpcrequest to print out a nice RPCRequest string + r = DelugeRPCRequest() + r.method = self.method + r.args = self.args + r.kwargs = self.kwargs + msg = "RPCError Message Received!" + msg += "\n" + "-" * 80 + msg += "\n" + "RPCRequest: " + r.__repr__() + msg += "\n" + "-" * 80 + msg += "\n" + self.traceback + "\n" + self.exception_type + ": " + self.exception_msg + msg += "\n" + "-" * 80 + return msg + class DelugeRPCRequest(object): """ This object is created whenever there is a RPCRequest to be sent to the @@ -118,6 +133,7 @@ class DelugeRPCRequest(object): return (self.request_id, self.method, self.args, self.kwargs) class DelugeRPCProtocol(Protocol): + def connectionMade(self): self.__rpc_requests = {} self.__buffer = None @@ -273,6 +289,9 @@ class DaemonSSLProxy(DaemonProxy): self.disconnect_deferred = None self.disconnect_callback = None + self.auth_levels_mapping = None + self.auth_levels_mapping_reverse = None + def connect(self, host, port): """ Connects to a daemon at host:port @@ -402,21 +421,8 @@ class DaemonSSLProxy(DaemonProxy): except: pass - # Get the DelugeRPCError object from the error_data - error = error_data.value - - # Create a delugerpcrequest to print out a nice RPCRequest string - r = DelugeRPCRequest() - r.method = error.method - r.args = error.args - r.kwargs = error.kwargs - msg = "RPCError Message Received!" - msg += "\n" + "-" * 80 - msg += "\n" + "RPCRequest: " + r.__repr__() - msg += "\n" + "-" * 80 - msg += "\n" + error.traceback + "\n" + error.exception_type + ": " + error.exception_msg - msg += "\n" + "-" * 80 - log.error(msg) + if error_data.value.exception_type != 'AuthManagerError': + log.error(error_data.value.logable()) return error_data def __on_connect(self, result): @@ -452,13 +458,24 @@ class DaemonSSLProxy(DaemonProxy): self.authentication_level = result # We need to tell the daemon what events we're interested in receiving if self.__factory.event_handlers: - self.call("daemon.set_event_interest", self.__factory.event_handlers.keys()) + self.call("daemon.set_event_interest", + self.__factory.event_handlers.keys()) + + self.call("core.get_auth_levels_mappings").addCallback( + self.__on_auth_levels_mappings + ) + self.login_deferred.callback(result) def __on_login_fail(self, result): log.debug("_on_login_fail(): %s", result) self.login_deferred.errback(result) + def __on_auth_levels_mappings(self, result): + auth_levels_mapping, auth_levels_mapping_reverse = result + self.auth_levels_mapping = auth_levels_mapping + self.auth_levels_mapping_reverse = auth_levels_mapping_reverse + def set_disconnect_callback(self, cb): """ Set a function to be called when the connection to the daemon is lost @@ -481,9 +498,13 @@ class DaemonClassicProxy(DaemonProxy): self.host = "localhost" self.port = 58846 # Running in classic mode, it's safe to import auth level - from deluge.core.authmanager import AUTH_LEVEL_ADMIN + from deluge.core.authmanager import (AUTH_LEVEL_ADMIN, + AUTH_LEVELS_MAPPING, + AUTH_LEVELS_MAPPING_REVERSE) self.username = "localclient" self.authentication_level = AUTH_LEVEL_ADMIN + self.auth_levels_mapping = AUTH_LEVELS_MAPPING + self.auth_levels_mapping_reverse = AUTH_LEVELS_MAPPING_REVERSE # Register the event handlers for event in event_handlers: for handler in event_handlers[event]: @@ -776,5 +797,13 @@ class Client(object): """ return self._daemon_proxy.authentication_level + @property + def auth_levels_mapping(self): + return self._daemon_proxy.auth_levels_mapping + + @property + def auth_levels_mapping_reverse(self): + return self._daemon_proxy.auth_levels_mapping_reverse + # This is the object clients will use client = Client() diff --git a/deluge/ui/gtkui/dialogs.py b/deluge/ui/gtkui/dialogs.py index 633ac3e64..d0c23471b 100644 --- a/deluge/ui/gtkui/dialogs.py +++ b/deluge/ui/gtkui/dialogs.py @@ -176,7 +176,7 @@ class ErrorDialog(BaseDialog): details = tb if details: - self.set_default_size(500, 400) + self.set_default_size(600, 400) textview = gtk.TextView() textview.set_editable(False) textview.get_buffer().set_text(details) @@ -245,3 +245,87 @@ class AuthenticationDialog(BaseDialog): def on_password_activate(self, widget): self.response(gtk.RESPONSE_OK) + +class AccountDialog(BaseDialog): + def __init__(self, username=None, password=None, authlevel=None, + levels_mapping=None, parent=None): + if username: + super(AccountDialog, self).__init__( + _("Edit Account"), + _("Edit existing account"), + gtk.STOCK_DIALOG_INFO, + (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, + gtk.STOCK_APPLY, gtk.RESPONSE_OK), + parent) + else: + super(AccountDialog, self).__init__( + _("New Account"), + _("Create a new account"), + gtk.STOCK_DIALOG_INFO, + (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, + gtk.STOCK_ADD, gtk.RESPONSE_OK), + parent) + + self.levels_mapping = levels_mapping + + table = gtk.Table(2, 3, False) + self.username_label = gtk.Label() + self.username_label.set_markup(_("Username:")) + self.username_label.set_alignment(1.0, 0.5) + self.username_label.set_padding(5, 5) + self.username_entry = gtk.Entry() + table.attach(self.username_label, 0, 1, 0, 1) + table.attach(self.username_entry, 1, 2, 0, 1) + + self.authlevel_label = gtk.Label() + self.authlevel_label.set_markup(_("Authentication Level:")) + self.authlevel_label.set_alignment(1.0, 0.5) + self.authlevel_label.set_padding(5, 5) + + self.authlevel_combo = gtk.combo_box_new_text() + active_idx = None + for idx, level in enumerate(levels_mapping.keys()): + self.authlevel_combo.append_text(level) + if authlevel and authlevel==level: + active_idx = idx + elif not authlevel and level == 'DEFAULT': + active_idx = idx + + print 'aidx', active_idx + if active_idx is not None: + self.authlevel_combo.set_active(active_idx) + + table.attach(self.authlevel_label, 0, 1, 1, 2) + table.attach(self.authlevel_combo, 1, 2, 1, 2) + + self.password_label = gtk.Label() + self.password_label.set_markup(_("Password:")) + self.password_label.set_alignment(1.0, 0.5) + self.password_label.set_padding(5, 5) + self.password_entry = gtk.Entry() + self.password_entry.set_visibility(False) + table.attach(self.password_label, 0, 1, 2, 3) + table.attach(self.password_entry, 1, 2, 2, 3) + + self.vbox.pack_start(table, False, False, padding=5) + if username: + self.username_entry.set_text(username) + self.username_entry.set_editable(False) + else: + self.set_focus(self.username_entry) + + if password: + self.password_entry.set_text(username) + + self.show_all() + + def get_username(self): + return self.username_entry.get_text() + + def get_password(self): + return self.password_entry.get_text() + + def get_authlevel(self): + combobox = self.authlevel_combo + level = combobox.get_model()[combobox.get_active()][0] + return level diff --git a/deluge/ui/gtkui/glade/preferences_dialog.glade b/deluge/ui/gtkui/glade/preferences_dialog.glade index 76c71ce58..9383846a0 100644 --- a/deluge/ui/gtkui/glade/preferences_dialog.glade +++ b/deluge/ui/gtkui/glade/preferences_dialog.glade @@ -1,4 +1,4 @@ - + @@ -11,13 +11,11 @@ 530 True dialog - False True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - vertical 2 @@ -50,7 +48,7 @@ False True - + True True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK @@ -66,7 +64,6 @@ True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - vertical True @@ -294,7 +291,7 @@ True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - + 1 @@ -501,7 +498,7 @@ - + True True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK @@ -517,7 +514,6 @@ True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - vertical True @@ -557,7 +553,6 @@ True - vertical True @@ -755,7 +750,6 @@ True - vertical 5 @@ -880,7 +874,7 @@ True Enter the IP address of the interface to listen for incoming bittorrent connections on. Leave this empty if you want to use the default. 60 - + 30 @@ -927,7 +921,6 @@ True - vertical True @@ -1127,7 +1120,6 @@ True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - vertical True @@ -1159,7 +1151,6 @@ True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - vertical True @@ -1193,7 +1184,6 @@ Either True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - vertical True @@ -1291,7 +1281,7 @@ Disabled - + True True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK @@ -1307,7 +1297,6 @@ Disabled True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - vertical True @@ -1347,7 +1336,6 @@ Disabled True - vertical 5 @@ -1361,7 +1349,7 @@ Disabled True True 1 - -1 -1 9999 1 10 0 + 0 -1 9999 1 10 0 True @@ -1377,7 +1365,7 @@ Disabled True True 1 - -1 -1 9999 1 10 0 + 0 -1 9999 1 10 0 True @@ -1456,7 +1444,7 @@ Disabled The maximum number of connections allowed. Set -1 for unlimited. 4 1 - -1 -1 9000 1 10 0 + 0 -1 9000 1 10 0 1 True True @@ -1488,7 +1476,7 @@ Disabled GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK The maximum download speed for all torrents. Set -1 for unlimited. 1 - -1 -1 60000 1 10 0 + 0 -1 60000 1 10 0 1 1 True @@ -1507,7 +1495,7 @@ Disabled True The maximum upload speed for all torrents. Set -1 for unlimited. 1 - -1 -1 60000 1 10 0 + 0 -1 60000 1 10 0 1 1 True @@ -1526,7 +1514,7 @@ Disabled True The maximum upload slots for all torrents. Set -1 for unlimited. 1 - -1 -1 9000 1 10 0 + 0 -1 9000 1 10 0 1 True True @@ -1631,7 +1619,7 @@ Disabled True The maximum upload slots per torrent. Set -1 for unlimited. 1 - -1 -1 9000 1 10 0 + 0 -1 9000 1 10 0 1 True True @@ -1650,7 +1638,7 @@ Disabled True The maximum number of connections per torrent. Set -1 for unlimited. 1 - -1 -1 9000 1 10 0 + 0 -1 9000 1 10 0 True True @@ -1716,7 +1704,7 @@ Disabled True The maximum number of connections per torrent. Set -1 for unlimited. 1 - -1 -1 9000 1 10 0 + 0 -1 9000 1 10 0 1 True @@ -1734,7 +1722,7 @@ Disabled True The maximum number of connections per torrent. Set -1 for unlimited. 1 - -1 -1 9000 1 10 0 + 0 -1 9000 1 10 0 1 True @@ -1790,7 +1778,7 @@ Disabled - + True True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK @@ -1806,7 +1794,6 @@ Disabled True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - vertical True @@ -1892,7 +1879,6 @@ Disabled True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - vertical Show session speed in titlebar @@ -1943,7 +1929,6 @@ Disabled True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - vertical Always show @@ -2013,7 +1998,6 @@ Disabled True - vertical Enable system tray icon @@ -2196,7 +2180,7 @@ Disabled - + True True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK @@ -2212,7 +2196,6 @@ Disabled True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - vertical True @@ -2254,7 +2237,6 @@ Disabled True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - vertical True @@ -2316,7 +2298,6 @@ Disabled True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - vertical True @@ -2392,7 +2373,6 @@ Disabled True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - vertical True @@ -2417,7 +2397,7 @@ Disabled True True If Deluge cannot find the database file at this location it will fallback to using DNS to resolve the peer's country. - + 1 @@ -2530,7 +2510,7 @@ Disabled - + True True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK @@ -2546,7 +2526,6 @@ Disabled True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - vertical True @@ -2588,7 +2567,6 @@ Disabled True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - vertical True @@ -2736,6 +2714,111 @@ Disabled 4 + + + True + 0 + none + + + True + 12 + + + True + + + True + True + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + + + 0 + + + + + True + 5 + True + start + + + gtk-add + True + True + True + True + + + + False + False + 0 + + + + + gtk-edit + True + False + True + True + True + + + + False + False + 1 + + + + + gtk-delete + True + False + True + True + True + + + + False + False + 2 + + + + + False + 4 + 1 + + + + + + + + + True + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + 0 + 10 + 10 + <b>Accounts</b> + True + + + label_item + + + + + 5 + + @@ -2757,7 +2840,7 @@ Disabled - + True True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK @@ -2773,7 +2856,6 @@ Disabled True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - vertical True @@ -2803,7 +2885,6 @@ Disabled True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - vertical 5 @@ -2821,7 +2902,6 @@ Disabled True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - vertical Queue new torrents to top @@ -2872,7 +2952,6 @@ Disabled True - vertical 5 @@ -3024,7 +3103,6 @@ Disabled True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - vertical 2 @@ -3234,7 +3312,7 @@ Disabled - + True True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK @@ -3250,7 +3328,6 @@ Disabled True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - vertical True @@ -3278,7 +3355,6 @@ Disabled True - vertical 5 @@ -3372,7 +3448,7 @@ Disabled True True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - 8080 0 65535 1 10 0 + 100 0 65535 1 10 0 True @@ -3556,7 +3632,7 @@ HTTP W/ Auth True True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - 8080 0 65535 1 10 0 + 100 0 65535 1 10 0 True @@ -3740,7 +3816,7 @@ HTTP W/ Auth True True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - 8080 0 65535 1 10 0 + 100 0 65535 1 10 0 True @@ -3927,7 +4003,7 @@ HTTP W/ Auth True True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - 8080 0 65535 1 10 0 + 100 0 65535 1 10 0 True @@ -4042,7 +4118,7 @@ HTTP W/ Auth - + True True automatic @@ -4055,7 +4131,6 @@ HTTP W/ Auth True - vertical True @@ -4087,7 +4162,6 @@ HTTP W/ Auth True - vertical True @@ -4132,9 +4206,9 @@ HTTP W/ Auth True True - + 1 - 512 0 99999 1 10 0 + 100 0 99999 1 10 0 True if-valid @@ -4149,7 +4223,7 @@ HTTP W/ Auth True True 5 - + 5 1 60 1 32000 1 10 0 @@ -4197,7 +4271,6 @@ HTTP W/ Auth True - vertical True @@ -4590,7 +4663,7 @@ HTTP W/ Auth - + True True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK @@ -4606,7 +4679,6 @@ HTTP W/ Auth True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - vertical True @@ -4637,7 +4709,6 @@ HTTP W/ Auth True True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - vertical True @@ -4996,7 +5067,7 @@ HTTP W/ Auth - 9 + 2 diff --git a/deluge/ui/gtkui/menubar.py b/deluge/ui/gtkui/menubar.py index 88b19c9a4..6026f1387 100644 --- a/deluge/ui/gtkui/menubar.py +++ b/deluge/ui/gtkui/menubar.py @@ -2,6 +2,7 @@ # menubar.py # # Copyright (C) 2007, 2008 Andrew Resch +# Copyright (C) 2011 Pedro Algarvio # # Deluge is free software. # @@ -212,7 +213,9 @@ class MenuBar(component.Component): self.menuitem_change_owner.set_visible(False) # Get Known accounts to allow chaning ownership - client.core.get_known_accounts().addCallback(self._on_known_accounts) + client.core.get_known_accounts().addCallback( + self._on_known_accounts).addErrback(self._on_known_accounts_fail + ) def stop(self): log.debug("MenuBar stopping") @@ -354,9 +357,9 @@ class MenuBar(component.Component): def show_move_storage_dialog(self, status): log.debug("show_move_storage_dialog") - glade = gtk.glade.XML( - pkg_resources.resource_filename("deluge.ui.gtkui", - "glade/move_storage_dialog.glade")) + glade = gtk.glade.XML(pkg_resources.resource_filename( + "deluge.ui.gtkui", "glade/move_storage_dialog.glade" + )) dialog = glade.get_widget("move_storage_dialog") dialog.set_transient_for(self.window.window) entry = glade.get_widget("entry_destination") @@ -439,10 +442,21 @@ class MenuBar(component.Component): } # widget: (header, type_str, image_stockid, image_filename, default) other_dialog_info = { - "menuitem_down_speed": (_("Set Maximum Download Speed"), "KiB/s", None, "downloading.svg", -1.0), - "menuitem_up_speed": (_("Set Maximum Upload Speed"), "KiB/s", None, "seeding.svg", -1.0), - "menuitem_max_connections": (_("Set Maximum Connections"), "", gtk.STOCK_NETWORK, None, -1), - "menuitem_upload_slots": (_("Set Maximum Upload Slots"), "", gtk.STOCK_SORT_ASCENDING, None, -1) + "menuitem_down_speed": ( + _("Set Maximum Download Speed"), + "KiB/s", None, "downloading.svg", -1.0 + ), + "menuitem_up_speed": ( + _("Set Maximum Upload Speed"), + "KiB/s", None, "seeding.svg", -1.0 + ), + "menuitem_max_connections": ( + _("Set Maximum Connections"), "", gtk.STOCK_NETWORK, None, -1 + ), + "menuitem_upload_slots": ( + _("Set Maximum Upload Slots"), + "", gtk.STOCK_SORT_ASCENDING, None, -1 + ) } # Show the other dialog @@ -483,7 +497,15 @@ class MenuBar(component.Component): getattr(self.window.main_glade.get_widget(item), attr)() def _on_known_accounts(self, known_accounts): - log.debug("_on_known_accounts: %s", known_accounts) + known_accounts_to_log = [] + for account in known_accounts: + account_to_log = {} + for key, value in account.copy().iteritems(): + if key == 'password': + value = '*' * len(value) + account_to_log[key] = value + known_accounts_to_log.append(account_to_log) + log.debug("_on_known_accounts: %s", known_accounts_to_log) if len(known_accounts) <= 1: return @@ -496,14 +518,18 @@ class MenuBar(component.Component): self.change_owner_submenu_items[None] = gtk.RadioMenuItem(maingroup) for account in known_accounts: - self.change_owner_submenu_items[account] = item = gtk.RadioMenuItem(maingroup, account) + username = account["username"] + item = gtk.RadioMenuItem(maingroup, username) + self.change_owner_submenu_items[username] = item self.change_owner_submenu.append(item) - item.connect("toggled", self._on_change_owner_toggled, account) + item.connect("toggled", self._on_change_owner_toggled, username) self.change_owner_submenu.show_all() self.change_owner_submenu_items[None].set_active(True) self.change_owner_submenu_items[None].hide() - self.menuitem_change_owner.connect("activate", self._on_change_owner_submenu_active) + self.menuitem_change_owner.connect( + "activate", self._on_change_owner_submenu_active + ) self.menuitem_change_owner.set_submenu(self.change_owner_submenu) def _on_known_accounts_fail(self, reason): @@ -517,18 +543,18 @@ class MenuBar(component.Component): return torrent_owner = component.get("TorrentView").get_torrent_status(selected[0])["owner"] - for account, item in self.change_owner_submenu_items.iteritems(): - item.set_active(account == torrent_owner) + for username, item in self.change_owner_submenu_items.iteritems(): + item.set_active(username == torrent_owner) - def _on_change_owner_toggled(self, widget, account): + def _on_change_owner_toggled(self, widget, username): log.debug("_on_change_owner_toggled") update_torrents = [] selected = component.get("TorrentView").get_selected_torrents() for torrent_id in selected: torrent_status = component.get("TorrentView").get_torrent_status(torrent_id) - if torrent_status["owner"] != account: + if torrent_status["owner"] != username: update_torrents.append(torrent_id) if update_torrents: - log.debug("Setting torrent owner \"%s\" on %s", account, update_torrents) - client.core.set_torrents_owner(update_torrents, account) + log.debug("Setting torrent owner \"%s\" on %s", username, update_torrents) + client.core.set_torrents_owner(update_torrents, username) diff --git a/deluge/ui/gtkui/preferences.py b/deluge/ui/gtkui/preferences.py index a13937a2d..508effa4a 100644 --- a/deluge/ui/gtkui/preferences.py +++ b/deluge/ui/gtkui/preferences.py @@ -2,6 +2,7 @@ # preferences.py # # Copyright (C) 2007, 2008 Andrew Resch +# Copyright (C) 2011 Pedro Algarvio # # Deluge is free software. # @@ -46,11 +47,14 @@ from deluge.ui.client import client import deluge.common import deluge.error import common +import dialogs from deluge.configmanager import ConfigManager import deluge.configmanager log = logging.getLogger(__name__) +ACCOUNTS_USERNAME, ACCOUNTS_LEVEL, ACCOUNTS_PASSWORD = range(3) + class Preferences(component.Component): def __init__(self): component.Component.__init__(self, "Preferences") @@ -81,6 +85,35 @@ class Preferences(component.Component): self.liststore.append([i, category]) i += 1 + # Setup accounts tab lisview + self.accounts_levels_mapping = None + self.accounts_authlevel = self.glade.get_widget("accounts_authlevel") + self.accounts_liststore = gtk.ListStore(str, str, str, int) + self.accounts_liststore.set_sort_column_id(ACCOUNTS_USERNAME, + gtk.SORT_ASCENDING) + self.accounts_listview = self.glade.get_widget("accounts_listview") + self.accounts_listview.append_column( + gtk.TreeViewColumn( + _("Username"), gtk.CellRendererText(), text=ACCOUNTS_USERNAME + ) + ) + self.accounts_listview.append_column( + gtk.TreeViewColumn( + _("Level"), gtk.CellRendererText(), text=ACCOUNTS_LEVEL + ) + ) + password_column = gtk.TreeViewColumn( + 'password', gtk.CellRendererText(), text=ACCOUNTS_PASSWORD + ) + self.accounts_listview.append_column(password_column) + password_column.set_visible(False) + self.accounts_listview.set_model(self.accounts_liststore) + + self.accounts_listview.get_selection().connect( + "changed", self._on_accounts_selection_changed + ) + self.accounts_frame = self.glade.get_widget("AccountsFrame") + # Setup plugin tab listview self.plugin_liststore = gtk.ListStore(str, bool) self.plugin_liststore.set_sort_column_id(0, gtk.SORT_ASCENDING) @@ -96,11 +129,13 @@ class Preferences(component.Component): # Connect to the 'changed' event of TreeViewSelection to get selection # changes. - self.treeview.get_selection().connect("changed", - self.on_selection_changed) + self.treeview.get_selection().connect( + "changed", self.on_selection_changed + ) - self.plugin_listview.get_selection().connect("changed", - self.on_plugin_selection_changed) + self.plugin_listview.get_selection().connect( + "changed", self.on_plugin_selection_changed + ) self.glade.signal_autoconnect({ "on_pref_dialog_delete_event": self.on_pref_dialog_delete_event, @@ -114,7 +149,10 @@ class Preferences(component.Component): "on_button_find_plugins_clicked": self._on_button_find_plugins_clicked, "on_button_cache_refresh_clicked": self._on_button_cache_refresh_clicked, "on_combo_proxy_type_changed": self._on_combo_proxy_type_changed, - "on_button_associate_magnet_clicked": self._on_button_associate_magnet_clicked + "on_button_associate_magnet_clicked": self._on_button_associate_magnet_clicked, + "on_accounts_add_clicked": self._on_accounts_add_clicked, + "on_accounts_delete_clicked": self._on_accounts_delete_clicked, + "on_accounts_edit_clicked": self._on_accounts_edit_clicked }) # These get updated by requests done to the core @@ -191,9 +229,12 @@ class Preferences(component.Component): component.get("PluginManager").run_on_show_prefs() + # Update the preferences dialog to reflect current config settings self.core_config = {} if client.connected(): + self._get_accounts_tab_data() + def _on_get_config(config): self.core_config = config client.core.get_available_plugins().addCallback(_on_get_available_plugins) @@ -832,6 +873,9 @@ class Preferences(component.Component): # Show the correct notebook page based on what row is selected. (model, row) = treeselection.get_selected() try: + if model.get_value(row, 1) == _("Daemon"): + # Let's see update the accounts related stuff + self._get_accounts_tab_data() self.notebook.set_current_page(model.get_value(row, 0)) except TypeError: pass @@ -961,3 +1005,169 @@ class Preferences(component.Component): def _on_button_associate_magnet_clicked(self, widget): common.associate_magnet_links(True) + + + def _get_accounts_tab_data(self): + def on_ok(accounts): + self.accounts_frame.show() + self._on_get_known_accounts(accounts) + + def on_fail(failure): + if failure.value.exception_type == 'NotAuthorizedError': + self.accounts_frame.hide() + else: + dialogs.ErrorDialog( + _("Server Side Error"), + _("An error ocurred on the server"), + self.pref_dialog, details=failure.value.logable() + ).run() + client.core.get_known_accounts().addCallback(on_ok).addErrback(on_fail) + + def _on_get_known_accounts(self, known_accounts): + known_accounts_to_log = [] + for account in known_accounts: + account_to_log = {} + for key, value in account.copy().iteritems(): + if key == 'password': + value = '*' * len(value) + account_to_log[key] = value + known_accounts_to_log.append(account_to_log) + log.debug("_on_known_accounts: %s", known_accounts_to_log) + + self.accounts_liststore.clear() + + for account in known_accounts: + iter = self.accounts_liststore.append() + self.accounts_liststore.set_value( + iter, ACCOUNTS_USERNAME, account['username'] + ) + self.accounts_liststore.set_value( + iter, ACCOUNTS_LEVEL, account['authlevel'] + ) + self.accounts_liststore.set_value( + iter, ACCOUNTS_PASSWORD, account['password'] + ) + + def _on_accounts_selection_changed(self, treeselection): + log.debug("_on_accounts_selection_changed") + (model, itr) = treeselection.get_selected() + if not itr: + return + username = model[itr][0] + if username: + self.glade.get_widget("accounts_edit").set_sensitive(True) + self.glade.get_widget("accounts_delete").set_sensitive(True) + else: + self.glade.get_widget("accounts_edit").set_sensitive(False) + self.glade.get_widget("accounts_delete").set_sensitive(False) + + def _on_accounts_add_clicked(self, widget): + dialog = dialogs.AccountDialog( + levels_mapping=client.auth_levels_mapping, + parent=self.pref_dialog + ) + + def dialog_finished(response_id): + username = dialog.get_username() + password = dialog.get_password() + authlevel = dialog.get_authlevel() + + def add_ok(rv): + iter = self.accounts_liststore.append() + self.accounts_liststore.set_value( + iter, ACCOUNTS_USERNAME, username + ) + self.accounts_liststore.set_value( + iter, ACCOUNTS_LEVEL, authlevel + ) + self.accounts_liststore.set_value( + iter, ACCOUNTS_PASSWORD, password + ) + + def add_fail(failure): + if failure.value.exception_type == 'AuthManagerError': + dialogs.ErrorDialog( + _("Error Adding Account"), + failure.value.exception_msg + ).run() + else: + dialogs.ErrorDialog( + _("Error Adding Account"), + _("An error ocurred while adding account"), + self.pref_dialog, details=failure.value.logable() + ).run() + + if response_id == gtk.RESPONSE_OK: + client.core.create_account( + username, password, authlevel + ).addCallback(add_ok).addErrback(add_fail) + + dialog.run().addCallback(dialog_finished) + + def _on_accounts_edit_clicked(self, widget): + (model, itr) = self.accounts_listview.get_selection().get_selected() + if not itr: + return + + dialog = dialogs.AccountDialog( + model[itr][ACCOUNTS_USERNAME], + model[itr][ACCOUNTS_PASSWORD], + model[itr][ACCOUNTS_LEVEL], + levels_mapping=client.auth_levels_mapping, + parent=self.pref_dialog + ) + + def dialog_finished(response_id): + + def update_ok(rc): + model.set_value(itr, ACCOUNTS_PASSWORD, dialog.get_username()) + model.set_value(itr, ACCOUNTS_LEVEL, dialog.get_authlevel()) + + def update_fail(failure): + dialogs.ErrorDialog( + _("Error Updating Account"), + _("An error ocurred while updating account"), + self.pref_dialog, details=failure.value.logable() + ).run() + + if response_id == gtk.RESPONSE_OK: + client.core.update_account( + dialog.get_username(), + dialog.get_password(), + dialog.get_authlevel() + ).addCallback(update_ok).addErrback(update_fail) + + dialog.run().addCallback(dialog_finished) + + def _on_accounts_delete_clicked(self, widget): + (model, itr) = self.accounts_listview.get_selection().get_selected() + if not itr: + return + + username = model[itr][0] + header = _("Remove Account") + text = _("Are you sure you wan't do remove the account with the " + "username \"%(username)s\"?" % dict(username=username)) + dialog = dialogs.YesNoDialog(header, text, parent=self.pref_dialog) + + def dialog_finished(response_id): + def remove_ok(rc): + model.remove(itr) + + def remove_fail(failure): + if failure.value.exception_type == 'AuthManagerError': + dialogs.ErrorDialog( + _("Error Removing Account"), + failure.value.exception_msg + ).run() + else: + dialogs.ErrorDialog( + _("Error Removing Account"), + _("An error ocurred while removing account"), + self.pref_dialog, details=failure.value.logable() + ).run() + if response_id == gtk.RESPONSE_YES: + client.core.remove_account( + username + ).addCallback(remove_ok).addErrback(remove_fail) + dialog.run().addCallback(dialog_finished)