from datetime import datetime, timezone from PyQt6.QtCore import QObject, pyqtSignal from core.controllers.ProfileController import ProfileController from core.controllers.SubscriptionController import SubscriptionController from core.controllers.tickets.UseTicketController import ( use_ticket, do_we_use_a_random_ticket, ) from core.models.session.SessionProfile import SessionProfile from core.models.system.SystemProfile import SystemProfile from core.Errors import ( CommandNotFoundError, EndpointVerificationError, FileIntegrityError, InvalidSubscriptionError, MissingSubscriptionError, ProfileActivationError, ProfileModificationError, ProfileStateConflictError, UnsupportedApplicationVersionError, ) from gui.v2.actions.locations import location_candidates from gui.v2.infrastructure.setup_observers import ( application_version_observer, connection_observer, profile_observer, ticket_observer, ) class Worker(QObject): update_signal = pyqtSignal(str, bool, int, int, str) change_page = pyqtSignal(str, bool) ticket_data_loss = pyqtSignal(str, str) def __init__(self, profile_data): self.profile_data = profile_data super().__init__() profile_observer.subscribe( 'disabled', lambda event: self.handle_profile_status(event.subject, False)) profile_observer.subscribe( 'enabled', lambda event: self.handle_profile_status(event.subject, True)) self.profile_type = None self._ticket_error_emitted = False self._consumed_ticket = None def run(self): self.profile = ProfileController.get(int(self.profile_data['id'])) if 'use_ticket' in self.profile_data: ticket_billing_code = self._consume_ticket( self.profile_data['use_ticket'], self.profile_data.get('ticket_location')) if ticket_billing_code is None: return self.profile_data['billing_code'] = ticket_billing_code elif 'billing_code' not in self.profile_data and self.profile is not None: if not self._profile_has_valid_subscription(): ticket_billing_code = self._maybe_auto_use_ticket() if ticket_billing_code is None and self._ticket_error_emitted: return if ticket_billing_code: self.profile_data['billing_code'] = ticket_billing_code if 'billing_code' in self.profile_data: subscription = SubscriptionController.get( self.profile_data['billing_code'], connection_observer=connection_observer) 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', '')), ) return self.change_page.emit('The billing code is invalid.', True) return if self.profile: try: ignore_exceptions = [] if self.profile_data.get('ignore_endpoint_verification', False): ignore_exceptions.append(EndpointVerificationError) if self.profile_data.get('ignore_profile_state_conflict', False): ignore_exceptions.append(ProfileStateConflictError) ignore_tuple = tuple(ignore_exceptions) ProfileController.enable(self.profile, ignore=ignore_tuple, profile_observer=profile_observer, application_version_observer=application_version_observer, connection_observer=connection_observer) except EndpointVerificationError: self.update_signal.emit( "ENDPOINT_VERIFICATION_ERROR", False, self.profile_data['id'], None, None) except (InvalidSubscriptionError, MissingSubscriptionError) as e: self.change_page.emit( f"Subscription missing or invalid for profile {self.profile_data['id']}", True) except ProfileActivationError: self.update_signal.emit( "The profile could not be enabled", False, None, None, None) except UnsupportedApplicationVersionError: self.update_signal.emit( "The application version in question is not supported", False, None, None, None) except FileIntegrityError: self.update_signal.emit( "Application version file integrity could not be verified.", False, None, None, None) except ProfileModificationError: self.update_signal.emit( "WireGuard configuration could not be attached.", False, None, None, None) except ProfileStateConflictError: self.update_signal.emit( "PROFILE_STATE_CONFLICT_ERROR", False, self.profile_data['id'], None, None) except CommandNotFoundError as e: self.update_signal.emit(str(e.subject), False, -1, None, None) except Exception as e: print(e) self.update_signal.emit( "An unknown error occurred", False, None, None, None) else: self.update_signal.emit( f"No profile found with ID: {self.profile_data['id']}", False, None, None, 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) if not sub: return False expires_at = getattr(sub, 'expires_at', None) if not expires_at: return False if expires_at.tzinfo is None: expires_at = expires_at.replace(tzinfo=timezone.utc) return expires_at > datetime.now(timezone.utc) except Exception: return False def _maybe_auto_use_ticket(self): profile_connection_type = self.profile.connection.code if profile_connection_type != "wireguard": error_msg = "Tickets are not yet supported for Tor. We are very sorry, but we're not able to develop all features for all products." print(error_msg) return None try: which_ticket, error_msg = do_we_use_a_random_ticket(ticket_observer) except Exception: return None if error_msg: self._ticket_error_emitted = True self.change_page.emit(f'Ticket use failed: {error_msg}', True) return None if which_ticket is None or which_ticket == 'error': return None return self._consume_ticket(which_ticket, None) 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) return None last_msg = None for cand in candidates: try: outcome = use_ticket(which_ticket, cand, ticket_observer, connection_observer) 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) return None self._ticket_error_emitted = True self.change_page.emit(f'Ticket use failed: {last_msg or "no valid location"}', True) return None def handle_profile_status(self, profile, is_enabled): profile_id = profile.id profile_connection = str(profile.connection.code) message = self.generate_profile_message(profile, is_enabled) if isinstance(profile, SessionProfile): self.profile_type = 1 elif isinstance(profile, SystemProfile): self.profile_type = 2 else: self.profile_type = None self.update_signal.emit( message, is_enabled, profile_id, self.profile_type, profile_connection) @staticmethod def generate_profile_message(profile, is_enabled, idle=False): profile_id = profile.id if not profile.subscription or not profile.subscription.expires_at: return f"Offline. No subscription found." profile_date = profile.subscription.expires_at status = 'enabled' if is_enabled else 'disabled' expiration_date = profile_date.replace(tzinfo=timezone.utc) time_left = expiration_date - datetime.now(timezone.utc) days_left = time_left.days hours_left, remainder = divmod(time_left.seconds, 3600) formatted_expiration = expiration_date.strftime("%Y-%m-%d %H:%M:%S") if expiration_date < datetime.now(timezone.utc): return "Offline. Subscription has expired." if idle: return f"Offline. Expires in {days_left} days." if is_enabled: return f"Profile {int(profile_id)} {status}. Expires on {formatted_expiration}. Time left: {days_left} days, {hours_left} hours." else: return f"Profile {int(profile_id)} {status}"