ServerReply model initiated client-side, and used for wireguard config replies.
This commit is contained in:
parent
77c7123d73
commit
6982f46c37
6 changed files with 156 additions and 21 deletions
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
72
core/models/manage/server_reply_tools.py
Normal file
72
core/models/manage/server_reply_tools.py
Normal file
|
|
@ -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)
|
||||
|
||||
55
core/models/manage/validate_dataclass.py
Normal file
55
core/models/manage/validate_dataclass.py
Normal file
|
|
@ -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
|
||||
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in a new issue