# This file is part of Gajim. # # Gajim is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published # by the Free Software Foundation; version 3 only. # # Gajim is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with Gajim. If not, see . from functools import partial from gi.repository import Gtk from gi.repository import Gdk from gi.repository import GLib from gajim.common import app from gajim.common import passwords from gajim.common import helpers from gajim.common import ged from gajim.common.i18n import _ from gajim.common.connection import Connection from gajim.common.zeroconf.connection_zeroconf import ConnectionZeroconf from gajim.common.const import Option from gajim.common.const import OptionKind from gajim.common.const import OptionType from gajim import gtkgui_helpers from gajim import gui_menu_builder from gajim import config from gajim.options_dialog import OptionsDialog from gajim.options_dialog import OptionsBox from gajim.gtk.dialogs import ConfirmationDialog from gajim.gtk.dialogs import YesNoDialog 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) 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) 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: 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) app.plugin_manager.register_modules_for_account( app.connections[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.last_message_time[account] = {} app.status_before_autoaway[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.last_message_time[account] del app.status_before_autoaway[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.config.set_per( 'accounts', account, 'roster_version', '') 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(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(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 self.label = Gtk.Label(app.get_account_label(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): self.label.set_text(app.get_account_label(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.get_account_label(self.account) if update: self.entry.set_text(account_label) 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 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, '')