from functools import partial from gi.repository import Gtk, GLib, Gdk from gajim.common import app from gajim import gtkgui_helpers from gajim import gui_menu_builder from gajim.common import passwords from gajim import dialogs from gajim import config from gajim.common import helpers from gajim.common import ged from gajim.common.connection import Connection from gajim.common.zeroconf.connection_zeroconf import ConnectionZeroconf from gajim.options_dialog import OptionsDialog, OptionsBox from gajim.common.const import Option, OptionKind, OptionType class AccountsWindow(Gtk.ApplicationWindow): def __init__(self): Gtk.ApplicationWindow.__init__(self) self.set_application(app.app) self.set_position(Gtk.WindowPosition.CENTER) self.set_show_menubar(False) self.set_name('AccountsWindow') self.set_default_size(700, 550) self.set_resizable(False) self.set_title(_('Accounts')) self.need_relogin = {} glade_objects = ['stack', 'box', 'account_list'] self.builder = gtkgui_helpers.get_gtk_builder('accounts_window.ui') for obj in glade_objects: setattr(self, obj, self.builder.get_object(obj)) self.account_list.add(Preferences(self)) account_item = AddAccount() self.account_list.add(account_item) account_item.set_activatable() accounts = app.config.get_per('accounts') accounts.sort() for account in accounts: self.need_relogin[account] = self.get_relogin_options(account) account_item = Account(account, self) self.account_list.add(account_item) account_item.set_activatable() self.add(self.box) self.builder.connect_signals(self) self.connect('destroy', self.on_destroy) self.connect('key-press-event', self.on_key_press) self._activate_preferences_page() self.show_all() app.ged.register_event_handler( 'our-show', ged.GUI2, self._nec_our_status) def _nec_our_status(self, event): self.update_accounts() def _activate_preferences_page(self): row = self.account_list.get_row_at_index(0) self.account_list.select_row(row) self.account_list.emit('row-activated', row) def on_key_press(self, widget, event): if event.keyval == Gdk.KEY_Escape: self.destroy() def on_destroy(self, *args): self.check_relogin() app.ged.remove_event_handler( 'our-show', ged.GUI2, self._nec_our_status) del app.interface.instances['accounts'] def on_child_visible(self, stack, *args): page = stack.get_visible_child_name() if page is None: return if page == 'account': self.check_relogin() def update_accounts(self): for row in self.account_list.get_children(): row.get_child().update() @staticmethod def on_row_activated(listbox, row): row.get_child().on_row_activated() def remove_all_pages(self): for page in self.stack.get_children(): self.stack.remove(page) def set_page(self, page, name): self.remove_all_pages() self.stack.add_named(page, name) page.update() page.show_all() self.stack.set_visible_child(page) def update_proxy_list(self): page = self.stack.get_child_by_name('connection') if page is None: return page.listbox.get_option('proxy').update_values() def check_relogin(self): for account in self.need_relogin: options = self.get_relogin_options(account) active = app.config.get_per('accounts', account, 'active') if options != self.need_relogin[account]: self.need_relogin[account] = options if active: self.relog(account) break def relog(self, account): if app.connections[account].connected == 0: return if account == app.ZEROCONF_ACC_NAME: app.connections[app.ZEROCONF_ACC_NAME].update_details() return def login(account, show_before, status_before): """ Login with previous status """ # first make sure connection is really closed, # 0.5 may not be enough app.connections[account].disconnect(True) app.interface.roster.send_status( account, show_before, status_before) def relog(account): show_before = app.SHOW_LIST[app.connections[account].connected] status_before = app.connections[account].status app.interface.roster.send_status( account, 'offline', _('Be right back.')) GLib.timeout_add(500, login, account, show_before, status_before) dialogs.YesNoDialog( _('Relogin now?'), _('If you want all the changes to apply instantly, ' 'you must relogin.'), transient_for=self, on_response_yes=lambda *args: relog(account)) @staticmethod def get_relogin_options(account): if account == app.ZEROCONF_ACC_NAME: options = ['zeroconf_first_name', 'zeroconf_last_name', 'zeroconf_jabber_id', 'zeroconf_email', 'keyid'] else: options = ['client_cert', 'proxy', 'resource', 'use_custom_host', 'custom_host', 'custom_port', 'keyid'] values = [] for option in options: values.append(app.config.get_per('accounts', account, option)) return values def on_remove_account(self, button, account): if app.events.get_events(account): app.interface.raise_dialog('unread-events-on-remove-account') return if app.config.get_per('accounts', account, 'is_zeroconf'): # Should never happen as button is insensitive return win_opened = False if app.interface.msg_win_mgr.get_controls(acct=account): win_opened = True elif account in app.interface.instances: for key in app.interface.instances[account]: if (app.interface.instances[account][key] and key != 'remove_account'): win_opened = True break # Detect if we have opened windows for this account def remove(account): if (account in app.interface.instances and 'remove_account' in app.interface.instances[account]): dialog = app.interface.instances[account]['remove_account'] dialog.window.present() else: if account not in app.interface.instances: app.interface.instances[account] = {} app.interface.instances[account]['remove_account'] = \ config.RemoveAccountWindow(account) if win_opened: dialogs.ConfirmationDialog( _('You have opened chat in account %s') % account, _('All chat and groupchat windows will be closed. ' 'Do you want to continue?'), on_response_ok=(remove, account), transient_for=self) else: remove(account) def remove_account(self, account): for row in self.account_list.get_children(): if row.get_child().account == account: self.account_list.remove(row) del self.need_relogin[account] break self._activate_preferences_page() def add_account(self, account): account_item = Account(account, self) self.account_list.add(account_item) account_item.set_activatable() self.account_list.show_all() self.stack.show_all() self.need_relogin[account] = self.get_relogin_options(account) def select_account(self, account): for row in self.account_list.get_children(): if row.get_child().account == account: self.account_list.select_row(row) self.account_list.emit('row-activated', row) break @staticmethod def enable_account(account): if account == app.ZEROCONF_ACC_NAME: app.connections[account] = ConnectionZeroconf(account) else: app.connections[account] = Connection(account) # update variables app.interface.instances[account] = { 'infos': {}, 'disco': {}, 'gc_config': {}, 'search': {}, 'online_dialog': {}, 'sub_request': {}} app.interface.minimized_controls[account] = {} app.connections[account].connected = 0 app.groups[account] = {} app.contacts.add_account(account) app.gc_connected[account] = {} app.automatic_rooms[account] = {} app.newly_added[account] = [] app.to_be_removed[account] = [] if account == app.ZEROCONF_ACC_NAME: app.nicks[account] = app.ZEROCONF_ACC_NAME else: app.nicks[account] = app.config.get_per( 'accounts', account, 'name') app.block_signed_in_notifications[account] = True app.sleeper_state[account] = 'off' app.encrypted_chats[account] = [] app.last_message_time[account] = {} app.status_before_autoaway[account] = '' app.transport_avatar[account] = {} app.gajim_optional_features[account] = [] app.caps_hash[account] = '' helpers.update_optional_features(account) # refresh roster if len(app.connections) >= 2: # Do not merge accounts if only one exists app.interface.roster.regroup = app.config.get('mergeaccounts') else: app.interface.roster.regroup = False app.interface.roster.setup_and_draw_roster() gui_menu_builder.build_accounts_menu() @staticmethod def disable_account(account): app.interface.roster.close_all(account) if account == app.ZEROCONF_ACC_NAME: app.connections[account].disable_account() app.connections[account].cleanup() del app.connections[account] del app.interface.instances[account] del app.interface.minimized_controls[account] del app.nicks[account] del app.block_signed_in_notifications[account] del app.groups[account] app.contacts.remove_account(account) del app.gc_connected[account] del app.automatic_rooms[account] del app.to_be_removed[account] del app.newly_added[account] del app.sleeper_state[account] del app.encrypted_chats[account] del app.last_message_time[account] del app.status_before_autoaway[account] del app.transport_avatar[account] del app.gajim_optional_features[account] del app.caps_hash[account] if len(app.connections) >= 2: # Do not merge accounts if only one exists app.interface.roster.regroup = app.config.get('mergeaccounts') else: app.interface.roster.regroup = False app.interface.roster.setup_and_draw_roster() gui_menu_builder.build_accounts_menu() class AddAccount(Gtk.Box): def __init__(self): Gtk.Box.__init__(self, orientation=Gtk.Orientation.HORIZONTAL, spacing=12) self.account = None self.label = Gtk.Label(_('Add Account…')) self.label.set_halign(Gtk.Align.START) self.label.set_hexpand(True) self.image = Gtk.Image.new_from_icon_name( 'list-add-symbolic', Gtk.IconSize.MENU) self.add(self.image) self.add(self.label) def set_activatable(self): self.get_parent().set_selectable(False) def on_row_activated(self): app.app.activate_action('add-account') def update(self): pass class Preferences(Gtk.Box): def __init__(self, parent): Gtk.Box.__init__(self, orientation=Gtk.Orientation.HORIZONTAL, spacing=12) self.options = PreferencesPage() self.parent = parent self.account = None self.label = Gtk.Label(_('Preferences')) self.label.set_halign(Gtk.Align.START) self.label.set_hexpand(True) self.image = Gtk.Image.new_from_icon_name( 'system-run-symbolic', Gtk.IconSize.MENU) self.add(self.image) self.add(self.label) def set_activatable(self): pass def on_row_activated(self): self.options.update_states() self.parent.set_page(self.options, 'pref') def update(self): pass class Account(Gtk.Box): def __init__(self, account, parent): Gtk.Box.__init__(self, orientation=Gtk.Orientation.HORIZONTAL, spacing=12) self.account = account if account == app.ZEROCONF_ACC_NAME: self.options = ZeroConfPage(account, parent) else: self.options = AccountPage(account, parent) self.parent = parent account_label = app.config.get_per('accounts', account, 'account_label') self.label = Gtk.Label(label=account_label or account) self.label.set_halign(Gtk.Align.START) self.label.set_hexpand(True) self.image = Gtk.Image() self._update_image() self.add(self.image) self.add(self.label) def set_activatable(self): if self.account == app.ZEROCONF_ACC_NAME: zeroconf = app.is_installed('ZEROCONF') self.get_parent().set_activatable(zeroconf) self.get_parent().set_sensitive(zeroconf) if not zeroconf: self.get_parent().set_tooltip_text( _('Please check if Avahi or Bonjour is installed.')) def on_row_activated(self): self.options.update_states() self.parent.set_page(self.options, 'account') def update(self): account_label = app.config.get_per( 'accounts', self.account, 'account_label') self.label.set_text(account_label or self.account) self._update_image() def _update_image(self): show = helpers.get_current_show(self.account) icon = gtkgui_helpers.get_iconset_name_for(show) self.image.set_from_icon_name(icon, Gtk.IconSize.MENU) class GenericOptionPage(Gtk.Box): def __init__(self, account, parent, options): Gtk.Box.__init__(self, orientation=Gtk.Orientation.VERTICAL, spacing=12) self.account = account self.parent = parent button = Gtk.Button.new_from_icon_name( 'go-previous-symbolic', Gtk.IconSize.MENU) button.set_halign(Gtk.Align.START) button.connect('clicked', self._on_back_button) if not isinstance(self, (AccountPage, PreferencesPage, ZeroConfPage)): self.pack_start(button, False, True, 0) self.listbox = OptionsBox(account) self.listbox.set_selection_mode(Gtk.SelectionMode.NONE) self.listbox.set_vexpand(False) self.listbox.set_valign(Gtk.Align.END) for option in options: self.listbox.add_option(option) self.listbox.update_states() self.pack_end(self.listbox, True, True, 0) self.listbox.connect('row-activated', self.on_row_activated) def _on_back_button(self, *args): account_window = self.get_toplevel() child = account_window.stack.get_visible_child() account_window.remove_all_pages() account_window.stack.add_named(child.parent, 'account') account_window.stack.set_visible_child_name('account') def update_states(self): self.listbox.update_states() def on_row_activated(self, listbox, row): row.get_child().on_row_activated() def set_entry_text(self, toggle, update=False): account_label = app.config.get_per( 'accounts', self.account, 'account_label') if update: self.entry.set_text(account_label or self.account) return if toggle.get_active(): self.entry.set_sensitive(True) self.entry.grab_focus() else: self.entry.set_sensitive(False) value = self.entry.get_text() if not value: value = account_label or self.account app.config.set_per('accounts', self.account, 'account_label', value or self.account) if app.config.get_per('accounts', self.account, 'active'): app.interface.roster.draw_account(self.account) gui_menu_builder.build_accounts_menu() def update(self): pass def set_page(self, options, name): options.update_states() self.get_toplevel().set_page(options, name) def _add_top_buttons(self, parent): # This adds the Account enable switch and the back button box = Gtk.Box() box.set_hexpand(True) box.set_halign(Gtk.Align.FILL) switch = Gtk.Switch() switch.set_active(app.config.get_per('accounts', self.account, 'active')) switch.set_vexpand(False) switch.set_valign(Gtk.Align.CENTER) switch.set_halign(Gtk.Align.END) if self.account == app.ZEROCONF_ACC_NAME and not app.is_installed('ZEROCONF'): switch.set_sensitive(False) switch.set_active(False) switch.connect('notify::active', self._on_enable_switch, self.account) box.pack_start(switch, False, False, 0) if self.account != app.ZEROCONF_ACC_NAME: button = Gtk.Button(label=_('Remove')) button.connect( 'clicked', parent.on_remove_account, self.account) button.get_style_context().add_class('destructive-action') button.set_halign(Gtk.Align.END) switch.set_vexpand(False) box.pack_end(button, False, False, 0) self.pack_start(box, True, True, 0) def _on_enable_switch(self, switch, param, account): old_state = app.config.get_per('accounts', account, 'active') state = switch.get_active() if old_state == state: return if (account in app.connections and app.connections[account].connected > 0): # connecting or connected app.interface.raise_dialog('connected-on-disable-account') switch.set_active(not state) return if state: self.parent.enable_account(account) else: self.parent.disable_account(account) app.config.set_per('accounts', account, 'active', state) class PreferencesPage(GenericOptionPage): def __init__(self): options = [ Option(OptionKind.SWITCH, _('Merge Accounts'), OptionType.ACTION, 'merge'), Option(OptionKind.SWITCH, _('Use PGP Agent'), OptionType.ACTION, 'agent'), ] GenericOptionPage.__init__(self, None, None, options) class AccountPage(GenericOptionPage): def __init__(self, account, parent=None): general = partial( self.set_page, GeneralPage(account, self), 'general') connection = partial( self.set_page, ConnectionPage(account, self), 'connection') options = [ Option(OptionKind.ENTRY, _('Label'), OptionType.ACCOUNT_CONFIG, 'account_label', callback=self._on_account_name_change), Option(OptionKind.LOGIN, _('Login'), OptionType.DIALOG, props={'dialog': LoginDialog}), Option(OptionKind.ACTION, _('Profile'), OptionType.ACTION, '-profile', props={'action_args': account}), Option(OptionKind.CALLBACK, _('General'), name='general', props={'callback': general}), Option(OptionKind.CALLBACK, _('Connection'), name='connection', props={'callback': connection}), Option(OptionKind.ACTION, _('Import Contacts'), OptionType.ACTION, '-import-contacts', props={'action_args': account}), Option(OptionKind.DIALOG, _('Client Certificate'), OptionType.DIALOG, props={'dialog': CertificateDialog}), Option(OptionKind.GPG, _('OpenPGP Key'), OptionType.DIALOG, props={'dialog': None}), ] GenericOptionPage.__init__(self, account, parent, options) self._add_top_buttons(parent) def _on_account_name_change(self, account_name, *args): self.parent.update_accounts() class GeneralPage(GenericOptionPage): def __init__(self, account, parent=None): options = [ Option(OptionKind.SWITCH, _('Connect on startup'), OptionType.ACCOUNT_CONFIG, 'autoconnect'), Option(OptionKind.SWITCH, _('Reconnect when connection is lost'), OptionType.ACCOUNT_CONFIG, 'autoreconnect'), Option(OptionKind.SWITCH, _('Save conversations for all contacts'), OptionType.ACCOUNT_CONFIG, 'no_log_for', desc=_('Store conversations on the harddrive')), Option(OptionKind.SWITCH, _('Server Message Archive'), OptionType.ACCOUNT_CONFIG, 'sync_logs_with_server', desc=_('Messages get stored on the server.\n' 'The archive is used to sync messages\n' 'between multiple devices.\n' 'XEP-0313')), Option(OptionKind.SWITCH, _('Global Status'), OptionType.ACCOUNT_CONFIG, 'sync_with_global_status', desc=_('Synchronise the status of all accounts')), Option(OptionKind.SWITCH, _('Message Carbons'), OptionType.ACCOUNT_CONFIG, 'enable_message_carbons', desc=_('All your other online devices get copies\n' 'of sent and received messages.\n' 'XEP-0280')), Option(OptionKind.SWITCH, _('Use file transfer proxies'), OptionType.ACCOUNT_CONFIG, 'use_ft_proxies'), ] GenericOptionPage.__init__(self, account, parent, options) class ConnectionPage(GenericOptionPage): def __init__(self, account, parent=None): options = [ Option(OptionKind.SWITCH, 'HTTP_PROXY', OptionType.ACCOUNT_CONFIG, 'use_env_http_proxy', desc=_('Use environment variable')), Option(OptionKind.PROXY, _('Proxy'), OptionType.ACCOUNT_CONFIG, 'proxy', name='proxy'), Option(OptionKind.SWITCH, _('Warn on insecure connection'), OptionType.ACCOUNT_CONFIG, 'warn_when_insecure_ssl_connection'), Option(OptionKind.SWITCH, _('Send keep-alive packets'), OptionType.ACCOUNT_CONFIG, 'keep_alives_enabled'), Option(OptionKind.HOSTNAME, _('Hostname'), OptionType.DIALOG, desc=_('Manually set the hostname for the server'), props={'dialog': CutstomHostnameDialog}), Option(OptionKind.ENTRY, _('Resource'), OptionType.ACCOUNT_CONFIG, 'resource'), Option(OptionKind.PRIORITY, _('Priority'), OptionType.DIALOG, props={'dialog': PriorityDialog}), ] GenericOptionPage.__init__(self, account, parent, options) class ZeroConfPage(GenericOptionPage): def __init__(self, account, parent=None): options = [ Option(OptionKind.DIALOG, _('Profile'), OptionType.DIALOG, props={'dialog': ZeroconfProfileDialog}), Option(OptionKind.SWITCH, _('Connect on startup'), OptionType.ACCOUNT_CONFIG, 'autoconnect', desc=_('Use environment variable')), Option(OptionKind.SWITCH, _('Save conversations for all contacts'), OptionType.ACCOUNT_CONFIG, 'no_log_for', desc=_('Store conversations on the harddrive')), Option(OptionKind.SWITCH, _('Global Status'), OptionType.ACCOUNT_CONFIG, 'sync_with_global_status', desc=_('Synchronize the status of all accounts')), Option(OptionKind.GPG, _('OpenPGP Key'), OptionType.DIALOG, props={'dialog': None}), ] GenericOptionPage.__init__(self, account, parent, options) self._add_top_buttons(None) class ZeroconfProfileDialog(OptionsDialog): def __init__(self, account, parent): options = [ Option(OptionKind.ENTRY, _('First Name'), OptionType.ACCOUNT_CONFIG, 'zeroconf_first_name'), Option(OptionKind.ENTRY, _('Last Name'), OptionType.ACCOUNT_CONFIG, 'zeroconf_last_name'), Option(OptionKind.ENTRY, _('Jabber ID'), OptionType.ACCOUNT_CONFIG, 'zeroconf_jabber_id'), Option(OptionKind.ENTRY, _('Email'), OptionType.ACCOUNT_CONFIG, 'zeroconf_email'), ] OptionsDialog.__init__(self, parent, _('Profile'), Gtk.DialogFlags.MODAL, options, account) class PriorityDialog(OptionsDialog): def __init__(self, account, parent): neg_priority = app.config.get('enable_negative_priority') if neg_priority: range_ = (-128, 127) else: range_ = (0, 127) options = [ Option(OptionKind.SWITCH, _('Adjust to status'), OptionType.ACCOUNT_CONFIG, 'adjust_priority_with_status', 'adjust'), Option(OptionKind.SPIN, _('Priority'), OptionType.ACCOUNT_CONFIG, 'priority', enabledif=('adjust', False), props={'range_': range_}), ] OptionsDialog.__init__(self, parent, _('Priority'), Gtk.DialogFlags.MODAL, options, account) self.connect('destroy', self.on_destroy) def on_destroy(self, *args): # Update priority if self.account not in app.connections: return show = app.SHOW_LIST[app.connections[self.account].connected] status = app.connections[self.account].status app.connections[self.account].change_status(show, status) class CutstomHostnameDialog(OptionsDialog): def __init__(self, account, parent): options = [ Option(OptionKind.SWITCH, _('Enable'), OptionType.ACCOUNT_CONFIG, 'use_custom_host', name='custom'), Option(OptionKind.ENTRY, _('Hostname'), OptionType.ACCOUNT_CONFIG, 'custom_host', enabledif=('custom', True)), Option(OptionKind.ENTRY, _('Port'), OptionType.ACCOUNT_CONFIG, 'custom_port', enabledif=('custom', True)), ] OptionsDialog.__init__(self, parent, _('Connection Options'), Gtk.DialogFlags.MODAL, options, account) class CertificateDialog(OptionsDialog): def __init__(self, account, parent): options = [ Option(OptionKind.FILECHOOSER, _('Client Certificate'), OptionType.ACCOUNT_CONFIG, 'client_cert', props={'filefilter': (_('PKCS12 Files'), '*.p12')}), Option(OptionKind.SWITCH, _('Encrypted Certificate'), OptionType.ACCOUNT_CONFIG, 'client_cert_encrypted'), ] OptionsDialog.__init__(self, parent, _('Certificate Options'), Gtk.DialogFlags.MODAL, options, account) class LoginDialog(OptionsDialog): def __init__(self, account, parent): options = [ Option(OptionKind.ENTRY, _('Password'), OptionType.ACCOUNT_CONFIG, 'password', name='password', enabledif=('savepass', True)), Option(OptionKind.SWITCH, _('Save Password'), OptionType.ACCOUNT_CONFIG, 'savepass', name='savepass'), Option(OptionKind.CHANGEPASSWORD, _('Change Password'), OptionType.DIALOG, callback=self.on_password_change, props={'dialog': None}), ] OptionsDialog.__init__(self, parent, _('Login Options'), Gtk.DialogFlags.MODAL, options, account) self.connect('destroy', self.on_destroy) def on_password_change(self, new_password, data): passwords.save_password(self.account, new_password) def on_destroy(self, *args): savepass = app.config.get_per('accounts', self.account, 'savepass') if not savepass: passwords.save_password(self.account, '')