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

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

View file

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

View file

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