import shlex import subprocess from PyQt6.QtCore import QThread, pyqtSignal from core.controllers.ApplicationVersionController import ApplicationVersionController from core.controllers.ClientController import ClientController from core.controllers.ConfigurationController import ConfigurationController from core.controllers.SyncController import new_sync from core.controllers.InvoiceController import InvoiceController from core.controllers.LocationController import LocationController from core.controllers.ProfileController import ProfileController from core.controllers.SubscriptionController import SubscriptionController from core.controllers.SubscriptionPlanController import SubscriptionPlanController from core.models.session.SessionConnection import SessionConnection from core.models.session.SessionProfile import SessionProfile from core.models.system.SystemConnection import SystemConnection from core.models.BaseProfile import ProfileType from core.models.system.SystemProfile import SystemProfile from core.models.Result import ResultError from core.services.helpers.install_dependencies import setup_singbox_binary as install_singbox_binary from gui.v2.actions.profile_operations import execute_menu_task, synchronize from gui.v2.actions.menu_results import MenuResult from gui.v2.infrastructure.setup_observers import ( application_version_observer, client_observer, connection_observer, invoice_observer, profile_observer, ticket_observer, ) 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) special_output = pyqtSignal(str) finished = pyqtSignal(bool) update_finished = pyqtSignal(dict) def __init__(self, action=None, profile_data=None, profile_type=None, package_name=None, package_command=None): super().__init__() self.action = action self.profile_data = profile_data self.profile_type = profile_type self.package_name = package_name self.package_command = package_command self.is_running = True self.is_disabling = False def run(self): action_handlers = { 'LIST_PROFILES': self.list_profiles, 'CREATE_SESSION_PROFILE': lambda: self.create_profile(self.profile_type), 'CREATE_SYSTEM_PROFILE': lambda: self.create_profile(self.profile_type), 'GET_SUBSCRIPTION': self.get_subscription, 'DISABLE_PROFILE': self.disable_profile, 'SYNC': self.sync, 'SYNC_TOR': self.sync, 'DESTROY_PROFILE': self.destroy_profile, 'DISABLE_ALL_PROFILES': self.disable_all_profiles, 'INSTALL_PACKAGE': self.install_package, 'SETUP_SINGBOX_BINARY': self.setup_singbox_binary, 'CHECK_FOR_UPDATE': self.check_for_update, 'DOWNLOAD_UPDATE': self.download_update, 'CHECK_INVOICE_STATUS': self.check_invoice_status, } handler = action_handlers.get(self.action) if handler is not None: handler() def check_invoice_status(self): try: invoice = InvoiceController.get(self.profile_data['billing_code']) if invoice: status = invoice.status if status == "expired": self.invoice_finished.emit(False) else: self.invoice_finished.emit(True) else: self.invoice_finished.emit(False) except Exception as e: print(f"Error retrieving invoice: {str(e)}") self.invoice_finished.emit(False) def download_update(self): self._menu_operation({"task": "update"}) def check_for_update(self): self.text_output.emit("Checking for updates...") new_sync(client_observer=client_observer, connection_observer=connection_observer) update_available = ClientController.can_be_updated() if update_available: self.text_output.emit("An update is available. Downloading...") self.finished.emit(True) else: self.text_output.emit("No updates available.") self.finished.emit(False) def install_package(self): try: self.text_output.emit(f"Installing {self.package_name}...") subprocess.run(shlex.split(self.package_command), check=True) self.text_output.emit(f"{self.package_name} installed!") self.finished.emit(True) except subprocess.CalledProcessError: self.text_output.emit("Installation failed") self.finished.emit(False) except Exception as e: self.text_output.emit( f"An error occurred when installing {self.package_name}: {e}") self.finished.emit(False) def setup_singbox_binary(self): connection_error = "Connection problems downloading Singbox or related data. Please disable Tor or try again with a better connection." try: setup_result = install_singbox_binary(application_version_observer, connection_observer) except ConnectionError as e: self.text_output.emit(f"{connection_error}: {str(e)}") self.finished.emit(False) return except ValueError as e: self.text_output.emit(f"Your configuration files may be corrupted, or a server-side error gave bad data: {str(e)}") self.finished.emit(False) return except Exception as e: self.text_output.emit(f"Unknown error: {str(e)}") self.finished.emit(False) return if setup_result.valid: self.text_output.emit("Setup done. You're all set to proceed with Singbox.") self.finished.emit(True) return messages = { ResultError.NEED_SYNC: "You must sync to find out which Singbox version is supported.", ResultError.FILE_SYSTEM: "Please check the configuration file, disk space, permissions, and filesystem health.", ResultError.CONNECTION: connection_error, ResultError.INVALID_INPUT: "This is a rare bug. Check the error logs, then run the program again from the terminal with DEBUG=true.", ResultError.PERMISSION: "Singbox requires sudo for setup. After that, the wrapper allows it to run without sudo on an ongoing basis.", ResultError.MISSING_FILE: "Singbox download or file setup did not complete. Please try again.", ResultError.UNKNOWN: f"Unknown error: {setup_result.message}", } self.text_output.emit(messages.get(setup_result.error_type, setup_result.message or "Unknown Singbox setup error")) self.finished.emit(False) def disable_all_profiles(self): self._menu_operation({"task": "disable_all", "connected_ids": self.profile_data}) def destroy_profile(self): self._menu_operation({"task": "delete", "id": self.profile_data["id"], "confirmed": True}) def list_profiles(self): try: profiles = ProfileController.get_all() except Exception: profiles = {} self.text_output.emit("Could not load profiles. Sync or restart and try again.") self.profiles_output.emit(profiles) def create_profile(self, profile_type): try: location = LocationController.get( self.profile_data['country_code'], self.profile_data['code']) except Exception: location = None if location is None: self.text_output.emit( f"Invalid location code: {self.profile_data['location_code']}") return profile_id = int( self.profile_data['id']) if self.profile_data['id'] else None name = self.profile_data['name'] connection_type = self.profile_data['connection_type'] if profile_type == "session": application_details = self.profile_data['application'].split( ':', 1) try: application_version = ApplicationVersionController.get( application_details[0], application_details[1] if len(application_details) > 1 else None) except Exception: application_version = None if application_version is None: self.text_output.emit( f"Invalid application: {self.profile_data['application']}") return mask_connection = True if connection_type == 'tor' or connection_type == 'system' else False resolution = self.profile_data['resolution'] connection = SessionConnection(connection_type, mask_connection) profile = SessionProfile( id=profile_id, name=name, subscription=None, type=ProfileType.SESSION, location=location, resolution=resolution, application_version=application_version, connection=connection) elif profile_type == "system": connection = SystemConnection(connection_type) profile = SystemProfile( id=profile_id, name=name, type=ProfileType.SYSTEM, subscription=None, location=location, connection=connection) else: self.text_output.emit(f"Invalid profile type: {profile_type}") return try: ProfileController.create( profile, profile_observer=profile_observer) except Exception as e: self.text_output.emit( f"An error occurred when creating profile {profile.id}") self.text_output.emit( f"{profile_type.capitalize()} Profile created with ID: {profile.id}") def disable_profile(self): self._menu_operation({"task": "disable", "id": self.profile_data["id"]}) def _menu_operation(self, request): result = execute_menu_task(request, self._emit_menu_result, None) self._emit_menu_result(result) self.finished.emit(result["status"] == "success") def _emit_menu_result(self, result: MenuResult): self.text_output.emit(result.get("message", "")) def sync(self): connection = {"SYNC_TOR": "tor", "SYNC": "system"}[self.action] result = synchronize(connection) self.sync_output.emit(*result["sync_data"]) if result["status"] == "error": self.sync_error.emit(result["message"]) def get_connection(self): connection = ConfigurationController.get_connection() self.text_output.emit(f"Current connection: {connection}") def set_connection(self): ConfigurationController.set_connection( self.profile_data['connection_type']) self.text_output.emit( f"Connection set to '{self.profile_data['connection_type']}'") def get_subscription(self): try: invoice_observer.subscribe('retrieved', lambda event: self.handle_event( event, self.profile_data['currency'])) invoice_observer.subscribe('processing', lambda event: self.text_output.emit( 'A payment has been detected and is being verified...')) invoice_observer.subscribe('settled', lambda event: self.text_output.emit( 'The payment has been successfully verified.')) profile = ProfileController.get(int(self.profile_data['id'])) subscription_plan = SubscriptionPlanController.get( profile.connection, self.profile_data['duration']) if subscription_plan is None: self.text_output.emit( 'No compatible subscription plan was found.') return potential_subscription = SubscriptionController.create( subscription_plan, profile, connection_observer=connection_observer) if potential_subscription is not None: ProfileController.attach_subscription( profile, potential_subscription) else: self.text_output.emit( 'The subscription could not be created. Try again later.') return subscription = InvoiceController.handle_payment( potential_subscription.billing_code, invoice_observer=invoice_observer, connection_observer=connection_observer) if subscription is not None: ProfileController.attach_subscription(profile, subscription) self.text_output.emit( 'Successfully activated the subscription') self.invoice_finished.emit(True) else: self.text_output.emit( 'The subscription could not be activated. Try again later.') self.invoice_finished.emit(False) except Exception as e: self.text_output.emit('An unknown error occurred') self.invoice_finished.emit(False) def handle_connection_events(self, event): self.text_output.emit(f'Profile disabled') def handle_event(self, event, currency=None): invoice = event.subject if isinstance(invoice, object): self.invoice_output.emit(invoice, '') self.text_output.emit("Invoice generated. Awaiting payment...") else: self.text_output.emit("Invalid invoice data received.")