Compare commits

...

5 commits

Author SHA1 Message Date
JOhn
234a177689 update: new display for hysteria and vless protocols 2026-09-11 19:58:08 -04:00
JOhn
a3cfc06630 update: fixes enum ticket handling 2026-08-29 12:30:34 -04:00
JOhn
f13bc8d61e update: updated tickets enum handling 2026-08-28 17:23:42 -04:00
JOhn
8039ab08bb update: updated ticket error handling 2026-08-28 11:12:57 -04:00
JOhn
0cc496a40b update: updated ticketing logic from gui 2026-08-24 18:26:25 -04:00
13 changed files with 527 additions and 143 deletions

View file

@ -775,11 +775,13 @@ class CustomWindow(QMainWindow):
if clear:
self._set_status_font_size(16)
self.status_label.setText('')
self.disable_marquee()
return
text = str(text)
full_text = 'Status: ' + text
metrics = self.status_label.fontMetrics()
available_width = self.status_label.width() - 10
available_width = getattr(self, 'text_width', self.status_label.width() - 10)
if metrics.horizontalAdvance(full_text) > available_width:
self.enable_marquee(text)
@ -809,10 +811,12 @@ class CustomWindow(QMainWindow):
self.navigator.navigate("menu")
def enable_marquee(self, text):
self.marquee_text = text + " "
self.marquee_text = str(text)
self.marquee_gap = " "
self.marquee_position = 0
self.marquee_enabled = True
self.marquee_timer.start(500)
self.update_marquee()
self.marquee_timer.start(180)
def disable_marquee(self):
self.marquee_enabled = False
@ -823,31 +827,33 @@ class CustomWindow(QMainWindow):
return
metrics = self.status_label.fontMetrics()
text_width = metrics.horizontalAdvance(self.marquee_text)
prefix = 'Status: '
content_width = max(
80,
getattr(self, 'text_width', self.status_label.width() - 10)
- metrics.horizontalAdvance(prefix)
)
self.marquee_position += self.scroll_speed
if self.marquee_position >= text_width:
marquee_unit = self.marquee_text + getattr(self, 'marquee_gap', " ")
if not marquee_unit.strip():
self.disable_marquee()
return
if self.marquee_position >= len(marquee_unit):
self.marquee_position = 0
looped_text = self.marquee_text * 2
looped_text = marquee_unit + marquee_unit
visible_text = ''
for char in looped_text[self.marquee_position:]:
next_text = visible_text + char
if metrics.horizontalAdvance(next_text) > content_width:
break
visible_text = next_text
chars_that_fit = metrics.horizontalAdvance(
looped_text[:self.text_width])
visible_text = looped_text[self.marquee_position:
self.marquee_position + chars_that_fit]
while metrics.horizontalAdvance(visible_text) < self.text_width:
visible_text += self.marquee_text
while metrics.horizontalAdvance(visible_text) > self.text_width:
visible_text = visible_text[:-1]
display_text = 'Status: ' + ' ' * \
(self.text_start_x - metrics.horizontalAdvance('Status: '))
display_text += visible_text
display_text = prefix + visible_text
self.status_label.setText(display_text)
self.marquee_position += max(1, int(self.scroll_speed / 7))
def set_scroll_speed(self, speed):
self.scroll_speed = speed

0
gui/v2/actions/disable_profiles.py Normal file → Executable file
View file

View file

@ -1,35 +0,0 @@
def location_candidates(profile, preferred=None):
candidates = []
seen = set()
def add(val):
if val is None:
return
s = str(val).strip().lower().replace(' ', '_')
if s and s not in seen:
seen.add(s)
candidates.append(s)
if preferred:
add(preferred)
sources = []
try:
if profile and profile.connection and profile.connection.location:
sources.append(profile.connection.location)
except Exception:
pass
try:
if profile and profile.location:
sources.append(profile.location)
except Exception:
pass
for source in sources:
add(getattr(source, 'id', None))
add(getattr(source, 'country_name', None))
add(getattr(source, 'name', None))
add(getattr(source, 'code', None))
add(getattr(source, 'country_code', None))
return candidates

View file

