update: cli_failed_verification flow

This commit is contained in:
JOhn 2026-05-15 22:01:41 -04:00
parent cf817a62d0
commit 2e735c7303
2 changed files with 292 additions and 9 deletions

View file

@ -38,6 +38,10 @@ from core.observers.TicketObserver import TicketObserver
from core.controllers.tickets.TicketSyncController import sync_ticket_prices from core.controllers.tickets.TicketSyncController import sync_ticket_prices
from core.controllers.tickets.TicketPayController import initiate_payment, check_if_paid from core.controllers.tickets.TicketPayController import initiate_payment, check_if_paid
from core.controllers.tickets.TicketPrepController import prepare_tickets 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 ( from core.controllers.tickets.UseTicketController import (
use_ticket, use_ticket,
modify_random_tickets_setting, modify_random_tickets_setting,
@ -733,6 +737,59 @@ class CustomWindow(QMainWindow):
with open(self.gui_config_file, 'w') as f: with open(self.gui_config_file, 'w') as f:
json.dump(config, f, indent=4) 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): def check_logging(self):
config = self._load_gui_config() config = self._load_gui_config()
if config is None: if config is None:
@ -1084,18 +1141,25 @@ class CustomWindow(QMainWindow):
def clear_data(self): def clear_data(self):
self._data = {"Profile_1": {}} 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): def update_status(self, text, clear=False):
if text is None: if text is None:
self._set_status_font_size(16)
self.status_label.setText('Status:') self.status_label.setText('Status:')
self.disable_marquee() self.disable_marquee()
return return
if clear: if clear:
self._set_status_font_size(16)
self.status_label.setText('') self.status_label.setText('')
return return
full_text = f'Status: {text}' self.status_label.setText('Status: ' + text)
self.status_label.setText(full_text)
self.disable_marquee() self.disable_marquee()
def check_first_launch(self): def check_first_launch(self):
@ -7890,6 +7954,7 @@ class Settings(Page):
self.content_layout.setCurrentWidget(self.tickets_page) self.content_layout.setCurrentWidget(self.tickets_page)
self._select_menu_button("Tickets") self._select_menu_button("Tickets")
self._refresh_tickets_inventory() self._refresh_tickets_inventory()
self._refresh_ticket_recovery_controls()
def show_registrations_page(self): def show_registrations_page(self):
self.content_layout.setCurrentWidget(self.registrations_page) self.content_layout.setCurrentWidget(self.registrations_page)
@ -7928,14 +7993,33 @@ class Settings(Page):
def create_tickets_page(self): def create_tickets_page(self):
page = QWidget() page = QWidget()
layout = QVBoxLayout(page) page_layout = QVBoxLayout(page)
layout.setSpacing(15) page_layout.setSpacing(15)
layout.setContentsMargins(20, 20, 20, 20) page_layout.setContentsMargins(20, 20, 20, 20)
title = QLabel("TICKETS") title = QLabel("TICKETS")
title.setStyleSheet( title.setStyleSheet(
f"color: #808080; font-size: 12px; font-weight: bold; {self.font_style}") 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 = QGroupBox("Random Ticket Use")
random_group.setStyleSheet( random_group.setStyleSheet(
@ -7978,9 +8062,181 @@ class Settings(Page):
inventory_layout.addWidget(self.refresh_tickets_button) inventory_layout.addWidget(self.refresh_tickets_button)
layout.addWidget(inventory_group) 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() layout.addStretch()
scroll_area.setWidget(scroll_content)
page_layout.addWidget(scroll_area)
return page 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): def _on_random_toggle(self, checked):
try: try:
result = modify_random_tickets_setting('on' if checked else 'off', ticket_observer) result = modify_random_tickets_setting('on' if checked else 'off', ticket_observer)
@ -10488,6 +10744,8 @@ class TicketingWorkerThread(QThread):
paid_check_failed = pyqtSignal(str) paid_check_failed = pyqtSignal(str)
prep_done = pyqtSignal(object) prep_done = pyqtSignal(object)
use_done = pyqtSignal(object) use_done = pyqtSignal(object)
failed_verification_evaluated = pyqtSignal(object)
saved_blind_prep_done = pyqtSignal(object)
error = pyqtSignal(str) error = pyqtSignal(str)
def __init__(self, action, params=None): def __init__(self, action, params=None):
@ -10524,6 +10782,16 @@ class TicketingWorkerThread(QThread):
elif self.action == 'PREPARE_TICKETS': elif self.action == 'PREPARE_TICKETS':
result = prepare_tickets(self.params['how_many_profiles'], ticket_observer, connection_observer) result = prepare_tickets(self.params['how_many_profiles'], ticket_observer, connection_observer)
self.prep_done.emit(result) 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': elif self.action == 'USE_TICKET':
result = use_ticket( result = use_ticket(
self.params['which_ticket'], self.params['which_ticket'],
@ -10758,6 +11026,7 @@ class TicketPrepPage(Page):
self.update_status = main_window self.update_status = main_window
self.worker = None self.worker = None
self._terminal_bound = False self._terminal_bound = False
self._tickets_ready = False
self.title.setText("Preparing Tickets") self.title.setText("Preparing Tickets")
self.title.setGeometry(QtCore.QRect(280, 20, 240, 40)) self.title.setGeometry(QtCore.QRect(280, 20, 240, 40))
@ -10780,7 +11049,7 @@ class TicketPrepPage(Page):
} }
QPushButton:disabled { background-color: #555555; } QPushButton:disabled { background-color: #555555; }
""") """)
self.continue_button.setEnabled(False) self.continue_button.setEnabled(True)
self.continue_button.clicked.connect(self.on_continue) self.continue_button.clicked.connect(self.on_continue)
def _bind_terminal_once(self): def _bind_terminal_once(self):
@ -10796,7 +11065,8 @@ class TicketPrepPage(Page):
def start_prep(self): def start_prep(self):
self._bind_terminal_once() self._bind_terminal_once()
self.terminal.append("=== Starting ticket preparation ===") 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.status_label.setText("Preparing tickets...")
self.update_status.update_status("Preparing tickets...") self.update_status.update_status("Preparing tickets...")
@ -10813,6 +11083,8 @@ class TicketPrepPage(Page):
self.status_label.setText("Preparation failed.") self.status_label.setText("Preparation failed.")
return return
if result.get('valid') is True: if result.get('valid') is True:
self._tickets_ready = True
self.update_status.clear_ticket_verification_failure()
self.terminal.append("=== Ticket preparation complete ===") self.terminal.append("=== Ticket preparation complete ===")
self.status_label.setText("Tickets ready. Click Continue to apply one to your profile.") self.status_label.setText("Tickets ready. Click Continue to apply one to your profile.")
self.update_status.update_status("Tickets ready.") self.update_status.update_status("Tickets ready.")
@ -10825,18 +11097,29 @@ class TicketPrepPage(Page):
failed_validations = result.get('failed_validations', []) failed_validations = result.get('failed_validations', [])
self.terminal.append(f" failed count: {how_many_failed}") self.terminal.append(f" failed count: {how_many_failed}")
self.terminal.append(f" failed indices: {failed_validations}") 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.status_label.setText(f"Preparation failed: {msg}")
self.update_status.update_status(f"An error occurred") self.update_status.update_status(f"An error occurred")
self.continue_button.setEnabled(True)
def on_error(self, msg): def on_error(self, msg):
self.terminal.append(f"EXCEPTION: {msg}") self.terminal.append(f"EXCEPTION: {msg}")
self.status_label.setText(f"An unkown error occured") self.status_label.setText(f"An unkown error occured")
self.continue_button.setEnabled(True)
def on_continue(self): def on_continue(self):
menu_page = self.page_stack.findChild(MenuPage) menu_page = self.page_stack.findChild(MenuPage)
profile_id = getattr(self.update_status, 'current_profile_id', None) profile_id = getattr(self.update_status, 'current_profile_id', None)
if menu_page: if menu_page:
self.page_stack.setCurrentWidget(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: if profile_id is None or menu_page is None:
self.update_status.update_status("Tickets ready. Random ticket use is ON.") self.update_status.update_status("Tickets ready. Random ticket use is ON.")
return return

View file

@ -1,6 +1,6 @@
[project] [project]
name = "sp-hydra-veil-gui" name = "sp-hydra-veil-gui"
version = "2.2.7" version = "2.2.8"
authors = [ authors = [
{ name = "Simplified Privacy" }, { name = "Simplified Privacy" },
] ]