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.models.Result import Result, ResultError
|
||||||
from core.controllers.SyncController import new_sync
|
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 = {
|
ENUM_MAP = {
|
||||||
ResultError.UNKNOWN: (test),
|
ResultError.UNKNOWN: None,
|
||||||
ResultError.NEED_SYNC: (new_sync),
|
ResultError.NEED_SYNC: (new_sync),
|
||||||
ResultError.INVALID_API_REPLY: None,
|
ResultError.INVALID_API_REPLY: None,
|
||||||
ResultError.ENCRYPTION: None,
|
ResultError.ENCRYPTION: None,
|
||||||
|
|
@ -22,7 +12,3 @@ ENUM_MAP = {
|
||||||
# ResultError.BILLING_CODE_EXISTS: (get_invoice) # bypass_existing: True
|
# 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"
|
INVALID = "invalid"
|
||||||
MISSING_DATA = "missing_data"
|
MISSING_DATA = "missing_data"
|
||||||
WRONG_LOCATION = "wrong_location"
|
WRONG_LOCATION = "wrong_location"
|
||||||
NODE_DOWN = "node_down"
|
NODE_ERROR = "node_error"
|
||||||
BTCPAY = "btcpay"
|
BTCPAY = "btcpay"
|
||||||
|
SERVER_SIDE = "server_side"
|
||||||
|
|
||||||
|
|
||||||
@dataclass_json
|
@dataclass_json
|
||||||
@validate_types
|
@validate_types
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,16 @@
|
||||||
from core.services.networking.httpx import connect
|
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.session.SessionProfile import SessionProfile
|
||||||
from core.models.system.SystemProfile import SystemProfile
|
from core.models.system.SystemProfile import SystemProfile
|
||||||
from core.Errors import MissingSubscriptionError, MissingLocationError, InvalidSubscriptionError
|
from core.Errors import MissingSubscriptionError, MissingLocationError, InvalidSubscriptionError
|
||||||
from core.services.WebServiceApiService import WebServiceApiService
|
# from core.services.WebServiceApiService import WebServiceApiService
|
||||||
from core.controllers.ConnectionController import ConnectionController
|
# from core.controllers.ConnectionController import ConnectionController
|
||||||
from core.observers.ConnectionObserver import ConnectionObserver
|
from core.observers.ConnectionObserver import ConnectionObserver
|
||||||
from core.Constants import Constants
|
from core.Constants import Constants
|
||||||
from core.errors.logger import logger
|
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
|
from typing import Union, Optional
|
||||||
import base64
|
import base64
|
||||||
|
|
@ -16,7 +19,7 @@ import re
|
||||||
def register_wireguard_session(
|
def register_wireguard_session(
|
||||||
profile: Union[SessionProfile, SystemProfile],
|
profile: Union[SessionProfile, SystemProfile],
|
||||||
connection_observer: Optional[ConnectionObserver] = None
|
connection_observer: Optional[ConnectionObserver] = None
|
||||||
):
|
) -> Result:
|
||||||
"""Register a WireGuard session for the given profile."""
|
"""Register a WireGuard session for the given profile."""
|
||||||
|
|
||||||
if not profile.has_subscription():
|
if not profile.has_subscription():
|
||||||
|
|
@ -58,12 +61,29 @@ def register_wireguard_session(
|
||||||
raise ConnectionError(error_msg)
|
raise ConnectionError(error_msg)
|
||||||
|
|
||||||
wireguard_configuration = api_result.data
|
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:
|
if wireguard_configuration is None:
|
||||||
raise InvalidSubscriptionError()
|
raise InvalidSubscriptionError()
|
||||||
|
|
||||||
wireguard_configuration = _inject_private_key(wireguard_configuration, wireguard_keys.get('private'))
|
wireguard_configuration = _inject_private_key(wireguard_configuration, wireguard_keys.get('private'))
|
||||||
profile.attach_wireguard_configuration(wireguard_configuration)
|
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:
|
def _generate_wireguard_keys() -> dict:
|
||||||
|
|
|
||||||
|
|
@ -113,7 +113,7 @@ def create_invoice(
|
||||||
# Translate Server-side to client-side errors in THIS CONTEXT,
|
# Translate Server-side to client-side errors in THIS CONTEXT,
|
||||||
TRANSLATION_DICT = {
|
TRANSLATION_DICT = {
|
||||||
ServerError.WRONG_LOCATION: ResultError.WRONG_LOCATION,
|
ServerError.WRONG_LOCATION: ResultError.WRONG_LOCATION,
|
||||||
ServerError.NODE_DOWN: ResultError.WRONG_LOCATION,
|
ServerError.NODE_ERROR: ResultError.WRONG_LOCATION,
|
||||||
ServerError.BTCPAY: ResultError.SERVER_SIDE,
|
ServerError.BTCPAY: ResultError.SERVER_SIDE,
|
||||||
ServerError.INVALID: ResultError.INVALID_INPUT,
|
ServerError.INVALID: ResultError.INVALID_INPUT,
|
||||||
ServerError.MISSING_DATA: ResultError.INVALID_INPUT,
|
ServerError.MISSING_DATA: ResultError.INVALID_INPUT,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue