From 2e735c7303c865472127398a791e898978913515 Mon Sep 17 00:00:00 2001 From: JOhn Date: Fri, 15 May 2026 22:01:41 -0400 Subject: [PATCH] update: cli_failed_verification flow --- gui/__main__.py | 299 ++++++++++++++++++++++++++++++++++++++++++++++-- pyproject.toml | 2 +- 2 files changed, 292 insertions(+), 9 deletions(-) diff --git a/gui/__main__.py b/gui/__main__.py index 47df0fc..79aa9b0 100755 --- a/gui/__main__.py +++ b/gui/__main__.py @@ -38,6 +38,10 @@ from core.observers.TicketObserver import TicketObserver from core.controllers.tickets.TicketSyncController import sync_ticket_prices from core.controllers.tickets.TicketPayController import initiate_payment, check_if_paid from core.controllers.tickets.TicketPrepController import prepare_tickets +from core.controllers.tickets.FailedVerificationController import ( + evaluate_if_its_the_key, + prepare_tickets_with_saved_blind_sigs, +) from core.controllers.tickets.UseTicketController import ( use_ticket, modify_random_tickets_setting, @@ -733,6 +737,59 @@ class CustomWindow(QMainWindow): with open(self.gui_config_file, 'w') as f: json.dump(config, f, indent=4) + def _default_gui_config(self): + return {"logging": {"gui_logging_enabled": False, "log_level": "INFO"}} + + def save_ticket_verification_failure(self, result): + try: + failed_validations = result.get('failed_validations', []) if isinstance(result, dict) else [] + if not failed_validations: + return False + + config = self._load_gui_config() + if config is None: + config = self._default_gui_config() + if "tickets" not in config: + config["tickets"] = {} + + config["tickets"]["failed_verification"] = { + "message": result.get('message', 'verification_failed'), + "how_many_failed": result.get('how_many_failed', len(failed_validations)), + "failed_validations": list(failed_validations), + "updated_at": datetime.now(timezone.utc).isoformat(), + } + self._save_gui_config(config) + return True + except Exception as e: + logging.error(f"Error saving ticket verification failure: {e}") + return False + + def get_ticket_verification_failure(self): + try: + config = self._load_gui_config() + if not config: + return None + failure = config.get("tickets", {}).get("failed_verification") + if not isinstance(failure, dict): + return None + failed_validations = failure.get("failed_validations") + if not failed_validations: + return None + return failure + except Exception as e: + logging.error(f"Error loading ticket verification failure: {e}") + return None + + def clear_ticket_verification_failure(self): + try: + config = self._load_gui_config() + if not config or "tickets" not in config: + return + config["tickets"].pop("failed_verification", None) + self._save_gui_config(config) + except Exception as e: + logging.error(f"Error clearing ticket verification failure: {e}") + def check_logging(self): config = self._load_gui_config() if config is None: @@ -1084,18 +1141,25 @@ class CustomWindow(QMainWindow): def clear_data(self): self._data = {"Profile_1": {}} + def _set_status_font_size(self, font_size): + self.status_label.setStyleSheet( + f"color: rgb(0, 255, 255); font-size: {font_size}px;") + + + def update_status(self, text, clear=False): if text is None: + self._set_status_font_size(16) self.status_label.setText('Status:') self.disable_marquee() return if clear: + self._set_status_font_size(16) self.status_label.setText('') return - full_text = f'Status: {text}' - self.status_label.setText(full_text) + self.status_label.setText('Status: ' + text) self.disable_marquee() def check_first_launch(self): @@ -7890,6 +7954,7 @@ class Settings(Page): self.content_layout.setCurrentWidget(self.tickets_page) self._select_menu_button("Tickets") self._refresh_tickets_inventory() + self._refresh_ticket_recovery_controls() def show_registrations_page(self): self.content_layout.setCurrentWidget(self.registrations_page) @@ -7928,14 +7993,33 @@ class Settings(Page): def create_tickets_page(self): page = QWidget() - layout = QVBoxLayout(page) - layout.setSpacing(15) - layout.setContentsMargins(20, 20, 20, 20) + page_layout = QVBoxLayout(page) + page_layout.setSpacing(15) + page_layout.setContentsMargins(20, 20, 20, 20) title = QLabel("TICKETS") title.setStyleSheet( f"color: #808080; font-size: 12px; font-weight: bold; {self.font_style}") - layout.addWidget(title) + page_layout.addWidget(title) + + scroll_area = QScrollArea() + scroll_area.setWidgetResizable(True) + scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff) + scroll_area.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded) + scroll_area.setStyleSheet(""" + QScrollArea { + background-color: transparent; + border: none; + } + QScrollArea > QWidget > QWidget { + background-color: transparent; + } + """) + + scroll_content = QWidget() + layout = QVBoxLayout(scroll_content) + layout.setSpacing(15) + layout.setContentsMargins(0, 0, 8, 0) random_group = QGroupBox("Random Ticket Use") random_group.setStyleSheet( @@ -7978,9 +8062,181 @@ class Settings(Page): inventory_layout.addWidget(self.refresh_tickets_button) layout.addWidget(inventory_group) + saved_failure = self.update_status.get_ticket_verification_failure() + has_failure = saved_failure is not None + + recovery_group = QGroupBox("Verification Failure Recovery") + recovery_group.setStyleSheet( + f"QGroupBox {{ color: white; padding: 15px; {self.font_style} }}") + recovery_layout = QVBoxLayout(recovery_group) + + self.ticket_recovery_status_label = QLabel(self._format_ticket_failure_status(saved_failure)) + self.ticket_recovery_status_label.setStyleSheet( + f"color: white; font-size: 12px; {self.font_style}") + self.ticket_recovery_status_label.setWordWrap(True) + recovery_layout.addWidget(self.ticket_recovery_status_label) + + self.evaluate_public_key_button = QPushButton("Evaluate Public Key") + self.evaluate_public_key_button.setFixedSize(180, 36) + self.evaluate_public_key_button.setEnabled(has_failure) + self.evaluate_public_key_button.setStyleSheet(f""" + QPushButton {{ + background: #00aaff; color: white; border: none; + border-radius: 5px; font-weight: bold; {self.font_style} + }} + QPushButton:hover:!disabled {{ background: #0088cc; }} + QPushButton:disabled {{ background: #666666; color: #bbbbbb; }} + """) + self.evaluate_public_key_button.clicked.connect(self.evaluate_ticket_public_key) + recovery_layout.addWidget(self.evaluate_public_key_button) + + self.ticket_recovery_output = TerminalWidget() + self.ticket_recovery_output.setFixedHeight(130) + if not has_failure: + self.ticket_recovery_output.setPlainText("No saved verification failure.") + recovery_layout.addWidget(self.ticket_recovery_output) + + layout.addWidget(recovery_group) + + debug_group = QGroupBox("Debug Recovery") + debug_group.setStyleSheet( + f"QGroupBox {{ color: white; padding: 15px; {self.font_style} }}") + debug_layout = QVBoxLayout(debug_group) + + self.prepare_saved_blind_sigs_button = QPushButton( + "Prepare Tickets even if validation of the server's signature failed") + self.prepare_saved_blind_sigs_button.setFixedSize(500, 40) + self.prepare_saved_blind_sigs_button.setEnabled(has_failure) + self.prepare_saved_blind_sigs_button.setStyleSheet(f""" + QPushButton {{ + background: #c0392b; color: white; border: none; + border-radius: 5px; font-size: 10px; font-weight: bold; {self.font_style} + }} + QPushButton:hover:!disabled {{ background: #a93226; }} + QPushButton:disabled {{ background: #666666; color: #bbbbbb; }} + """) + self.prepare_saved_blind_sigs_button.clicked.connect(self.prepare_tickets_with_saved_blind_signatures) + debug_layout.addWidget(self.prepare_saved_blind_sigs_button) + + layout.addWidget(debug_group) layout.addStretch() + scroll_area.setWidget(scroll_content) + page_layout.addWidget(scroll_area) return page + def _format_ticket_failure_status(self, failure): + if not failure: + return "No saved verification failure. If ticket preparation fails validation, recovery data will appear here." + failed_validations = failure.get("failed_validations", []) + how_many_failed = failure.get("how_many_failed", len(failed_validations)) + updated_at = failure.get("updated_at", "unknown time") + failed_text = ", ".join(str(item) for item in failed_validations) + return ( + f"Saved verification failure: {how_many_failed} failed. " + f"Failed validation indices: {failed_text}. Saved: {updated_at}" + ) + + def _refresh_ticket_recovery_controls(self): + failure = self.update_status.get_ticket_verification_failure() + has_failure = failure is not None + if hasattr(self, 'ticket_recovery_status_label'): + self.ticket_recovery_status_label.setText(self._format_ticket_failure_status(failure)) + if hasattr(self, 'evaluate_public_key_button'): + self.evaluate_public_key_button.setEnabled(has_failure) + if hasattr(self, 'prepare_saved_blind_sigs_button'): + self.prepare_saved_blind_sigs_button.setEnabled(has_failure) + + def _set_ticket_recovery_busy(self, busy): + if hasattr(self, 'evaluate_public_key_button'): + self.evaluate_public_key_button.setEnabled(not busy and self.update_status.get_ticket_verification_failure() is not None) + if hasattr(self, 'prepare_saved_blind_sigs_button'): + self.prepare_saved_blind_sigs_button.setEnabled(not busy and self.update_status.get_ticket_verification_failure() is not None) + + def _write_ticket_recovery_output(self, text): + if hasattr(self, 'ticket_recovery_output'): + self.ticket_recovery_output.setPlainText(text) + + def _format_ticket_recovery_result(self, label, result): + try: + payload = json.dumps(result, indent=2, default=str) + except TypeError: + payload = str(result) + return f"{label}:\n{payload}" + + def _get_ticket_failure_for_action(self): + failure = self.update_status.get_ticket_verification_failure() + if failure is None: + self._write_ticket_recovery_output("No saved verification failure.") + self.update_status.update_status("No ticket verification failure is saved.") + self._refresh_ticket_recovery_controls() + return None + return failure + + def evaluate_ticket_public_key(self): + failure = self._get_ticket_failure_for_action() + if failure is None: + return + failed_validations = list(failure.get("failed_validations", [])) + self._write_ticket_recovery_output("Evaluating public key...") + self.update_status.update_status("Evaluating ticket public key...") + self._set_ticket_recovery_busy(True) + + self.ticket_recovery_worker = TicketingWorkerThread( + 'EVALUATE_FAILED_VERIFICATION', + params={'failed_validations': failed_validations}, + ) + self.ticket_recovery_worker.failed_verification_evaluated.connect(self.on_failed_verification_evaluated) + self.ticket_recovery_worker.error.connect(self.on_ticket_recovery_error) + self.ticket_recovery_worker.start() + + def on_failed_verification_evaluated(self, result): + self._write_ticket_recovery_output(self._format_ticket_recovery_result("evaluation_results", result)) + self.update_status.update_status("Ticket public key evaluation complete.") + self._set_ticket_recovery_busy(False) + self._refresh_ticket_recovery_controls() + + def prepare_tickets_with_saved_blind_signatures(self): + failure = self._get_ticket_failure_for_action() + if failure is None: + return + + reply = QMessageBox.warning( + self, + "Prepare Tickets Anyway", + "This will prepare tickets even though server signature validation failed. Continue only if you have evaluated the situation.", + QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, + QMessageBox.StandardButton.No, + ) + if reply != QMessageBox.StandardButton.Yes: + return + + self._write_ticket_recovery_output("Preparing tickets with saved blind signatures...") + self.update_status.update_status("Preparing tickets with saved blind signatures...") + self._set_ticket_recovery_busy(True) + + self.ticket_recovery_worker = TicketingWorkerThread('PREPARE_SAVED_BLIND_SIGS') + self.ticket_recovery_worker.saved_blind_prep_done.connect(self.on_saved_blind_prep_done) + self.ticket_recovery_worker.error.connect(self.on_ticket_recovery_error) + self.ticket_recovery_worker.start() + + def on_saved_blind_prep_done(self, result): + self._write_ticket_recovery_output(self._format_ticket_recovery_result("Results of preparation", result)) + if isinstance(result, dict) and result.get('valid') is True: + self.update_status.clear_ticket_verification_failure() + self.update_status.update_status("Tickets prepared from saved blind signatures.") + self._refresh_tickets_inventory() + else: + msg = result.get('message', 'failed') if isinstance(result, dict) else 'failed' + self.update_status.update_status(f"Saved blind signature prep failed: {msg}") + self._set_ticket_recovery_busy(False) + self._refresh_ticket_recovery_controls() + + def on_ticket_recovery_error(self, msg): + self._write_ticket_recovery_output(f"EXCEPTION: {msg}") + self.update_status.update_status(f"Ticket recovery error: {msg}") + self._set_ticket_recovery_busy(False) + self._refresh_ticket_recovery_controls() + def _on_random_toggle(self, checked): try: result = modify_random_tickets_setting('on' if checked else 'off', ticket_observer) @@ -10488,6 +10744,8 @@ class TicketingWorkerThread(QThread): paid_check_failed = pyqtSignal(str) prep_done = pyqtSignal(object) use_done = pyqtSignal(object) + failed_verification_evaluated = pyqtSignal(object) + saved_blind_prep_done = pyqtSignal(object) error = pyqtSignal(str) def __init__(self, action, params=None): @@ -10524,6 +10782,16 @@ class TicketingWorkerThread(QThread): elif self.action == 'PREPARE_TICKETS': result = prepare_tickets(self.params['how_many_profiles'], ticket_observer, connection_observer) self.prep_done.emit(result) + elif self.action == 'EVALUATE_FAILED_VERIFICATION': + result = evaluate_if_its_the_key( + self.params['failed_validations'], + ticket_observer, + connection_observer, + ) + self.failed_verification_evaluated.emit(result) + elif self.action == 'PREPARE_SAVED_BLIND_SIGS': + result = prepare_tickets_with_saved_blind_sigs(ticket_observer, connection_observer) + self.saved_blind_prep_done.emit(result) elif self.action == 'USE_TICKET': result = use_ticket( self.params['which_ticket'], @@ -10758,6 +11026,7 @@ class TicketPrepPage(Page): self.update_status = main_window self.worker = None self._terminal_bound = False + self._tickets_ready = False self.title.setText("Preparing Tickets") self.title.setGeometry(QtCore.QRect(280, 20, 240, 40)) @@ -10780,7 +11049,7 @@ class TicketPrepPage(Page): } QPushButton:disabled { background-color: #555555; } """) - self.continue_button.setEnabled(False) + self.continue_button.setEnabled(True) self.continue_button.clicked.connect(self.on_continue) def _bind_terminal_once(self): @@ -10796,7 +11065,8 @@ class TicketPrepPage(Page): def start_prep(self): self._bind_terminal_once() self.terminal.append("=== Starting ticket preparation ===") - self.continue_button.setEnabled(False) + self._tickets_ready = False + self.continue_button.setEnabled(True) self.status_label.setText("Preparing tickets...") self.update_status.update_status("Preparing tickets...") @@ -10813,6 +11083,8 @@ class TicketPrepPage(Page): self.status_label.setText("Preparation failed.") return if result.get('valid') is True: + self._tickets_ready = True + self.update_status.clear_ticket_verification_failure() self.terminal.append("=== Ticket preparation complete ===") self.status_label.setText("Tickets ready. Click Continue to apply one to your profile.") self.update_status.update_status("Tickets ready.") @@ -10825,18 +11097,29 @@ class TicketPrepPage(Page): failed_validations = result.get('failed_validations', []) self.terminal.append(f" failed count: {how_many_failed}") self.terminal.append(f" failed indices: {failed_validations}") + if self.update_status.save_ticket_verification_failure(result): + self.terminal.append("Recovery data saved. Open Settings > Tickets to evaluate or resume.") + self.status_label.setText("Verification failed. Open Settings > Tickets for recovery.") + self.update_status.update_status("Ticket verification failed") + self.continue_button.setEnabled(True) + return self.status_label.setText(f"Preparation failed: {msg}") self.update_status.update_status(f"An error occurred") + self.continue_button.setEnabled(True) def on_error(self, msg): self.terminal.append(f"EXCEPTION: {msg}") self.status_label.setText(f"An unkown error occured") + self.continue_button.setEnabled(True) def on_continue(self): menu_page = self.page_stack.findChild(MenuPage) profile_id = getattr(self.update_status, 'current_profile_id', None) if menu_page: self.page_stack.setCurrentWidget(menu_page) + if not self._tickets_ready: + self.update_status.update_status("Ticket preparation was not completed.") + return if profile_id is None or menu_page is None: self.update_status.update_status("Tickets ready. Random ticket use is ON.") return diff --git a/pyproject.toml b/pyproject.toml index 677b9cf..2f16ad4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "sp-hydra-veil-gui" -version = "2.2.7" +version = "2.2.8" authors = [ { name = "Simplified Privacy" }, ]