diff --git a/gui/v2/actions/menu_coordinator.py b/gui/v2/actions/menu_coordinator.py index bd273e8..33e5f40 100644 --- a/gui/v2/actions/menu_coordinator.py +++ b/gui/v2/actions/menu_coordinator.py @@ -1,23 +1,62 @@ +# core +from core.controllers.ProfileController import ProfileController +from core.models.Configuration import Configuration, ConnectionChoice from core.services.subscriptions.subscriptions_by_protocol import get_profile_subscription, start_new_payment_cycle from core.models.enum_models import PaymentCategory +from core.utils.type_checker import type_checker from core.models.Result import Result, ResultError from core.errors.logger import logger +from core.Errors import ( + CommandNotFoundError, EndpointVerificationError, FileIntegrityError, + InvalidSubscriptionError, MissingSubscriptionError, ProfileActivationError, + ProfileModificationError, ProfileStateConflictError, UnsupportedApplicationVersionError, +) +# gui +from gui.v2.infrastructure.action_registry import register_action from gui.v2.infrastructure.take_plumbing_action import take_action +from gui.v2.infrastructure.screen_size import get_max_screensize +from gui.v2.infrastructure.setup_observers import ( + application_version_observer, connection_observer, + profile_observer, ticket_observer, +) # generic -from typing import Callable, Optional +from typing import Callable, Optional, Type + +ERROR_TO_UI_MESSAGE = { + ProfileActivationError: "The profile could not be enabled", + UnsupportedApplicationVersionError: "The application version in question is not supported", + FileIntegrityError: "Application version file integrity could not be verified.", + ProfileModificationError: "WireGuard configuration could not be attached.", +} -def confirm_prereqs(PRE_REQS: list, allow_false: Optional[bool] = False) -> Result: - for each_prereq in PRE_REQS: - if allow_false: - if each_prereq == None: - return Result(valid=False, error_type=ResultError.INVALID_INPUT, message=f"You are missing {each_prereq}") - else: - if not each_prereq: - return Result(valid=False, error_type=ResultError.INVALID_INPUT, message=f"You are missing {each_prereq}") - return Result(valid=True) +def get_profile(profile_id: int): + profile = ProfileController.get(int(profile_id)) + if not profile: + return Result(valid=False, error_type=ResultError.INVALID_INPUT, message=f"There is no profile with the id {profile_id}") + else: + return Result(valid=True, data=profile) + +def get_ignored_exceptions(ignore_state_conflict: Optional[bool] = False): + ignored_as_list = [] + config = Configuration.get() + endpoint_verification_enabled = config.endpoint_verification_enabled + + if endpoint_verification_enabled: + ignored_as_list.append(EndpointVerificationError) + + if ignore_state_conflict: + ignored_as_list.append(ProfileStateConflictError) + + ignored = tuple(ignored_as_list) + return ignored + + +@register_action(ResultError.SUBSCRIPTION) +def navigate_to_new_sub_page() -> Result: + return Result(valid=False, error_type=ResultError.NAVIGATE_TO, navigate_to="id", message="No subscription. Pick a payment method.") def filter_subscription(profile_id: int) -> Result: @@ -28,7 +67,7 @@ def filter_subscription(profile_id: int) -> Result: if not subscription or not subscription.billing_code: # auto check tickets would be here. - return Result(valid=False, error_type=ResultError.NAVIGATE_TO, navigate_to="id", message="No subscription. Pick a payment method.") + return navigate_to_new_sub_page() if subscription.billing_pending: start_result = start_new_payment_cycle(category=PaymentCategory.INDIVIDUAL, profile_id=profile_id) @@ -44,24 +83,152 @@ def filter_subscription(profile_id: int) -> Result: return Result(valid=True) + +@register_action(ResultError.TOR_NEEDED) +def switch_to_tor_connection() -> Result: + config = Configuration.get() + config.connection = ConnectionChoice.TOR + config.save() + return Result(valid=True) + + +def NOT_need_tor_warnings(profile_id: int) -> Result: + """ + Purpose: + Filters if we need to warn the user about a potential Tor leak. + + Context: + If it's a Tor profile, and the user does NOT have Tor connection enabled, do we need to warn them? + + Returns: + If we need it, it emits Result as False, with ResultError.TOR_NEEDED + If not, returns Result as True, to move on. + """ + + # PROFILE + profile_result = get_profile(profile_id) + profile = profile_result.data + + # Actual Profile is a Tor Profile: + if profile.connection.code != "tor": + return Result(valid=True) # no warning needed, it's not even Tor + + # Config shows a non-Tor connection to download apps: + config = Configuration.get() + if config.connection != ConnectionChoice.TOR: + return Result(valid=True) # no warning needed, it's already using Tor to download apps + + # Lacks Application: + if profile.application_version: + return Result(valid=True) # no warning needed, they have the app already. + + error_msg = "This app is not yet installed. Since it's a Tor profile, do you want to switch to Tor to download it? (Hit 'No' to proceed over clearweb)" + return Result(valid=False, error_type=ResultError.TOR_NEEDED, message=error_msg) + + +@register_action(ResultError.ENDPOINT_VERIFICATION) +def literally_connect( + profile_id: int, + ignore_state_conflict: Optional[bool] = False + ) -> Result: + + # PROFILE + profile_result = get_profile(profile_id) + profile = profile_result.data + + # EXCEPTIONS + ignored = get_ignored_exceptions(ignore_state_conflict=ignore_state_conflict) + + # SCREEN SIZE: + max_resolution = get_max_screensize() + + try: + final_result = ProfileController.enable( + profile, + ignore=ignored, + profile_observer=profile_observer, + application_version_observer=application_version_observer, + connection_observer=connection_observer, + ticket_observer=ticket_observer, + max_resolution=max_resolution + ) + if final_result: + return final_result + else: + return Result(valid=True) + except Exception as error: + return _enable_error(error=error, profile_id=profile_id) + + +def _enable_error(error: Type[Exception], profile_id: int) -> Result: + """Translate core errors into actions and/or messages via Result Objects.""" + logger.error(str(error)) + + #---------------------- + # PURE MESSAGES (NO SOLUTION FUNCTION) + #---------------------- + if error in ERROR_TO_UI_MESSAGE: + error_msg = ERROR_TO_UI_MESSAGE[error] + return Result(valid=False, error_type=ResultError.UNKNOWN, message=error_msg) + + #---------------------- + # ENDPOINT VERIFICATION + #---------------------- + if isinstance(error, EndpointVerificationError): + return Result(valid=False, error_type=ResultError.ENDPOINT_VERIFICATION, message="Operator verification failed. Do you want to continue anyway?") + + #---------------------- + # STATE CONFLICT + #---------------------- + if isinstance(error, ProfileStateConflictError): + function_choice = literally_connect( + profile_id=profile_id, + ignore_state_conflict=True + ) + error_msg = "The profile is already enabled. Do you want to force enable it anyway?" + return Result(valid=False, function_choice=function_choice, error_type=ResultError.STATE_CONFLICT, message=error_msg) + + #---------------------- + # SUBSCRIPTION + #---------------------- + if isinstance(error, (InvalidSubscriptionError, MissingSubscriptionError)): + error_msg = f"Subscription missing or invalid for profile {profile_id}. Do you want to get a new sub?" + return Result(valid=False, error_type=ResultError.SUBSCRIPTION, message=error_msg) + + #---------------------- + # COMMAND NOT FOUND + #---------------------- + if isinstance(error, CommandNotFoundError): + # add install later!! + name_of_package = str(error.subject) + error_msg = f"You're missing the {name_of_package} package. Please install it." + return Result(valid=False, error_type=ResultError.MISSING_SOFTWARE, message=error_msg) + + else: + return Result(valid=False, error_type=ResultError.UNKNOWN, message="An unknown error occured.") + + def _connect_profile(profile_id: int) -> Result: + """Background Threaded Orchestration to Start a Connection""" + # Step 1: Subscription filter: sub_result = filter_subscription(profile_id=profile_id) if not sub_result.valid: return sub_result - # fake connection + # Step 2: Tor filter check: + result = NOT_need_tor_warnings(profile_id=profile_id) + if not result.valid: + return result + + # Actual Connection: logger.info(f"Profile id {profile_id} is ready to connect..") - return Result(valid=True) + return literally_connect(profile_id=profile_id) +@type_checker def connect_profile(profile_id: int, ui_caller: Callable) -> Result: - PRE_REQS = [profile_id, ui_caller] - - confirmed_result = confirm_prereqs(PRE_REQS) - if not confirmed_result.valid: - return confirmed_result - + """Type check before we thread""" take_action( action=_connect_profile, ui_caller=ui_caller, diff --git a/gui/v2/infrastructure/setup_observers.py b/gui/v2/infrastructure/setup_observers.py index 1c6a10f..06191f7 100755 --- a/gui/v2/infrastructure/setup_observers.py +++ b/gui/v2/infrastructure/setup_observers.py @@ -33,6 +33,12 @@ def setup_observers(update_status): profile_observer.subscribe( 'destroyed', lambda event: update_status('Profile destroyed')) + profile_observer.subscribe( + 'enabled', lambda event: update_status('Profile Enabled')) + profile_observer.subscribe( + 'disabled', lambda event: update_status('Profile Disabled')) + + # client_observer.subscribe( # 'synchronizing', lambda event: update_status('Sync in progress...')) diff --git a/gui/v2/ui/pages/menu_page.py b/gui/v2/ui/pages/menu_page.py index 6cc1bfd..432a14d 100755 --- a/gui/v2/ui/pages/menu_page.py +++ b/gui/v2/ui/pages/menu_page.py @@ -23,6 +23,7 @@ from gui.v2.actions.menu_results import MenuResult from gui.v2.ui.pages.Page import Page from gui.v2.ui.pages.location_verification_page import LocationVerificationPage from gui.v2.ui.styles.styles import SCROLLBAR_CYAN_QSS +from gui.v2.ui.styles.css.menu_css import CREATE_BUTTON_STYLE from gui.v2.ui.popups.confirmation_popup import ConfirmationPopup from gui.v2.ui.popups.endpoint_verification_popup import EndpointVerificationPopup from gui.v2.ui.popups.ticket_data_loss_popup import TicketDataLossPopup @@ -230,18 +231,7 @@ class MenuPage(Page): def create_profile_button(self, parent_label, profile_id, key): button = QPushButton(parent_label) button.setGeometry(0, 0, 175, 60) - button.setStyleSheet(""" - QPushButton { - background-color: transparent; - border: none; - } - QPushButton:hover { - background-color: rgba(200, 200, 200, 50); - } - QPushButton:checked { - background-color: rgba(200, 200, 200, 80); - } - """) + button.setStyleSheet(CREATE_BUTTON_STYLE) icon = QIcon(self.button_states.get( profile_id, self.not_connected_profile)) @@ -524,6 +514,14 @@ class MenuPage(Page): def handle_result(self, result: Result): print(f"handle error got result {result}") + + if result.message: + self.update_status.update_status(result.message) + + if result.valid: + self.update_status.enable_marquee + return + if result.error_type == ResultError.NAVIGATE_TO: print(f"Navigating to page {result.navigate_to}") self.custom_window.navigator.navigate(result.navigate_to) @@ -542,13 +540,14 @@ class MenuPage(Page): pre_thread_result = connect_profile(profile_id=self.reverse_id, ui_caller=self.handle_result) if not pre_thread_result.valid: + print(f"Pre-thread errors: {pre_thread_result.message}") return # Temporary until moved to menu coordinator: - print("connecting...") - self._set_task_busy(True) - self.update_status.update_status("Attempting to connect...") - self.worker = enable_profile({"id": self.reverse_id}, self.handle_task_result) + # print("connecting...") + # self._set_task_busy(True) + # self.update_status.update_status("Attempting to connect...") + # self.worker = enable_profile({"id": self.reverse_id}, self.handle_task_result) def disconnect(self): diff --git a/gui/v2/ui/styles/css/menu_css.py b/gui/v2/ui/styles/css/menu_css.py new file mode 100644 index 0000000..dc4d9b8 --- /dev/null +++ b/gui/v2/ui/styles/css/menu_css.py @@ -0,0 +1,14 @@ + + +CREATE_BUTTON_STYLE = """ + QPushButton { + background-color: transparent; + border: none; + } + QPushButton:hover { + background-color: rgba(200, 200, 200, 50); + } + QPushButton:checked { + background-color: rgba(200, 200, 200, 80); + } +"""