@ -0,0 +1,74 @@
import inspect
from enum import Enum
from typing import Any
from core.controllers.ConfigurationController import ConfigurationController
from core.models.Configuration import ConnectionChoice
from core.models.Result import ResultError
from core.services.prepare_tickets import ticket_tracker
DEFAULT_ENUM_MAP = {
ResultError.SUBSCRIPTION: (
ticket_tracker.wipe_one_ticket_sub,
{},
),
ResultError.CONNECTION: (
ConfigurationController.set_connection_enum,
{'connection_enum': ConnectionChoice.SYSTEM},
),
ResultError.INVALID_API_REPLY: None,
}
def has_function(enum_map: dict[Enum, Any], enum_key: Enum) -> bool:
action = enum_map.get(enum_key)
if action is None:
return False
function, _ = _normalize_action(action)
return callable(function)
def error_dispatch(enum_map: dict[Enum, Any], enum_key: Enum, **kwargs: Any) -> Any:
if enum_key not in enum_map:
return None
action = enum_map[enum_key]
if action is None:
return None
function, default_kwargs = _normalize_action(action)
call_kwargs = dict(default_kwargs)
call_kwargs.update(kwargs)
return function(**_filter_kwargs(function, call_kwargs))
def has_result_action(result, enum_map: dict[Enum, Any] | None = None) -> bool:
return has_function(enum_map or DEFAULT_ENUM_MAP, result.error_type)
def dispatch_result_action(result, enum_map: dict[Enum, Any] | None = None, **kwargs: Any) -> Any:
return error_dispatch(enum_map or DEFAULT_ENUM_MAP, result.error_type, **kwargs)
def _normalize_action(action: Any) -> tuple[Any, dict[str, Any]]:
if isinstance(action, tuple):
function = action[0]
if len(action) > 1 and isinstance(action[1], dict):
return function, action[1]
return function, {}
return action, {}
def _filter_kwargs(function: Any, kwargs: dict[str, Any]) -> dict[str, Any]:
signature = inspect.signature(function)
parameters = signature.parameters
if any(param.kind == inspect.Parameter.VAR_KEYWORD for param in parameters.values()):
return kwargs
accepted = {
name
for name, param in parameters.items()
if param.kind in (
inspect.Parameter.POSITIONAL_OR_KEYWORD,
inspect.Parameter.KEYWORD_ONLY,
)
}
return {key: value for key, value in kwargs.items() if key in accepted}

View file

@ -0,0 +1,18 @@
from core.models.Result import Result, ResultError
def result_from_exception(exception: Exception, error_type: ResultError = ResultError.UNKNOWN) -> Result:
message = str(exception) or type(exception).__name__
return Result(valid=False, error_type=error_type, message=message)
def result_from_payload(payload) -> Result:
if isinstance(payload, Result):
return payload
if isinstance(payload, Exception):
return result_from_exception(payload)
return Result(
valid=False,
error_type=ResultError.UNKNOWN,
message=str(payload) or 'The operation could not be completed.',
)

View file

@ -26,5 +26,5 @@ def create_top_section(parent, css_path):
'marquee_timer': marquee_timer,
'text_start_x': 15,
'text_end_x': 420,
'text_width': 405,
'text_width': 500,
}

View file

