diff --git a/core/controllers/ProfileController.py b/core/controllers/ProfileController.py index ffdad9c..9edd4f4 100644 --- a/core/controllers/ProfileController.py +++ b/core/controllers/ProfileController.py @@ -35,13 +35,6 @@ class ProfileController: if profile_observer is not None: profile_observer.notify('created', profile) - @staticmethod - def update(profile: Union[SessionProfile, SystemProfile], profile_observer: ProfileObserver = None): - - profile.save() - - if profile_observer is not None: - profile_observer.notify('updated', profile) @staticmethod def enable(profile: Union[SessionProfile, SystemProfile], ignore: tuple[type[Exception]] = (), pristine: bool = False, asynchronous: bool = False, profile_observer: ProfileObserver = None, application_version_observer: ApplicationVersionObserver = None, connection_observer: ConnectionObserver = None): diff --git a/core/controllers/profile_state/update_profile.py b/core/controllers/profile_state/update_profile.py new file mode 100644 index 0000000..847c1d5 --- /dev/null +++ b/core/controllers/profile_state/update_profile.py @@ -0,0 +1,117 @@ +# utils +from core.errors.logger import logger +from core.models.Result import Result, ResultError + +# JSON Models +from core.models.BaseProfile import BaseProfile as Profile +from core.models.Subscription import Subscription +from core.models.session.SessionProfile import SessionProfile +from core.models.system.SystemProfile import SystemProfile + +# ORM Models +from core.models.orm_models.Location import Location +from core.models.orm_models.Operator import Operator +from core.models.orm_calls.location_calls import get_profile_location_data + +SYSTEMWIDE_CHOICES = ['wireguard', 'hysteria2', 'vless'] +SESSION_CHOICES = ['wireguard', 'tor', 'proxy'] + +""" +Steps: +1) Looks up the profile by id number +2) Filters what values to save, using the ORM for some such as location +3) Saves it with the abstract class' json serialization methods, which use the ORM methods + +Returns: + Result Object + +Called by: + GUI's editor_page + +Raises Errors: + False +""" + +def update_profile( + profile_id: int, + key: str, + new_value: str) -> Result: + + final_data = None + logger.info(f"[UPDATE PROFILE] Recieved profile_id {profile_id}, key {key}, and new_value {new_value}.") + + if not key or not new_value: + return Result(valid=False, data=f"Invalid Inputs of {key} and {new_value}", error_type=ResultError.INVALID_INPUT) + + profile = Profile.find_by_id(profile_id) + if not profile: + error_msg = f"Invalid profile id of {profile_id}" + return Result(valid=False, message=error_msg, error_type=ResultError.INVALID_INPUT) + + # logger.info(f"[UPDATE PROFILE] Found a relevant profile {profile_id}.") + + if key == 'dimentions': + profile.resolution = new_value + + elif key == 'name': + profile.name = new_value + + # ============= CONNECTION TYPE ============= + elif key == 'connection': + if new_value == 'tor': + profile.connection.code = new_value + profile.connection.masked = True + + elif new_value == 'just proxy': + profile.connection.code = 'system' + profile.connection.masked = True + else: + error_msg = 'System wide profiles not supported atm' + return Result(valid=False, message=error_msg, error_type=ResultError.NOT_SUPPORTED) + + # ============= BROWSER ============= + elif key == 'browser': + browser_type, browser_version = new_value.split(':', 1) + profile.application_version.application_code = browser_type + profile.application_version.version_number = browser_version + + elif key == 'protocol': + # ============= SYSTEMWIDE ============= + if profile.connection == 'system-wide': + if new_value in SYSTEMWIDE_CHOICES: + profile.connection.code = new_value + final_data = "edit_session" + else: + error_msg = f"{new_value} is not a systemwide choice" + return Result(valid=False, message=error_msg, error_type=ResultError.NOT_SUPPORTED) + + # ============= SESSION ============= + else: + if new_value in SESSION_CHOICES: + profile.connection.code = new_value + if new_value == 'wireguard': + profile.connection.masked = False + + # ============= LOCATION ============= + elif key == "location": + country_code, city_code = new_value.split("_") + location = get_profile_location_data( # SQLAlchemy + country_code=country_code, + city_code=city_code + ) + # SQLAlchemy foreign key assignment — fills in id, timezone, operator, etc. + profile.location = location + + # Subscription gets wiped on a location change, and is outside ORM + profile.subscription = None + else: + return Result(valid=False, message="Invalid Value to Edit", error_type=ResultError.INVALID_INPUT) + + logger.info("[UPDATE PROFILE] Passing to Profile model to save..") + try: + profile.save() + logger.info("[UPDATE PROFILE] Save worked, passing to the GUI the Result..") + return Result(valid=True, data=final_data) + except: + error_msg = "Profile save failed" + return Result(valid=False, message=error_msg, error_type=ResultError.UNKNOWN) diff --git a/core/models/BaseProfile.py b/core/models/BaseProfile.py index 50dbe89..5dddec0 100644 --- a/core/models/BaseProfile.py +++ b/core/models/BaseProfile.py @@ -41,14 +41,17 @@ def execute_location_sql(country_code: str, city_code: str, session: Session) -> def get_profile_location_data(country_code: str, city_code: str) -> Location: # with get_session() as session: - location_object = execute_location_sql(country_code, city_code) - if location_object.valid: - return location_object.data + api_reply_object = execute_location_sql(country_code, city_code) + if api_reply_object.valid: + data = api_reply_object.data + location_obj = api_reply_object.data + return location_obj else: logger.error(f"[BaseProfile] Got invalid SQL Query which could not be solved by the wrapper, with error message {location_object.message} and type {location_object.error_type}") return None + @dataclass_json @dataclass class BaseProfile(ABC): @@ -88,8 +91,10 @@ class BaseProfile(ABC): def is_system_profile(self): return type(self).__name__ == 'SystemProfile' + def save(self: Self): - config_dict = self.to_dict() # Get dict, not JSON string + config_dict = self.to_dict() + location_dict = self.location.to_dict() # this is from SQLAlchemy, and not JSON-models, that's why it's separate. if self.location: @@ -224,10 +229,10 @@ class BaseProfile(ABC): city_code = profile.location.code # =========== GET DATA USING THAT COUNTRY & LOCATION =========== - location_object = get_profile_location_data(country_code, city_code) + location_dict = get_profile_location_data(country_code, city_code) - if location_object: - profile['location'] = location_object + if location_dict: + profile['location'] = location_dict # this needs error handling if there's no location or malconformed config. diff --git a/core/models/Result.py b/core/models/Result.py new file mode 100644 index 0000000..4246e6a --- /dev/null +++ b/core/models/Result.py @@ -0,0 +1,29 @@ + + +from enum import Enum +from dataclasses import dataclass +from typing import Optional, Any + +class ResultError(Enum): + """Classified error categories.""" + SUCCESS = "success" + NOT_SUPPORTED = "NOT_SUPPORTED" + INVALID_INPUT = "invalid_input" + CONNECTION = "connection" + DATABASE = "database" + UNKNOWN = "unknown" + +@dataclass +class Result: + valid: bool + error_type: ResultError = ResultError.SUCCESS + data: Optional[Any] = None + message: Optional[str] = None + + def user_message(self) -> str: + """Human-readable error for the UI.""" + messages = { + DBErrorType.SUCCESS: "Operation completed successfully.", + DBErrorType.UNKNOWN: f"Error: {self.message}", + } + return messages.get(self.error_type, "Unknown error")