forked from Support/sp-hydra-veil-gui
Menu Coordinator now capable of enabling profiles with moderate error handling
This commit is contained in:
parent
1308b025c2
commit
e21905c101
4 changed files with 221 additions and 35 deletions
|
|
@ -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.services.subscriptions.subscriptions_by_protocol import get_profile_subscription, start_new_payment_cycle
|
||||||
from core.models.enum_models import PaymentCategory
|
from core.models.enum_models import PaymentCategory
|
||||||
|
from core.utils.type_checker import type_checker
|
||||||
from core.models.Result import Result, ResultError
|
from core.models.Result import Result, ResultError
|
||||||
from core.errors.logger import logger
|
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.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
|
# 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:
|
def get_profile(profile_id: int):
|
||||||
for each_prereq in PRE_REQS:
|
profile = ProfileController.get(int(profile_id))
|
||||||
if allow_false:
|
if not profile:
|
||||||
if each_prereq == None:
|
return Result(valid=False, error_type=ResultError.INVALID_INPUT, message=f"There is no profile with the id {profile_id}")
|
||||||
return Result(valid=False, error_type=ResultError.INVALID_INPUT, message=f"You are missing {each_prereq}")
|
|
||||||
else:
|
else:
|
||||||
if not each_prereq:
|
return Result(valid=True, data=profile)
|
||||||
return Result(valid=False, error_type=ResultError.INVALID_INPUT, message=f"You are missing {each_prereq}")
|
|
||||||
return Result(valid=True)
|
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:
|
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:
|
if not subscription or not subscription.billing_code:
|
||||||
# auto check tickets would be here.
|
# 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:
|
if subscription.billing_pending:
|
||||||
start_result = start_new_payment_cycle(category=PaymentCategory.INDIVIDUAL, profile_id=profile_id)
|
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)
|
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:
|
def _connect_profile(profile_id: int) -> Result:
|
||||||
|
"""Background Threaded Orchestration to Start a Connection"""
|
||||||
|
|
||||||
# Step 1: Subscription filter:
|
# Step 1: Subscription filter:
|
||||||
sub_result = filter_subscription(profile_id=profile_id)
|
sub_result = filter_subscription(profile_id=profile_id)
|
||||||
if not sub_result.valid:
|
if not sub_result.valid:
|
||||||
return sub_result
|
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..")
|
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:
|
def connect_profile(profile_id: int, ui_caller: Callable) -> Result:
|
||||||
PRE_REQS = [profile_id, ui_caller]
|
"""Type check before we thread"""
|
||||||
|
|
||||||
confirmed_result = confirm_prereqs(PRE_REQS)
|
|
||||||
if not confirmed_result.valid:
|
|
||||||
return confirmed_result
|
|
||||||
|
|
||||||
take_action(
|
take_action(
|
||||||
action=_connect_profile,
|
action=_connect_profile,
|
||||||
ui_caller=ui_caller,
|
ui_caller=ui_caller,
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,12 @@ def setup_observers(update_status):
|
||||||
profile_observer.subscribe(
|
profile_observer.subscribe(
|
||||||
'destroyed', lambda event: update_status('Profile destroyed'))
|
'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(
|
# client_observer.subscribe(
|
||||||
# 'synchronizing', lambda event: update_status('Sync in progress...'))
|
# 'synchronizing', lambda event: update_status('Sync in progress...'))
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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.Page import Page
|
||||||
from gui.v2.ui.pages.location_verification_page import LocationVerificationPage
|
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.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.confirmation_popup import ConfirmationPopup
|
||||||
from gui.v2.ui.popups.endpoint_verification_popup import EndpointVerificationPopup
|
from gui.v2.ui.popups.endpoint_verification_popup import EndpointVerificationPopup
|
||||||
from gui.v2.ui.popups.ticket_data_loss_popup import TicketDataLossPopup
|
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):
|
def create_profile_button(self, parent_label, profile_id, key):
|
||||||
button = QPushButton(parent_label)
|
button = QPushButton(parent_label)
|
||||||
button.setGeometry(0, 0, 175, 60)
|
button.setGeometry(0, 0, 175, 60)
|
||||||
button.setStyleSheet("""
|
button.setStyleSheet(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);
|
|
||||||
}
|
|
||||||
""")
|
|
||||||
|
|
||||||
icon = QIcon(self.button_states.get(
|
icon = QIcon(self.button_states.get(
|
||||||
profile_id, self.not_connected_profile))
|
profile_id, self.not_connected_profile))
|
||||||
|
|
@ -524,6 +514,14 @@ class MenuPage(Page):
|
||||||
|
|
||||||
def handle_result(self, result: Result):
|
def handle_result(self, result: Result):
|
||||||
print(f"handle error got 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:
|
if result.error_type == ResultError.NAVIGATE_TO:
|
||||||
print(f"Navigating to page {result.navigate_to}")
|
print(f"Navigating to page {result.navigate_to}")
|
||||||
self.custom_window.navigator.navigate(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)
|
pre_thread_result = connect_profile(profile_id=self.reverse_id, ui_caller=self.handle_result)
|
||||||
|
|
||||||
if not pre_thread_result.valid:
|
if not pre_thread_result.valid:
|
||||||
|
print(f"Pre-thread errors: {pre_thread_result.message}")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Temporary until moved to menu coordinator:
|
# Temporary until moved to menu coordinator:
|
||||||
print("connecting...")
|
# print("connecting...")
|
||||||
self._set_task_busy(True)
|
# self._set_task_busy(True)
|
||||||
self.update_status.update_status("Attempting to connect...")
|
# self.update_status.update_status("Attempting to connect...")
|
||||||
self.worker = enable_profile({"id": self.reverse_id}, self.handle_task_result)
|
# self.worker = enable_profile({"id": self.reverse_id}, self.handle_task_result)
|
||||||
|
|
||||||
|
|
||||||
def disconnect(self):
|
def disconnect(self):
|
||||||
|
|
|
||||||
14
gui/v2/ui/styles/css/menu_css.py
Normal file
14
gui/v2/ui/styles/css/menu_css.py
Normal file
|
|
@ -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);
|
||||||
|
}
|
||||||
|
"""
|
||||||
Loading…
Reference in a new issue