ServerReply model initiated client-side, and used for wireguard config replies.

This commit is contained in:
SimplifiedPrivacy 2026-09-30 18:50:19 -04:00
parent 77c7123d73
commit 6982f46c37
6 changed files with 156 additions and 21 deletions

View file

@ -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"
}

View 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)

View 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

View file

@ -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

View file

@ -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:

View file

@ -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,