@ -22,6 +22,8 @@ from core.models.system.SystemProfile import SystemProfile
from gui.v2.infrastructure.setup_observers import ticket_observer
from gui.v2.actions.database_health import GuiStorageDatabaseError
from gui.v2.actions.operation_result_dispatch import has_result_action
from gui.v2.actions.operation_results import result_from_payload
from gui.v2.actions.profile_order import normalize_profile_order
from gui.v2.actions.profile_status import is_profile_enabled_for_gui
from gui.v2.ui.pages.Page import Page
@ -29,6 +31,7 @@ from gui.v2.ui.pages.location_verification_page import LocationVerificationPage
from gui.v2.ui.styles.styles import SCROLLBAR_CYAN_QSS
from gui.v2.ui.popups.confirmation_popup import ConfirmationPopup
from gui.v2.ui.popups.endpoint_verification_popup import EndpointVerificationPopup
from gui.v2.ui.popups.operation_result_popup import OperationResultPopup
from gui.v2.ui.popups.ticket_data_loss_popup import TicketDataLossPopup
from gui.v2.workers.worker import Worker
from gui.v2.workers.worker_thread import WorkerThread
@ -781,7 +784,6 @@ class MenuPage(Page):
profile_location = None
profile_operator = self._location_operator(profile_obj)
operator_name = getattr(profile_operator, 'name', '') if profile_operator else ""
if protocol.lower() == "wireguard" and is_profile_enabled and profile_obj and profile_obj.connection and profile_obj.connection.code == 'wireguard':
verification_widget = self.create_verification_widget(
profile_obj)
@ -789,7 +791,7 @@ class MenuPage(Page):
show_verification_widget = True
label_principal = None
elif operator_name != 'Simplified Privacy' and protocol.lower() == "wireguard":
elif operator_name != 'Simplified Privacy' and protocol.lower() in ("wireguard", "hysteria2", "vless"):
if isinstance(profile_obj, SessionProfile):
text_color = "black"
label_background = QLabel(self)
@ -1258,10 +1260,43 @@ class MenuPage(Page):
self.worker.update_signal.connect(self.update_gui_main_thread)
self.worker.change_page.connect(self.change_app_page)
self.worker.ticket_data_loss.connect(self.show_ticket_data_loss_popup)
self.worker.operation_failed.connect(self.handle_operation_failure)
thread = threading.Thread(target=self.worker.run)
thread.start()
def handle_operation_failure(self, payload):
result = result_from_payload(payload)
if result.valid:
if result.message:
self.update_status.update_status(result.message)
return
message = result.message or result.user_message()
self.update_status.update_status(message)
self.boton_just.setEnabled(True)
self.boton_just_session.setEnabled(True)
self.disconnect_button.setEnabled(True)
self.disconnect_system_wide_button.setEnabled(True)
is_actionable = has_result_action(result)
self.popup = OperationResultPopup(
self,
message=message,
action_button_text="Yes" if is_actionable else "OK",
cancel_button_text="No" if is_actionable else None,
action_result=is_actionable,
)
self.popup.action_selected.connect(
lambda accepted: self.handle_operation_popup_action(accepted))
self.popup.show()
def handle_operation_popup_action(self, accepted):
if self.worker is None:
return
self.worker.handle_operation_popup_choice(accepted)
def show_ticket_data_loss_popup(self, ticket_number, billing_code):
self.custom_window.navigator.navigate("menu")
self.update_status.update_status('Critical: invalid billing code from ticket.')
@ -1307,6 +1342,8 @@ class MenuPage(Page):
lambda: self.handle_sync_after_verification(profile_id))
self.worker_thread.sync_output.connect(
self.update_status.update_values)
self.worker_thread.sync_error.connect(
self.update_status.update_status)
self.worker_thread.start()
else:
self.update_status.update_status("Profile enable aborted")

View file

@ -82,6 +82,8 @@ class SyncScreen(Page):
self.worker_thread = WorkerThread('SYNC')
self.worker_thread.sync_output.connect(self.update_output)
self.worker_thread.sync_error.connect(
self.update_status.update_status)
self.worker_thread.start()
def update_output(self, available_locations, available_browsers, status, is_tor, locations, all_browsers):

View file

@ -1,10 +1,8 @@
from PyQt6.QtWidgets import QLabel, QListWidget, QListWidgetItem, QPushButton
from PyQt6 import QtCore
from core.controllers.ProfileController import ProfileController
from core.controllers.tickets.UseTicketController import get_unused_tickets
from gui.v2.actions.locations import location_candidates
from gui.v2.infrastructure.setup_observers import ticket_observer
from gui.v2.ui.pages.Page import Page
@ -91,20 +89,14 @@ class TicketOrBillingChoicePage(Page):
self.status_label.setText("Pick a ticket from the list first.")
return
which_ticket = item.data(QtCore.Qt.ItemDataRole.UserRole)
profile_id = self.update_status.current_profile_id
try:
profile = ProfileController.get(int(profile_id))
except Exception:
profile = None
candidates = location_candidates(profile)
if not candidates:
self.status_label.setText("Could not determine profile location.")
profile_id = int(self.update_status.current_profile_id)
except (TypeError, ValueError):
self.status_label.setText("Could not determine profile.")
return
profile_data = {
'id': int(profile_id),
'use_ticket': which_ticket,
'ticket_location': candidates[0],
'profile': profile
'id': profile_id,
'use_ticket': which_ticket
}
menu_page = self.custom_window.navigator.get_cached("menu")
if menu_page:

View file

@ -0,0 +1,200 @@
from PyQt6.QtCore import Qt, QTimer, pyqtSignal
from PyQt6.QtGui import QFont, QGuiApplication
from PyQt6.QtWidgets import (
QDialog,
QFrame,
QHBoxLayout,
QLabel,
QPushButton,
QScrollArea,
QVBoxLayout,
QWidget,
)
from gui.v2.ui.styles.styles import (
POPUP_ACTION_BUTTON_RED_QSS,
POPUP_BG_QSS,
POPUP_CANCEL_BUTTON_QSS,
)
class OperationResultPopup(QDialog):
action_selected = pyqtSignal(bool)
def __init__(self, parent=None, message="", title="Operation Failed", action_button_text="Yes", cancel_button_text="No", action_result=True):
super().__init__(parent)
self.parent_window = parent
self.title_text = title or "Operation Failed"
self.message = message or "The operation could not be completed."
self.action_button_text = action_button_text
self.cancel_button_text = cancel_button_text
self.action_result = action_result
self._completed = False
self._use_parent_local_geometry = QGuiApplication.platformName().lower().startswith("wayland")
self.initUI()
def initUI(self):
self.setFixedSize(560, 300)
if self._use_parent_local_geometry:
if self.parent_window is not None:
self.setParent(self.parent_window)
self.setWindowFlags(Qt.WindowType.Widget)
else:
self.setWindowFlags(Qt.WindowType.Dialog | Qt.WindowType.FramelessWindowHint)
self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
outer = QVBoxLayout(self)
outer.setContentsMargins(0, 0, 0, 0)
bg_widget = QWidget(self)
bg_widget.setStyleSheet(POPUP_BG_QSS)
outer.addWidget(bg_widget)
content_layout = QVBoxLayout(bg_widget)
content_layout.setContentsMargins(22, 14, 22, 18)
content_layout.setSpacing(12)
top_row = QHBoxLayout()
top_row.setContentsMargins(0, 0, 0, 0)
top_row.setSpacing(10)
title_label = QLabel(self.title_text)
title_label.setFont(QFont("Arial", 18, QFont.Weight.Bold))
title_label.setStyleSheet("color: #d62828; background: transparent; border: none;")
title_label.setAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter)
title_label.setMinimumWidth(0)
top_row.addWidget(title_label, 1)
close_button = QPushButton("X")
close_button.setFixedSize(36, 36)
close_button.setFont(QFont("Arial", 14, QFont.Weight.Bold))
close_button.setStyleSheet("""
QPushButton {
background-color: transparent;
color: #444444;
font-size: 18px;
font-weight: 900;
border: none;
}
QPushButton:hover {
color: #d62828;
}
""")
close_button.clicked.connect(lambda: self._choose(False))
top_row.addWidget(close_button, 0, Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignTop)
content_layout.addLayout(top_row)
scroll_area = QScrollArea()
scroll_area.setWidgetResizable(True)
scroll_area.setFrameShape(QFrame.Shape.NoFrame)
scroll_area.setStyleSheet("background: transparent; border: none;")
content_layout.addWidget(scroll_area, 1)
scroll_content = QWidget()
scroll_content.setStyleSheet("background: transparent; border: none;")
scroll_layout = QVBoxLayout(scroll_content)
scroll_layout.setContentsMargins(0, 0, 0, 0)
message_label = QLabel(self.message)
message_label.setFont(QFont("Arial", 12))
message_label.setStyleSheet("color: #333333; background: transparent; border: none;")
message_label.setWordWrap(True)
message_label.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse)
message_label.setAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignTop)
scroll_layout.addWidget(message_label)
scroll_layout.addStretch()
scroll_area.setWidget(scroll_content)
button_row = QHBoxLayout()
button_row.addStretch()
if self.cancel_button_text is not None:
cancel_button = QPushButton(self.cancel_button_text)
cancel_button.setFixedSize(max(110, min(180, 22 + len(self.cancel_button_text) * 8)), 42)
cancel_button.setFont(QFont("Arial", 11, QFont.Weight.Bold))
cancel_button.setCursor(Qt.CursorShape.PointingHandCursor)
cancel_button.setStyleSheet(POPUP_CANCEL_BUTTON_QSS)
cancel_button.clicked.connect(lambda: self._choose(False))
button_row.addWidget(cancel_button)
action_button = QPushButton(self.action_button_text)
action_button.setFixedSize(max(110, min(180, 22 + len(self.action_button_text) * 8)), 42)
action_button.setFont(QFont("Arial", 11, QFont.Weight.Bold))
action_button.setCursor(Qt.CursorShape.PointingHandCursor)
action_button.setStyleSheet(POPUP_ACTION_BUTTON_RED_QSS)
action_button.clicked.connect(lambda: self._choose(self.action_result))
button_row.addWidget(action_button)
content_layout.addLayout(button_row)
if not self._use_parent_local_geometry:
self.setWindowModality(Qt.WindowModality.ApplicationModal)
def _choose(self, accepted):
if self._completed:
return
self._completed = True
self.action_selected.emit(accepted)
self.accept()
def closeEvent(self, event):
if not self._completed:
self._completed = True
self.action_selected.emit(False)
event.accept()
def show(self):
self._center_on_parent()
super().show()
def showEvent(self, event):
super().showEvent(event)
self._center_on_parent()
QTimer.singleShot(0, self._center_on_parent)
def _center_on_parent(self):
parent = self.parent_window
if parent is None:
return
try:
if not hasattr(parent, 'mapToGlobal') or not hasattr(parent, 'rect'):
return
parent_local_rect = parent.rect()
if parent_local_rect.isEmpty():
return
my_size = self.size()
if my_size.isEmpty() or my_size.width() <= 0 or my_size.height() <= 0:
my_size = self.sizeHint()
if self._use_parent_local_geometry:
target_x = parent_local_rect.x() + (parent_local_rect.width() - my_size.width()) // 2
target_y = parent_local_rect.y() + (parent_local_rect.height() - my_size.height()) // 2
target_x = max(parent_local_rect.left(), min(target_x, parent_local_rect.right() - my_size.width() + 1))
target_y = max(parent_local_rect.top(), min(target_y, parent_local_rect.bottom() - my_size.height() + 1))
else:
parent_center = parent.mapToGlobal(parent_local_rect.center())
target_x = parent_center.x() - my_size.width() // 2
target_y = parent_center.y() - my_size.height() // 2
screen = None
if hasattr(parent, 'screen'):
try:
screen = parent.screen()
except Exception:
screen = None
if screen is not None:
avail = screen.availableGeometry()
target_x = max(avail.left(), min(target_x, avail.right() - my_size.width()))
target_y = max(avail.top(), min(target_y, avail.bottom() - my_size.height()))
self.setGeometry(target_x, target_y, my_size.width(), my_size.height())
if self._use_parent_local_geometry:
self.raise_()
self.setFocus(Qt.FocusReason.PopupFocusReason)
except Exception:
pass
def mousePressEvent(self, event):
self.oldPos = event.globalPosition().toPoint()
def mouseMoveEvent(self, event):
delta = event.globalPosition().toPoint() - self.oldPos
self.move(self.x() + delta.x(), self.y() + delta.y())
self.oldPos = event.globalPosition().toPoint()

