sp-hydra-veil-gui/gui/v2/workers/worker_thread.py

415 lines
17 KiB
Python
Executable file

import shlex
import subprocess
# import inspect
import time
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.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.errors.exceptions import SudoScript, MissingPreReqs, FirewallError
from core.models.Result import Result, ResultError
from core.services.helpers.install_dependencies import setup_singbox_binary as install_singbox_binary
from gui.v2.actions.disable_profiles import (
filter_profiles_by_type,
disable_profile_via_controller
)
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)
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 _disable_profile(self, profile):
# kwargs = {
# 'profile_observer': profile_observer,
# 'ticket_observer': ticket_observer,
# 'connection_observer': connection_observer,
# }
# supported = inspect.signature(ProfileController.disable).parameters
# ProfileController.disable(
# profile,
# **{key: value for key, value in kwargs.items() if key in supported}
# )
def run(self):
if self.action == 'LIST_PROFILES':
self.list_profiles()
elif self.action == 'CREATE_SESSION_PROFILE':
self.create_profile(self.profile_type)
elif self.action == 'CREATE_SYSTEM_PROFILE':
self.create_profile(self.profile_type)
elif self.action == 'GET_SUBSCRIPTION':
self.get_subscription()
elif self.action == 'DISABLE_PROFILE':
self.disable_profile()
elif self.action == 'SYNC' or self.action == 'SYNC_TOR':
self.sync()
elif self.action == 'DESTROY_PROFILE':
self.destroy_profile()
elif self.action == 'DISABLE_ALL_PROFILES':
self.disable_all_profiles()
elif self.action == 'INSTALL_PACKAGE':
self.install_package()
elif self.action == 'SETUP_SINGBOX_BINARY':
self.setup_singbox_binary()
elif self.action == 'CHECK_FOR_UPDATE':
self.check_for_update()
elif self.action == 'DOWNLOAD_UPDATE':
self.download_update()
elif self.action == 'CHECK_INVOICE_STATUS':
self.check_invoice_status()
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.text_output.emit("Starting update process...")
ClientController.update(client_observer=client_observer)
client_observer.subscribe('update_progressing', lambda event: self.text_output.emit(
f"Downloading: {event.meta.get('progress'):.1f}%"))
client_observer.subscribe(
'updated', lambda event: self.text_output.emit("Update process completed"))
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):
"""
Purpose:
Loop through all profiles in the class data,
Classify them, and disable them.
Why:
Session profiles must be disabled first before System profiles.
Called by:
Same Class run()
"""
session_profiles, system_profiles = filter_profiles_by_type(self.profile_data)
try:
# SESSION
for profile in session_profiles:
disable_profile_via_controller(profile)
print("finished with session profiles. now moving onto session profiles")
if session_profiles and system_profiles:
time.sleep(1)
# SYSTEM
for profile in system_profiles:
disable_profile_via_controller(profile)
self.text_output.emit("All profiles were successfully disabled")
except SudoScript as e:
self.text_output.emit(str(e))
except FirewallError as e:
self.text_output.emit(str(e))
except MissingPreReqs as e:
self.text_output.emit(str(e))
except Exception as e:
print(f"Error: {str(e)}")
self.text_output.emit("An error occurred when disabling profile")
finally:
self.finished.emit(True)
def destroy_profile(self):
profile_id = int(self.profile_data['id'])
profile = ProfileController.get(profile_id)
if profile is not None:
try:
ProfileController.destroy(profile, profile_observer, ticket_observer, connection_observer)
except Exception as e:
error_name = type(e).__name__
error_text = str(e) or 'Unknown deletion error'
self.text_output.emit(
f'Could not delete profile {profile_id}: {error_name}: {error_text}')
self.finished.emit(False)
return
self.text_output.emit(f'Profile {profile_id} deleted')
self.finished.emit(True)
else:
self.text_output.emit(f'Profile {profile_id} does not exist')
self.finished.emit(False)
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):
try:
profile = ProfileController.get(int(self.profile_data['id']))
if profile:
disable_profile_via_controller(profile)
else:
self.text_output.emit(
f"No profile found with ID: {self.profile_data['id']}")
except SudoScript as e:
self.text_output.emit(str(e))
except FirewallError as e:
self.text_output.emit(str(e))
except MissingPreReqs as e:
self.text_output.emit(str(e))
except Exception as e:
self.text_output.emit("An error occurred when disabling profile")
finally:
self.finished.emit(True)
def sync(self):
try:
if self.action == 'SYNC_TOR':
ConfigurationController.set_connection('tor')
else:
ConfigurationController.set_connection('system')
self.check_for_update()
locations = LocationController.get_all()
browser = ApplicationVersionController.get_all()
# print('the browser is: ', browser)
all_browser_versions = [
f"{browser.application_code}:{browser.version_number}" for browser in browser if browser.supported]
all_location_codes = [
f"{location.country_code}_{location.code}" for location in locations]
self.sync_output.emit(
all_location_codes, all_browser_versions, True, False, locations, browser)
except Exception as e:
print(f'the error is: {e}')
self.sync_output.emit([], [], False, False, [], [])
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.")