forked from Support/sp-hydra-veil-gui
232 lines
9.7 KiB
Python
Executable file
232 lines
9.7 KiB
Python
Executable file
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):
|
|
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}"
|