diff --git a/core/infrastructure/action_registry.py b/core/infrastructure/action_registry.py index a527e9d..e1f6b1a 100644 --- a/core/infrastructure/action_registry.py +++ b/core/infrastructure/action_registry.py @@ -1,18 +1,8 @@ from core.models.Result import Result, ResultError from core.controllers.SyncController import new_sync - -def external_logic_handler(navigator, user_action): - if user_action == "go_to_settings": - # Use the signal to request navigation - navigator.request_navigation.emit("settings") - -def test() -> Result: - print("test") - return Result(valid=True) - ENUM_MAP = { - ResultError.UNKNOWN: (test), + ResultError.UNKNOWN: None, ResultError.NEED_SYNC: (new_sync), ResultError.INVALID_API_REPLY: None, ResultError.ENCRYPTION: None, @@ -22,7 +12,3 @@ ENUM_MAP = { # ResultError.BILLING_CODE_EXISTS: (get_invoice) # bypass_existing: True } - -AUTO_NAVIGATE_REGISTRY = { - ResultError.WRONG_LOCATION: "edit_profile" -} diff --git a/core/models/manage/server_reply_tools.py b/core/models/manage/server_reply_tools.py new file mode 100644 index 0000000..9b6a2e0 --- /dev/null +++ b/core/models/manage/server_reply_tools.py @@ -0,0 +1,72 @@ +from core.models.manage.validate_dataclass import validate_model +from core.models.pydantic_models.ServerReply import ServerReply, ServerError +from core.models.Result import Result, ResultError +from core.services.networking.api_requests.ApiResponseModel import ApiResponse +from core.errors.logger import logger + +from typing import Optional + +# Translate Server-side to client-side errors in THIS CONTEXT, +TRANSLATION_DICT = { + ServerError.WRONG_LOCATION: ResultError.WRONG_LOCATION, + ServerError.NODE_ERROR: ResultError.WRONG_LOCATION, + ServerError.BTCPAY: ResultError.SERVER_SIDE, + ServerError.INVALID: ResultError.INVALID_INPUT, + ServerError.MISSING_DATA: ResultError.INVALID_INPUT, + ServerError.UNPAID: ResultError.SUBSCRIPTION, + ServerError.SERVER_SIDE: ResultError.SERVER_SIDE +} + +def format_server_response(api_response: ApiResponse, desired_type: str) -> Result: + """Turn an API response into a Result Object""" + + # if it's in object format, then convert it, + if isinstance(api_response, ApiResponse): + raw_reply = api_response.data + elif isinstance(api_response, dict): + # otherwise take it as is: + raw_reply = api_response + elif isinstance(api_response, str): + # invalid format: + return invalid_server_format(error_msg=api_response) + else: + logger.error("Can't convert server response.") + return invalid_server_format() + + conversion_worked, server_reply = validate_model(ServerReply, raw_reply) + + if not conversion_worked: + logger.error(f"Invalid structure for ServerReply. The original raw reply was {raw_reply}") + return invalid_server_format(error_msg=server_reply) + + # Sanity check: + if not hasattr(server_reply, desired_type): + error_msg = "Developer Error: You're asking for a data type that isn't in the object!" + logger.error(error_msg) + return Result(valid=False, error_type=ResultError.INVALID_INPUT, message=error_msg) + + # Success! Extract and Return Data: + if server_reply.valid: + data = getattr(server_reply, desired_type) + return Result(valid=True, data=data) + + # Failure: + logger.error(f"invalid server reply: {server_reply.error_type}") + logger.error(f"invalid server reply: {server_reply.message}") + error_type = translate_server_failure(server_reply.error_type) + return Result(valid=False, error_type=error_type, message=server_reply.message) + + +def translate_server_failure(server_reply: ServerReply) -> Result: + """Translate the server's failure to the UI""" + logger.error(f"Server-side failure occured. The server claims the reason is of type {server_reply.error_type} with {server_reply.message}") + client_error = TRANSLATION_DICT[server_reply.error_type] + return Result(valid=False, error_type=client_error, message=server_reply.message) + +def invalid_server_format(error_msg: Optional[str] = None) -> Result: + if error_msg: + error_msg = f"The server gave an invalid response format {error_msg}." + else: + error_msg = "The server gave an invalid response format." + return Result(valid=False, error_type=ResultError.INVALID_API_REPLY, message=error_msg) + diff --git a/core/models/manage/validate_dataclass.py b/core/models/manage/validate_dataclass.py new file mode 100644 index 0000000..88ef38c --- /dev/null +++ b/core/models/manage/validate_dataclass.py @@ -0,0 +1,55 @@ +from core.errors.logger import logger +from dataclasses import dataclass, fields +from dataclasses_json import dataclass_json +from typing import Any, Type, TypeVar +from datetime import datetime, date, time +import json + +def validate_field_type(field_name: str, value: Any, expected_type: Type): + """Reusable validator to check if a value matches the expected type.""" + if not isinstance(value, expected_type): + raise TypeError( + f"Field '{field_name}' expected type {expected_type.__name__}, " + f"but got {type(value).__name__}." + ) + +def validate_types(cls): + """Wrapper / Class decorator that injects type validation into __post_init__.""" + original_post_init = getattr(cls, "__post_init__", None) + + def __post_init__(self): + for field in fields(self): + value = getattr(self, field.name) + validate_field_type(field.name, value, field.type) + + if original_post_init: + original_post_init(self) + + cls.__post_init__ = __post_init__ + return cls + + +# TypeVar so that the editor knows the function returns an instance of whatever model_type is passed in. +T = TypeVar('T') + +def validate_model(model_type: Type[T], data: dict|str) -> tuple: + """Utility Conversion tool for the wrapper, to be called to convert.""" + try: + if isinstance(data, dict): + final_result = model_type.from_dict(data) + elif isinstance(data, str): + final_result = model_type.from_json(data) + else: + error_msg = f"Invalid data format of {type(data)}" + logger.error(error_msg) + final_result = None + return False, error_msg + + return True, final_result + except (TypeError, ValueError) as e: + # TypeError comes from the @validate_types decorator + # ValueError can come from dataclasses-json if the JSON is malformed + error_msg = f"Validation Error: {e}" + logger.error(error_msg) + return False, error_msg + diff --git a/core/models/pydantic_models/ServerReply.py b/core/models/pydantic_models/ServerReply.py index ab72326..cb41fef 100644 --- a/core/models/pydantic_models/ServerReply.py +++ b/core/models/pydantic_models/ServerReply.py @@ -18,8 +18,10 @@ class ServerError(str, Enum): INVALID = "invalid" MISSING_DATA = "missing_data" WRONG_LOCATION = "wrong_location" - NODE_DOWN = "node_down" + NODE_ERROR = "node_error" BTCPAY = "btcpay" + SERVER_SIDE = "server_side" + @dataclass_json @validate_types diff --git a/core/services/keys_and_verifications/wireguard_keys.py b/core/services/keys_and_verifications/wireguard_keys.py index ac1e280..f5e29c6 100644 --- a/core/services/keys_and_verifications/wireguard_keys.py +++ b/core/services/keys_and_verifications/wireguard_keys.py @@ -1,13 +1,16 @@ from core.services.networking.httpx import connect -from core.services.networking.api_requests.ApiResponseModel import ApiResponse, ErrorType +from core.services.networking.api_requests.ApiResponseModel import ErrorType from core.models.session.SessionProfile import SessionProfile from core.models.system.SystemProfile import SystemProfile from core.Errors import MissingSubscriptionError, MissingLocationError, InvalidSubscriptionError -from core.services.WebServiceApiService import WebServiceApiService -from core.controllers.ConnectionController import ConnectionController +# from core.services.WebServiceApiService import WebServiceApiService +# from core.controllers.ConnectionController import ConnectionController from core.observers.ConnectionObserver import ConnectionObserver from core.Constants import Constants from core.errors.logger import logger +from core.models.manage.server_reply_tools import format_server_response +# from core.models.pydantic_models.ServerReply import ServerReply, ServerError +from core.models.Result import Result, ResultError from typing import Union, Optional import base64 @@ -16,7 +19,7 @@ import re def register_wireguard_session( profile: Union[SessionProfile, SystemProfile], connection_observer: Optional[ConnectionObserver] = None -): +) -> Result: """Register a WireGuard session for the given profile.""" if not profile.has_subscription(): @@ -58,12 +61,29 @@ def register_wireguard_session( raise ConnectionError(error_msg) wireguard_configuration = api_result.data + print(f"wireguard_configuration type is {type(wireguard_configuration)}") + # print(f"wireguard_configuration value is {wireguard_configuration}") + if isinstance(wireguard_configuration, dict): + logger.error("Server isn't returning a WG config, it's an error.") + return handle_config_error(wireguard_configuration) + if wireguard_configuration is None: raise InvalidSubscriptionError() wireguard_configuration = _inject_private_key(wireguard_configuration, wireguard_keys.get('private')) profile.attach_wireguard_configuration(wireguard_configuration) + return Result(valid=True) + + + +def handle_config_error(wireguard_configuration: str|dict) -> Result: + """The server gave the wireguard config in an invalid format. First convert it to get the error.""" + server_reply = format_server_response(wireguard_configuration, "message") + if server_reply.valid: + return server_reply + else: + return Result(valid=False, error_type=ResultError.NODE_ERROR, message="server-side error with wireguard config format.") def _generate_wireguard_keys() -> dict: diff --git a/core/services/subscriptions/subscriptions_by_protocol.py b/core/services/subscriptions/subscriptions_by_protocol.py index 6417cc4..524a0db 100644 --- a/core/services/subscriptions/subscriptions_by_protocol.py +++ b/core/services/subscriptions/subscriptions_by_protocol.py @@ -113,7 +113,7 @@ def create_invoice( # Translate Server-side to client-side errors in THIS CONTEXT, TRANSLATION_DICT = { ServerError.WRONG_LOCATION: ResultError.WRONG_LOCATION, - ServerError.NODE_DOWN: ResultError.WRONG_LOCATION, + ServerError.NODE_ERROR: ResultError.WRONG_LOCATION, ServerError.BTCPAY: ResultError.SERVER_SIDE, ServerError.INVALID: ResultError.INVALID_INPUT, ServerError.MISSING_DATA: ResultError.INVALID_INPUT,