View file

@ -7,7 +7,6 @@ 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
from gui.v2.infrastructure.setup_observers import (
connection_observer,
@ -22,7 +21,6 @@ class TicketingWorkerThread(QThread):
not_paid = pyqtSignal()
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)
@ -71,14 +69,5 @@ class TicketingWorkerThread(QThread):
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(
which_ticket=self.params['which_ticket'],
which_location=self.params['which_location'],
ticket_observer=ticket_observer,
connection_observer=connection_observer,
profile=self.params['profile']
)
self.use_done.emit(result)
except Exception as e:
self.error.emit(str(e))

View file

@ -24,8 +24,9 @@ from core.Errors import (
UnsupportedApplicationVersionError,
)
from gui.v2.actions.locations import location_candidates
from gui.v2.actions.database_health import GuiStorageDatabaseError
from gui.v2.actions.operation_result_dispatch import dispatch_result_action
from gui.v2.actions.operation_results import result_from_exception
from gui.v2.infrastructure.screen_size import get_max_screensize
from gui.v2.infrastructure.setup_observers import (
application_version_observer,
@ -39,6 +40,7 @@ class Worker(QObject):
update_signal = pyqtSignal(str, bool, int, int, str)
change_page = pyqtSignal(str, bool)
ticket_data_loss = pyqtSignal(str, str)
operation_failed = pyqtSignal(object)
def __init__(self, profile_data):
self.profile_data = profile_data
@ -50,6 +52,8 @@ class Worker(QObject):
self.profile_type = None
self._ticket_error_emitted = False
self._consumed_ticket = None
self._pending_operation_result = None
self._pending_operation_kwargs = {}
def run(self):
try:
@ -70,8 +74,7 @@ class Worker(QObject):
if 'use_ticket' in self.profile_data:
ticket_billing_code = self._consume_ticket(
self.profile_data['use_ticket'],
self.profile_data.get('ticket_location'))
self.profile_data['use_ticket'])
if ticket_billing_code is None:
return
self.profile_data['billing_code'] = ticket_billing_code
@ -84,18 +87,20 @@ class Worker(QObject):
self.profile_data['billing_code'] = ticket_billing_code
if 'billing_code' in self.profile_data:
try:
subscription = SubscriptionController.get(
self.profile_data['billing_code'], connection_observer=connection_observer)
except Exception as e:
if self._consumed_ticket is not None:
self._emit_subscription_lookup_failure(exception=e)
return
subscription = None
if subscription is not None:
ProfileController.attach_subscription(
self.profile, subscription)
else:
if self._consumed_ticket is not None:
self._ticket_error_emitted = True
self.ticket_data_loss.emit(
self._consumed_ticket,
str(self.profile_data.get('billing_code', '')),
)
self._emit_subscription_lookup_failure()
return
self.change_page.emit('The billing code is invalid.', True)
return
@ -108,13 +113,19 @@ class Worker(QObject):
ignore_exceptions.append(ProfileStateConflictError)
ignore_tuple = tuple(ignore_exceptions)
max_resolution = get_max_screensize()
ProfileController.enable(self.profile, ignore=ignore_tuple, profile_observer=profile_observer,
enable_result = ProfileController.enable(self.profile, ignore=ignore_tuple, profile_observer=profile_observer,
application_version_observer=application_version_observer,
connection_observer=connection_observer, ticket_observer=ticket_observer, max_resolution=max_resolution)
if isinstance(enable_result, Result) and not enable_result.valid:
self._emit_operation_failure(enable_result)
return
except EndpointVerificationError:
self.update_signal.emit(
"ENDPOINT_VERIFICATION_ERROR", False, self.profile_data['id'], None, None)
except (InvalidSubscriptionError, MissingSubscriptionError) as e:
if self._consumed_ticket is not None:
self._emit_subscription_lookup_failure(exception=e)
else:
self.change_page.emit(
f"Subscription missing or invalid for profile {self.profile_data['id']}", True)
except ProfileActivationError:
@ -170,9 +181,6 @@ class Worker(QObject):
return None
def _location_candidates(self, preferred=None):
return location_candidates(self.profile, preferred)
def _profile_has_valid_subscription(self):
try:
sub = getattr(self.profile, 'subscription', None)
@ -201,55 +209,137 @@ class Worker(QObject):
try:
print("using a random ticket from GUI...")
which_ticket, error_msg = do_we_use_a_random_ticket(ticket_observer)
except Exception:
except Exception as e:
self._emit_ticket_failure(None, exception=e)
return None
if error_msg:
self._ticket_error_emitted = True
self.change_page.emit(f'Ticket use failed: {error_msg}', True)
self._emit_ticket_failure(
None,
result=Result(
valid=False,
error_type=ResultError.TICKET,
message=f'Ticket use failed: {error_msg}',
),
)
return None
if which_ticket is None or which_ticket == 'error':
return None
return self._consume_ticket(which_ticket, None)
return self._consume_ticket(which_ticket)
def _consume_ticket(self, which_ticket, which_location):
candidates = self._location_candidates(preferred=which_location)
if not candidates:
self._ticket_error_emitted = True
self.change_page.emit('Could not determine profile location for ticket use.', True)
def _ticket_location_id(self):
location = getattr(self.profile, 'location', None)
location_id = getattr(location, 'id', None)
if location_id is None:
return None
if isinstance(location_id, str):
location_id = location_id.strip()
if location_id == '':
return None
try:
return int(location_id)
except (TypeError, ValueError):
return None
def _emit_ticket_failure(self, which_ticket, result=None, message=None, exception=None):
self._ticket_error_emitted = True
if exception is not None:
result = result_from_exception(exception)
elif result is None:
result = Result(
valid=False,
error_type=ResultError.UNKNOWN,
message=message or 'Ticket use failed.',
)
self._emit_operation_failure(result, which_ticket=which_ticket)
def _emit_operation_failure(self, result, **kwargs):
self._pending_operation_result = result
self._pending_operation_kwargs = {
key: value for key, value in kwargs.items()
if value is not None
}
self.operation_failed.emit(result)
def handle_operation_popup_choice(self, accepted):
if not accepted:
return None
if not isinstance(self._pending_operation_result, Result):
return None
return dispatch_result_action(
self._pending_operation_result,
**self._pending_operation_kwargs,
)
def _emit_subscription_lookup_failure(self, exception=None):
message = (
"Ticket was accepted, but the subscription details could not be "
"retrieved. Check your connection and try again."
)
if exception is not None:
error_text = str(exception)
if error_text:
message = f"{message} {error_text}"
self._emit_ticket_failure(
self._consumed_ticket,
result=Result(
valid=False,
error_type=ResultError.CONNECTION,
message=message,
),
)
def _consume_ticket(self, which_ticket):
which_location = self._ticket_location_id()
if which_location is None:
self._emit_ticket_failure(
which_ticket,
result=Result(
valid=False,
error_type=ResultError.MISSING_DATA,
message='Could not determine profile location for ticket use.',
),
)
return None
last_msg = None
for cand in candidates:
try:
outcome = use_ticket(
which_ticket=which_ticket,
which_location=cand,
which_location=which_location,
ticket_observer=ticket_observer,
connection_observer=connection_observer,
profile=self.profile
)
except Exception as e:
last_msg = str(e)
continue
if not isinstance(outcome, dict):
last_msg = 'invalid_response'
continue
billing_code = outcome.get('billing_code')
if outcome.get('valid') and billing_code:
self._consumed_ticket = str(which_ticket)
return billing_code
if billing_code:
self._consumed_ticket = str(which_ticket)
return billing_code
msg = outcome.get('message', 'failed')
last_msg = msg
if msg != 'invalid_location':
self._ticket_error_emitted = True
self.change_page.emit(f'Ticket use failed: {msg}', True)
self._emit_ticket_failure(which_ticket, exception=e)
return None
self._ticket_error_emitted = True
self.change_page.emit(f'Ticket use failed: {last_msg or "no valid location"}', True)
if not isinstance(outcome, Result):
self._emit_ticket_failure(
which_ticket,
result=Result(
valid=False,
error_type=ResultError.INVALID_API_REPLY,
message=f'use_ticket returned {type(outcome).__name__}, expected Result.',
),
)
return None
if outcome.valid:
billing_code = outcome.data
if not billing_code:
self._emit_ticket_failure(
which_ticket,
result=Result(
valid=False,
error_type=ResultError.INVALID_API_REPLY,
message='Ticket use succeeded but no billing code was returned.',
),
)
return None
self._consumed_ticket = str(which_ticket)
return billing_code
self._emit_ticket_failure(which_ticket, result=outcome)
return None
def handle_profile_status(self, profile, is_enabled):

View file

@ -8,6 +8,7 @@ from PyQt6.QtCore import QThread, pyqtSignal
from core.controllers.ApplicationVersionController import ApplicationVersionController
from core.controllers.ClientController import ClientController
from core.controllers.ConfigurationController import ConfigurationController, ConnectionChoice
from core.Constants import Constants
from core.controllers.SyncController import new_sync
from core.controllers.InvoiceController import InvoiceController
@ -42,6 +43,7 @@ from gui.v2.infrastructure.setup_observers import (
class WorkerThread(QThread):
text_output = pyqtSignal(str)
sync_output = pyqtSignal(list, list, bool, bool, list, list)
sync_error = pyqtSignal(str)
invoice_output = pyqtSignal(object, str)
invoice_finished = pyqtSignal(bool)
profiles_output = pyqtSignal(dict)
@ -334,7 +336,15 @@ class WorkerThread(QThread):
ConfigurationController.set_connection('tor')
else:
ConfigurationController.set_connection('system')
self.check_for_update()
print(f"[SYNC] Syncing from URL: {Constants.SP_API_BASE_URL}/cachedsync")
sync_result = new_sync(
client_observer=client_observer,
connection_observer=connection_observer)
if not sync_result.valid:
error_message = sync_result.message or "Connection error or invalid sync URL"
self.sync_output.emit([], [], False, False, [], [])
self.sync_error.emit(f"Sync failed: {error_message}")
return
locations = LocationController.get_all()
@ -350,6 +360,7 @@ class WorkerThread(QThread):
except Exception as e:
print(f'the error is: {e}')
self.sync_output.emit([], [], False, False, [], [])
self.sync_error.emit(f"Sync failed: {e}")
def get_connection(self):
connection = ConfigurationController.get_connection()