# -*- coding: utf-8 -*- import logging as log import sys from PyQt5.QtCore import QCoreApplication, Qt from PyQt5.QtGui import QIcon, QKeySequence from PyQt5.QtWidgets import ( QGridLayout, QLabel, QPushButton, QMessageBox, QProgressBar, QShortcut, QSizePolicy, QSpacerItem, QStackedWidget, QToolButton, QWidget, ) from twisted.internet import reactor from twisted.internet.defer import CancelledError from wormhole.errors import ( ServerConnectionError, WelcomeError, WrongPasswordError, ) from gridsync import resource, APP_NAME from gridsync import settings as global_settings from gridsync.invite import InviteReceiver, load_settings_from_cheatcode from gridsync.errors import UpgradeRequiredError from gridsync.gui.color import BlendedColor from gridsync.gui.font import Font from gridsync.gui.invite import InviteCodeWidget, show_failure from gridsync.gui.pixmap import Pixmap from gridsync.gui.widgets import TahoeConfigForm from gridsync.recovery import RecoveryKeyImporter from gridsync.setup import SetupRunner, validate_settings from gridsync.tahoe import is_valid_furl from gridsync.tor import TOR_PURPLE class WelcomeWidget(QWidget): def __init__(self, parent=None): super(WelcomeWidget, self).__init__() self.parent = parent application_settings = global_settings["application"] logo_icon = application_settings.get("logo_icon") if logo_icon: icon_file = logo_icon icon_size = 288 else: icon_file = application_settings.get("tray_icon") icon_size = 220 self.icon = QLabel() self.icon.setPixmap(Pixmap(icon_file, icon_size)) self.icon.setAlignment(Qt.AlignCenter) self.slogan = QLabel( "<i>{}</i>".format(application_settings.get("description", "")) ) self.slogan.setFont(Font(12)) p = self.palette() dimmer_grey = BlendedColor( p.windowText().color(), p.window().color() ).name() self.slogan.setStyleSheet("color: {}".format(dimmer_grey)) self.slogan.setAlignment(Qt.AlignCenter) if logo_icon: self.slogan.hide() self.invite_code_widget = InviteCodeWidget(self) self.lineedit = self.invite_code_widget.lineedit self.connect_button = QPushButton("Connect") try: default_code = global_settings["connection"]["default"] except KeyError: default_code = "" grid_settings = load_settings_from_cheatcode(default_code) if grid_settings: self.invite_code_widget.hide() nickname = grid_settings.get("nickname") if nickname: font = Font(11) self.connect_button.setFont(font) self.connect_button.setFixedHeight(32) self.connect_button.setText(f"Connect to {nickname}") self.connect_button.clicked.connect( lambda: self.parent.go(default_code, grid_settings) ) primary_color = grid_settings.get("color-1") if primary_color: self.connect_button.setStyleSheet( f"background: {primary_color}; color: white" ) else: self.connect_button.setStyleSheet( "background: green; color: white" ) else: self.connect_button.hide() self.message = QLabel() self.message.setStyleSheet("color: red") self.message.setAlignment(Qt.AlignCenter) self.message.hide() self.restore_link = QLabel() self.restore_link.setText("<a href>Restore from Recovery Key...</a>") self.restore_link.setFont(Font(9)) self.restore_link.setAlignment(Qt.AlignCenter) self.configure_link = QLabel() self.configure_link.setText("<a href>Manual configuration...</a>") self.configure_link.setFont(Font(9)) self.configure_link.setAlignment(Qt.AlignCenter) self.preferences_button = QPushButton() self.preferences_button.setIcon(QIcon(resource("preferences.png"))) self.preferences_button.setStyleSheet("border: 0px; padding: 0px;") self.preferences_button.setToolTip("Preferences...") self.preferences_button.setFocusPolicy(Qt.NoFocus) links_grid = QGridLayout() links_grid.addItem(QSpacerItem(0, 0, 0, QSizePolicy.Expanding), 1, 1) links_grid.addWidget(self.restore_link, 2, 1) links_grid.addItem(QSpacerItem(0, 0, 0, QSizePolicy.Expanding), 3, 1) links_grid.addWidget(self.configure_link, 4, 1) links_grid.addItem(QSpacerItem(0, 0, 0, QSizePolicy.Expanding), 5, 1) prefs_layout = QGridLayout() prefs_layout.addItem(QSpacerItem(0, 0, QSizePolicy.Expanding, 0), 1, 1) prefs_layout.addWidget(self.preferences_button, 1, 2) layout = QGridLayout(self) layout.addItem(QSpacerItem(0, 0, 0, QSizePolicy.Expanding), 0, 0) layout.addItem(QSpacerItem(0, 0, QSizePolicy.Expanding, 0), 1, 1) layout.addItem(QSpacerItem(0, 0, QSizePolicy.Expanding, 0), 1, 2) layout.addWidget(self.icon, 1, 3) layout.addItem(QSpacerItem(0, 0, QSizePolicy.Expanding, 0), 1, 4) layout.addItem(QSpacerItem(0, 0, QSizePolicy.Expanding, 0), 1, 5) layout.addWidget(self.slogan, 2, 3) layout.addItem(QSpacerItem(0, 0, 0, QSizePolicy.Expanding), 3, 1) layout.addWidget(self.invite_code_widget, 4, 2, 1, 3) layout.addWidget(self.connect_button, 4, 2, 1, 3) layout.addWidget(self.message, 5, 3) layout.addItem(QSpacerItem(0, 0, 0, QSizePolicy.Minimum), 6, 1) layout.addLayout(links_grid, 7, 3) layout.addItem(QSpacerItem(0, 0, 0, QSizePolicy.Minimum), 8, 1) layout.addItem(QSpacerItem(0, 0, 0, QSizePolicy.Expanding), 9, 1) layout.addLayout(prefs_layout, 10, 1, 1, 5) def show_error(self, message): self.message.setText(message) self.message.show() reactor.callLater(3, self.message.hide) def reset(self): self.lineedit.setText("") class ProgressBarWidget(QWidget): def __init__(self): super(ProgressBarWidget, self).__init__() self.icon_server = QLabel() self.icon_server.setPixmap(Pixmap("cloud.png", 220)) self.icon_server.setAlignment(Qt.AlignCenter) self.icon_overlay = QLabel() self.icon_overlay.setPixmap(Pixmap("pixel.png", 75)) self.icon_overlay.setAlignment(Qt.AlignHCenter) self.icon_connection = QLabel() self.icon_connection.setPixmap(Pixmap("wifi.png", 128)) self.icon_connection.setAlignment(Qt.AlignCenter) self.icon_client = QLabel() self.icon_client.setPixmap(Pixmap("laptop-with-icon.png", 128)) self.icon_client.setAlignment(Qt.AlignCenter) self.checkmark = QLabel() self.checkmark.setPixmap(Pixmap("pixel.png", 32)) self.checkmark.setAlignment(Qt.AlignCenter) self.tor_label = QLabel() self.tor_label.setToolTip( "This connection is being routed through the Tor network." ) self.tor_label.setPixmap(Pixmap("tor-onion.png", 24)) self.tor_label.hide() self.progressbar = QProgressBar() self.progressbar.setMaximum(10) self.progressbar.setTextVisible(False) self.progressbar.setValue(0) self.message = QLabel() p = self.palette() dimmer_grey = BlendedColor( p.windowText().color(), p.window().color() ).name() self.message.setStyleSheet("color: {}".format(dimmer_grey)) self.message.setAlignment(Qt.AlignCenter) self.finish_button = QPushButton("Finish") self.finish_button.hide() self.cancel_button = QToolButton() self.cancel_button.setIcon(QIcon(resource("close.png"))) self.cancel_button.setStyleSheet("border: 0px; padding: 0px;") layout = QGridLayout(self) layout.addWidget(self.cancel_button, 0, 5) layout.addItem(QSpacerItem(0, 0, QSizePolicy.Expanding, 0), 1, 1) layout.addItem(QSpacerItem(0, 0, QSizePolicy.Expanding, 0), 1, 2) layout.addWidget(self.icon_server, 1, 3) layout.addWidget(self.icon_overlay, 1, 3) layout.addItem(QSpacerItem(0, 0, QSizePolicy.Expanding, 0), 1, 4) layout.addItem(QSpacerItem(0, 0, QSizePolicy.Expanding, 0), 1, 5) layout.addWidget(self.icon_connection, 2, 3) layout.addWidget(self.icon_client, 3, 3) layout.addWidget(self.checkmark, 4, 3, 1, 1) layout.addWidget(self.tor_label, 5, 1, 1, 1, Qt.AlignRight) layout.addWidget(self.progressbar, 5, 2, 1, 3) layout.addWidget(self.message, 6, 3) layout.addWidget(self.finish_button, 6, 3) layout.addItem(QSpacerItem(0, 0, 0, QSizePolicy.Expanding), 7, 1) def update_progress(self, message): step = self.progressbar.value() + 1 self.progressbar.setValue(step) self.message.setText(message) if step == 2: # "Connecting to <nickname>..." self.icon_connection.setPixmap(Pixmap("lines_dotted.png", 128)) self.icon_server.setPixmap(Pixmap("cloud_storage.png", 220)) elif step == 5: # After await_ready() self.icon_connection.setPixmap(Pixmap("lines_solid.png", 128)) elif step == self.progressbar.maximum(): # "Done!" self.checkmark.setPixmap(Pixmap("green_checkmark.png", 32)) def is_complete(self): return self.progressbar.value() == self.progressbar.maximum() def reset(self): self.progressbar.setValue(0) self.message.setText("") self.finish_button.hide() self.checkmark.setPixmap(Pixmap("pixel.png", 32)) self.tor_label.hide() self.progressbar.setStyleSheet("") class WelcomeDialog(QStackedWidget): def __init__(self, gui, known_gateways=None): super(WelcomeDialog, self).__init__() self.gui = gui self.known_gateways = known_gateways self.gateway = None self.setup_runner = None self.recovery_key_importer = None self.use_tor = False self.prompt_to_export = True self.resize(400, 500) self.setWindowTitle(APP_NAME) self.page_1 = WelcomeWidget(self) self.page_2 = ProgressBarWidget() self.page_3 = TahoeConfigForm() self.addWidget(self.page_1) self.addWidget(self.page_2) self.addWidget(self.page_3) self.lineedit = self.page_1.lineedit self.tor_checkbox = self.page_1.invite_code_widget.tor_checkbox self.restore_link = self.page_1.restore_link self.configure_link = self.page_1.configure_link self.preferences_button = self.page_1.preferences_button self.progressbar = self.page_2.progressbar self.cancel_button = self.page_2.cancel_button self.finish_button = self.page_2.finish_button self.buttonbox = self.page_3.buttonbox self.shortcut_close = QShortcut(QKeySequence.Close, self) self.shortcut_close.activated.connect(self.close) self.shortcut_quit = QShortcut(QKeySequence.Quit, self) self.shortcut_quit.activated.connect(self.close) self.lineedit.go.connect(self.go) self.lineedit.error.connect(self.show_error) self.restore_link.linkActivated.connect(self.on_restore_link_activated) self.configure_link.linkActivated.connect( self.on_configure_link_activated ) self.preferences_button.clicked.connect( self.gui.show_preferences_window ) self.cancel_button.clicked.connect(self.cancel_button_clicked) self.finish_button.clicked.connect(self.finish_button_clicked) self.buttonbox.accepted.connect(self.on_accepted) self.buttonbox.rejected.connect(self.reset) def on_configure_link_activated(self): self.setCurrentIndex(2) def update_progress(self, message): self.page_2.update_progress(message) def show_error(self, message): self.page_1.show_error(message) def reset(self): self.page_1.reset() self.page_2.reset() self.page_3.reset() self.setCurrentIndex(0) def load_service_icon(self, filepath): self.page_2.icon_overlay.setPixmap(Pixmap(filepath, 100)) def handle_failure(self, failure): log.error(str(failure)) if failure.type == CancelledError: if self.progressbar.value() <= 2: show_failure(failure, self) self.show_error("Invite timed out") self.reset() return show_failure(failure, self) if failure.type == ServerConnectionError: self.show_error("Server connection error") if failure.type == WelcomeError: self.show_error("Invite refused") elif failure.type == WrongPasswordError: self.show_error("Invite confirmation failed") elif failure.type == UpgradeRequiredError: self.show_error("Upgrade required") else: self.show_error(str(failure.type.__name__)) self.reset() def on_done(self, gateway): self.gateway = gateway self.progressbar.setValue(self.progressbar.maximum()) self.page_2.checkmark.setPixmap(Pixmap("green_checkmark.png", 32)) self.finish_button.show() self.finish_button_clicked() # TODO: Cleanup def on_already_joined(self, grid_name): QMessageBox.information( self, "Already connected", 'You are already connected to "{}"'.format(grid_name), ) self.close() def verify_settings(self, settings, from_wormhole=True): self.show() self.raise_() settings = validate_settings( settings, self.known_gateways, self, from_wormhole ) self.setup_runner = SetupRunner(self.known_gateways, self.use_tor) steps = self.setup_runner.calculate_total_steps(settings) + 2 self.progressbar.setMaximum(steps) self.setup_runner.grid_already_joined.connect(self.on_already_joined) self.setup_runner.update_progress.connect(self.update_progress) self.setup_runner.got_icon.connect(self.load_service_icon) self.setup_runner.client_started.connect( lambda gateway: self.gui.populate([gateway]) ) self.setup_runner.done.connect(self.on_done) d = self.setup_runner.run(settings) d.addErrback(self.handle_failure) def on_import_done(self, settings): if settings.get("hide-ip") or self.tor_checkbox.isChecked(): self.use_tor = True self.page_2.tor_label.show() self.progressbar.setStyleSheet( "QProgressBar::chunk {{ background-color: {}; }}".format( TOR_PURPLE ) ) self.setCurrentIndex(1) self.progressbar.setValue(1) self.update_progress("Verifying invitation code...") self.prompt_to_export = False self.verify_settings(settings, from_wormhole=False) def on_restore_link_activated(self): self.recovery_key_importer = RecoveryKeyImporter(self.page_1) self.recovery_key_importer.done.connect(self.on_import_done) self.recovery_key_importer.do_import() def go(self, code, settings=None): if self.tor_checkbox.isChecked(): self.use_tor = True self.page_2.tor_label.show() self.progressbar.setStyleSheet( "QProgressBar::chunk {{ background-color: {}; }}".format( TOR_PURPLE ) ) self.setCurrentIndex(1) self.progressbar.setValue(1) self.update_progress("Verifying invitation code...") invite_receiver = InviteReceiver(self.known_gateways, self.use_tor) invite_receiver.grid_already_joined.connect(self.on_already_joined) invite_receiver.update_progress.connect(self.update_progress) invite_receiver.got_icon.connect(self.load_service_icon) invite_receiver.client_started.connect( lambda gateway: self.gui.populate([gateway]) ) invite_receiver.done.connect(self.on_done) d = invite_receiver.receive(code, settings) d.addErrback(self.handle_failure) reactor.callLater(30, d.cancel) def cancel_button_clicked(self): if self.page_2.is_complete(): self.finish_button_clicked() return msgbox = QMessageBox(self) msgbox.setIcon(QMessageBox.Question) msgbox.setWindowTitle("Cancel setup?") msgbox.setText("Are you sure you wish to cancel the setup process?") msgbox.setInformativeText( "If you cancel, you may need to obtain a new invite code." ) msgbox.setStandardButtons(QMessageBox.Yes | QMessageBox.No) msgbox.setDefaultButton(QMessageBox.No) if msgbox.exec_() == QMessageBox.Yes: self.reset() def on_accepted(self): settings = self.page_3.get_settings() msg = QMessageBox() msg.setIcon(QMessageBox.Warning) msg.setWindowTitle(APP_NAME) msg.setStandardButtons(QMessageBox.Ok) if not settings["nickname"]: msg.setText("Please enter a name.") msg.exec_() elif not settings["introducer"]: msg.setText("Please enter an Introducer fURL.") msg.exec_() elif not is_valid_furl(settings["introducer"]): msg.setText("Please enter a valid Introducer fURL.") msg.exec_() else: self.setCurrentIndex(1) self.verify_settings(settings, from_wormhole=False) def prompt_for_export(self, gateway): msg = QMessageBox(self) msg.setIcon(QMessageBox.Warning) msg.setStandardButtons(QMessageBox.Yes | QMessageBox.No) msg.setDefaultButton(QMessageBox.Yes) button_export = msg.button(QMessageBox.Yes) button_export.setText("&Export...") button_skip = msg.button(QMessageBox.No) button_skip.setText("&Skip") msg.setWindowTitle("Export Recovery Key?") # "Now that {} is configured..." msg.setText( "Before uploading any folders to {}, it is recommended that you " "export a Recovery Key and store it in a safe location (such as " "an encrypted USB drive or password manager).".format(gateway.name) ) msg.setInformativeText( "{} does not have access to your folders, and cannot restore " "access to them. But with a Recovery Key, you can restore access " "to uploaded folders in case something goes wrong (e.g., hardware " "failure, accidental data-loss).<p><p><a href=https://github.com/" "gridsync/gridsync/blob/master/docs/recovery-keys.md>More " "information...</a>".format(gateway.name) ) # msg.setText( # "Before uploading any folders to {}, it is <b>strongly " # "recommended</b> that you <i>export a Recovery Key</i> and store " # "it in a safe and secure location (such as an encrypted USB drive)" # ".<p><p>Possessing a Recovery Key will allow you to restore " # "access to any of the folders you've uploaded to {} in the event " # "that something goes wrong (e.g., hardware failure, accidental " # "data-loss).".format(gateway.name, gateway.name)) # msg.setDetailedText( # "A 'Recovery Key' is a small file that contains enough information" # " to re-establish a connection with your storage provider and " # "restore your previously-uploaded folders. Because access to this " # "file is sufficient to access to any of the the data you've " # "stored, it is important that you keep this file safe and secure; " # "do not share your Recovery Key with anybody!") reply = msg.exec_() if reply == QMessageBox.Yes: self.gui.main_window.export_recovery_key() # XXX else: # TODO: Nag user; "Are you sure?" pass def finish_button_clicked(self): self.gui.show_main_window() self.close() if self.prompt_to_export: self.prompt_for_export(self.gateway) self.reset() def enterEvent(self, event): event.accept() self.page_1.invite_code_widget.maybe_enable_tor_checkbox() self.lineedit.update_action_button() def closeEvent(self, event): if self.gui.main_window.gateways: event.accept() else: event.ignore() msgbox = QMessageBox(self) msgbox.setIcon(QMessageBox.Question) msgbox.setWindowTitle("Exit setup?") msgbox.setText("Are you sure you wish to exit?") msgbox.setInformativeText( "{} has not yet been configured.".format(APP_NAME) ) msgbox.setStandardButtons(QMessageBox.Yes | QMessageBox.No) msgbox.setDefaultButton(QMessageBox.No) if msgbox.exec_() == QMessageBox.Yes: if sys.platform == "win32": self.gui.systray.hide() QCoreApplication.instance().quit()