Ticket Refactor to accept ServerReply objects, and keep the internal flow returning Result objects

This commit is contained in:
SimplifiedPrivacy 2026-10-22 03:31:33 -04:00
parent 2208cedb7e
commit af368db8b7
8 changed files with 642 additions and 342 deletions

View file

@ -5,13 +5,14 @@ if TYPE_CHECKING:
from core.essentials.observers.ConnectionObserver import ConnectionObserver from core.essentials.observers.ConnectionObserver import ConnectionObserver
from core.observers.TicketObserver import TicketObserver from core.observers.TicketObserver import TicketObserver
from core.utils.type_checker import type_checker
from core.services.prepare_tickets.ticket_prep_orchestrator import ticket_prep_orchestrator from core.services.prepare_tickets.ticket_prep_orchestrator import ticket_prep_orchestrator
from core.services.prepare_tickets.make_sure_pub_key_exists import make_sure_pub_key_exists from core.services.prepare_tickets.make_sure_pub_key_exists import make_sure_pub_key_exists
from core.services.prepare_tickets import ticket_tracker from core.services.prepare_tickets import ticket_tracker
from core.services.helpers.get_how_many_profiles_were_ordered import ( from core.services.helpers.get_how_many_profiles_were_ordered import (
get_how_many_profiles_were_ordered, get_how_many_profiles_were_ordered,
) )
from core.observers.BaseObserver import BaseObserver # from core.observers.BaseObserver import BaseObserver
from core.models.Result import Result, ResultError from core.models.Result import Result, ResultError
from core.errors.logger import logger from core.errors.logger import logger
@ -33,13 +34,15 @@ If it doesn't have a config file, then it uses the temp billing id to get the pu
""" """
# Bulk Prep: # Bulk Prep:
@type_checker
def prepare_tickets( def prepare_tickets(
how_many_profiles: int, how_many_profiles: int,
ticket_observer: TicketObserver, ticket_observer: TicketObserver,
connection_observer: ConnectionObserver, connection_observer: ConnectionObserver,
which_ticket: Optional[int] = None, which_ticket: Optional[int] = None,
billing_code: Optional[int] = None billing_code: Optional[int] = None
) -> dict: ) -> Result:
# these two are for preparation of a single ticket to a profile. # these two are for preparation of a single ticket to a profile.
# can't have one blank without the other. (profile or ticket) # can't have one blank without the other. (profile or ticket)
@ -82,35 +85,48 @@ def prepare_tickets(
billing_code=billing_code # only relevant for single ticket prep billing_code=billing_code # only relevant for single ticket prep
) )
# rest of this function is evaluating the results: if prep_results.valid:
notification = "Done! All Tickets Ready!"
if prep_results == False or prep_results == None: if ticket_observer is not None:
return {"valid": False, "message": "error"} ticket_observer.notify("preparing", subject=notification)
else:
if "valid" not in prep_results: notification = "Ticket Preparation Failed"
return {"valid": False, "message": "error"}
if prep_results["valid"] == True:
notification = f"Done! All Tickets Ready!"
if ticket_observer is not None: if ticket_observer is not None:
ticket_observer.notify("preparing", subject=notification) ticket_observer.notify("preparing", subject=notification)
return prep_results
if "how_many_failed" in prep_results:
how_many_failed = prep_results.get("how_many_failed", 0)
failed_validations = prep_results.get("failed_validations", None)
if failed_validations:
notification = f"Error with Ticket Preparation or Verification!"
if ticket_observer is not None:
ticket_observer.notify("preparing", subject=notification)
return prep_results
notification = f"Error with Ticket Preparation or Verification!"
if ticket_observer is not None:
ticket_observer.notify("preparing", subject=notification)
return prep_results return prep_results
# if prep_results == False or prep_results == None:
# return {"valid": False, "message": "error"}
# if "valid" not in prep_results:
# return {"valid": False, "message": "error"}
# if prep_results["valid"] == True:
# notification = f"Done! All Tickets Ready!"
# if ticket_observer is not None:
# ticket_observer.notify("preparing", subject=notification)
# return prep_results
# if "how_many_failed" in prep_results:
# how_many_failed = prep_results.get("how_many_failed", 0)
# failed_validations = prep_results.get("failed_validations", None)
# if failed_validations:
# notification = f"Error with Ticket Preparation or Verification!"
# if ticket_observer is not None:
# ticket_observer.notify("preparing", subject=notification)
# return prep_results
# notification = f"Error with Ticket Preparation or Verification!"
# if ticket_observer is not None:
# ticket_observer.notify("preparing", subject=notification)
# return prep_results
# No profile required # No profile required
def respawn_billing_code_into_ticket( def respawn_billing_code_into_ticket(
billing_code: str, billing_code: str,
@ -167,7 +183,7 @@ def respawn_billing_code_into_ticket(
valid = ticket_result.get('valid', False) valid = ticket_result.get('valid', False)
if not valid: if not valid:
notification = f"Error in Respawn Prep!" notification = "Error in Respawn Prep!"
if ticket_observer is not None: if ticket_observer is not None:
ticket_observer.notify("preparing", subject=notification) ticket_observer.notify("preparing", subject=notification)
return prep_error_result(ticket_result) return prep_error_result(ticket_result)

View file

@ -18,7 +18,7 @@ class ApiCall(Enum):
TRANSLATION_DICT = { TRANSLATION_DICT = {
ServerError.WRONG_LOCATION: ResultError.WRONG_LOCATION, ServerError.WRONG_LOCATION: ResultError.WRONG_LOCATION,
ServerError.NODE_ERROR: ResultError.WRONG_LOCATION, ServerError.NODE_ERROR: ResultError.WRONG_LOCATION,
ServerError.BTCPAY: ResultError.SERVER_SIDE, ServerError.CRYPTO_NODE: 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,
ServerError.UNPAID: ResultError.SUBSCRIPTION, ServerError.UNPAID: ResultError.SUBSCRIPTION,
@ -71,7 +71,6 @@ def format_server_response(api_response: ApiResponse|dict, desired_type: str) ->
return Result(valid=False, error_type=ResultError.MISSING_DATA, message="Server Replied it was valid, but failed to provide the data.") return Result(valid=False, error_type=ResultError.MISSING_DATA, message="Server Replied it was valid, but failed to provide the data.")
# return Result(valid=False, error_type=error_type, message=server_reply.message)
def translate_server_failure(server_reply: ServerReply) -> Result: def translate_server_failure(server_reply: ServerReply) -> Result:
@ -89,12 +88,13 @@ def invalid_server_format(error_msg: Optional[str] = None) -> Result:
def api_call_and_extraction( def api_call_and_extraction(
desired_type: str, desired_type: Optional[str],
method: ApiCall, method: ApiCall,
url: str, url: str,
connection_observer: ConnectionObserver, connection_observer: ConnectionObserver,
payload: dict, payload: dict,
billing_code: Optional[str] = None billing_code: Optional[str] = None,
return_server_reply: bool = False,
) -> Result: ) -> Result:
"""Do an API call, extract the data, and if there's an error, then translate the server's reply to a Result Object""" """Do an API call, extract the data, and if there's an error, then translate the server's reply to a Result Object"""
@ -152,6 +152,11 @@ def api_call_and_extraction(
logger.error(error_msg) logger.error(error_msg)
return invalid_server_format() return invalid_server_format()
# Opt into getting the ServerReply object (instead of Result object):
if return_server_reply and desired_type is None:
logger.info("Validation successfull. Returning a raw ServerReply object")
return server_reply
logger.info("Validation successfull. Checking if the server's reply says the operation worked..") logger.info("Validation successfull. Checking if the server's reply says the operation worked..")
if not server_reply.valid: if not server_reply.valid:
error_message = f"Server reply was correctly structured, but the actual invoice failed to be created. The raw data is {server_reply}" error_message = f"Server reply was correctly structured, but the actual invoice failed to be created. The raw data is {server_reply}"
@ -169,12 +174,3 @@ def api_call_and_extraction(
data = getattr(server_reply, desired_type) data = getattr(server_reply, desired_type)
logger.info(f"Successfully returning the data of {data}") logger.info(f"Successfully returning the data of {data}")
return Result(valid=True, data=data) return Result(valid=True, data=data)
# else:
# error_msg = f"Server Replied it was valid, but failed to provide the data for the desired type of {desired_type}."
# logger.error(error_msg)
# return Result(valid=False, error_type=ResultError.MISSING_DATA, message=error_msg)

View file

@ -16,10 +16,11 @@ class ServerError(str, Enum):
QUANTITY = "quantity" QUANTITY = "quantity"
UNPAID = "unpaid" UNPAID = "unpaid"
INVALID = "invalid" INVALID = "invalid"
ALREADY_USED = "already_used"
MISSING_DATA = "missing_data" MISSING_DATA = "missing_data"
WRONG_LOCATION = "wrong_location" WRONG_LOCATION = "wrong_location"
NODE_ERROR = "node_error" NODE_ERROR = "node_error"
BTCPAY = "btcpay" CRYPTO_NODE = "crypto_node"
SERVER_SIDE = "server_side" SERVER_SIDE = "server_side"
@ -37,6 +38,8 @@ class ServerReply():
boolean_result: Optional[bool] = None boolean_result: Optional[bool] = None
conf: Optional[str] = None conf: Optional[str] = None
payment_status: Optional[bool] = None payment_status: Optional[bool] = None
expires_at: Optional[datetime] = None
@dataclass_json @dataclass_json

View file

@ -4,22 +4,25 @@ from typing import TYPE_CHECKING
if TYPE_CHECKING: if TYPE_CHECKING:
from essentials.observers.ConnectionObserver import ConnectionObserver from essentials.observers.ConnectionObserver import ConnectionObserver
from core.observers.TicketObserver import TicketObserver from core.observers.TicketObserver import TicketObserver
# services # services
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 ApiResponse, ErrorType
from core.services.networking.make_url import make_url from core.services.networking.make_url import make_url
# from core.services.payment_phase.ticket_config_tools import get_which_billing_key # from core.services.payment_phase.ticket_config_tools import get_which_billing_key
# models & their controllers # models & their controllers
from core.models.session.SessionProfile import SessionProfile from core.models.manage.server_reply_tools import api_call_and_extraction, ApiCall, translate_server_failure
from core.models.system.SystemProfile import SystemProfile from core.models.pydantic_models.ServerReply import ServerReply, ServerError
from core.models.Subscription import Subscription # not sure if needed # from core.models.session.SessionProfile import SessionProfile
# from core.models.system.SystemProfile import SystemProfile
# utils # utils
from core.utils.basic_operations.write_or_read_from_json import get_value_from_json_file from core.utils.basic_operations.write_or_read_from_json import get_value_from_json_file
# constants & errors # constants & errors
from core.Constants import Constants from core.Constants import Constants
from core.errors.exceptions import InvalidData, ServerSideError # from core.errors.exceptions import InvalidData, ServerSideError
from core.models.Result import Result, ResultError
from core.errors.logger import logger from core.errors.logger import logger
from typing import Optional from typing import Optional
@ -76,12 +79,24 @@ def prep_single_ticket_payload(billing_code: str, list_of_all_blinded_data: list
return payload return payload
def confirmed_data_result(blind_signatures: list) -> Result:
if (
blind_signatures == "error"
or blind_signatures == None
or blind_signatures == False
):
error_msg = f"The server's blind signature was blank or invalid. It said: {blind_signatures}"
return Result(valid=False, error_type=ResultError.INVALID_INPUT, message=error_msg)
return Result(valid=True, data=blind_signatures)
def send_blind_commitments( def send_blind_commitments(
list_of_all_blinded_data: list, list_of_all_blinded_data: list,
ticket_observer: TicketObserver, ticket_observer: TicketObserver,
connection_observer: ConnectionObserver, connection_observer: ConnectionObserver,
billing_code: Optional[int] = None billing_code: Optional[int] = None
) -> list | None: ) -> Result:
""" """
Purpose: Purpose:
1) Oversee payload prep 1) Oversee payload prep
@ -105,61 +120,121 @@ def send_blind_commitments(
# send it: # send it:
url = make_url(which_endpoint) url = make_url(which_endpoint)
try:
api_reply_object = connect.single_endpoint(
method="post",
url=url,
observer=connection_observer,
payload=payload
)
# # did API call work?
if not api_reply_object.valid:
logger.error(f"[SEND BLIND COMMITMENTS] Second Post request failed. {api_reply_object.error_type} {api_reply_object.message}")
ticket_observer.notify("error", subject=api_reply_object.message)
return None
# assuming it worked, extract the data: server_reply = api_call_and_extraction(
reply = api_reply_object.data desired_type=None, # get it all
method=ApiCall.POST,
url=url,
connection_observer=connection_observer,
payload=payload,
return_server_reply=True # get it all
)
# is it in valid format? # this is a connection error or invalid format itself:
if not isinstance(reply, dict): if isinstance(server_reply, Result) or not isinstance(server_reply, ServerReply):
final_error_msg = f"API returned an invalid format {reply}" ticket_observer.notify("error", subject=server_reply.message)
logger.error(final_error_msg) return server_reply
ticket_observer.notify("error", subject=final_error_msg)
return None
if reply.get("valid") == True: # Worked:
# this 'signed_data' variable is a list, if server_reply.valid:
signed_data = reply.get("signed_data") return confirmed_data_result(blind_signatures=server_reply.signed_data)
# it's then sending it to the other functions,
# to iterate through, when they save and unblind,
return signed_data
# but what if it didn't work? error_type = server_reply.error_type
elif reply.get("valid") == False: message = server_reply.message
error_msg = reply.get("message") ticket_observer.notify("error", subject=message)
error_msg_with_context = (
f"Issues with Signing. Server's error message said: {error_msg}" logger.error(f"Send blind commitments failed. {str(error_type)} {message}")
# WRONG QUANTITY:
if error_type == ServerError.QUANTITY:
quantity_required = server_reply.quantity
if not quantity_required or quantity_required < 1:
error_msg = f"Server claims it got an invalid quantity, but yet it suggested to give it just {quantity_required} items."
ticket_observer.notify("error", subject=error_msg)
return Result(
valid=False,
error_type=ResultError.INVALID_INPUT,
message=error_msg
) )
ticket_observer.notify("error", subject=error_msg_with_context)
return None
if reply == False: logger.info(f"Retrying with a different size of {quantity_required}.")
error_msg = f"It did not send. There's an error with sending the data to server. The payload either was or was going to be: {payload}" resized_list = list_of_all_blinded_data[:quantity_required]
ticket_observer.notify("error", subject=error_msg) resized_payload = prep_payload_for_bulk(resized_list)
return None second_server_reply = api_call_and_extraction(
else: desired_type="signed_data",
error_msg = "The JSON returned from the server is not formatted correctly to even be able to figure why it did not work. This is a critical error. Contact customer support" method=ApiCall.POST,
ticket_observer.notify("error", subject=error_msg) url=url,
return None connection_observer=connection_observer,
payload=resized_payload,
return_server_reply=False
)
if not second_server_reply.valid:
return second_server_reply
return confirmed_data_result(blind_signatures=server_reply.signed_data)
except InvalidData as e:
logger.error(f"InvalidData error: {e}", exc_info=True) return translate_server_failure(server_reply=server_reply)
return None
except ServerSideError as e:
logger.error(f"ServerSideError: {e}", exc_info=True)
return None
except Exception as e:
logger.error(f"Generic Python Error {e}", exc_info=True) # # # did API call work?
return None # if not api_reply_object.valid:
# ticket_observer.notify("error", subject=api_reply_object.message)
# return None
# # assuming it worked, extract the data:
# reply = api_reply_object.data
# # is it in valid format?
# if not isinstance(reply, dict):
# final_error_msg = f"API returned an invalid format {reply}"
# logger.error(final_error_msg)
# ticket_observer.notify("error", subject=final_error_msg)
# return None
# if reply.get("valid") == True:
# # this 'signed_data' variable is a list,
# signed_data = reply.get("signed_data")
# # it's then sending it to the other functions,
# # to iterate through, when they save and unblind,
# return signed_data
# # but what if it didn't work?
# elif reply.get("valid") == False:
# error_msg = reply.get("message")
# error_msg_with_context = (
# f"Issues with Signing. Server's error message said: {error_msg}"
# )
# ticket_observer.notify("error", subject=error_msg_with_context)
# return None
# if reply == False:
# error_msg = f"It did not send. There's an error with sending the data to server. The payload either was or was going to be: {payload}"
# ticket_observer.notify("error", subject=error_msg)
# return None
# else:
# error_msg = "The JSON returned from the server is not formatted correctly to even be able to figure why it did not work. This is a critical error. Contact customer support"
# ticket_observer.notify("error", subject=error_msg)
# return None
# except InvalidData as e:
# logger.error(f"InvalidData error: {e}", exc_info=True)
# return None
# except ServerSideError as e:
# logger.error(f"ServerSideError: {e}", exc_info=True)
# return None
# except Exception as e:
# logger.error(f"Generic Python Error {e}", exc_info=True)
# return None
# api_reply_object = connect.single_endpoint(
# method="post",
# url=url,
# observer=connection_observer,
# payload=payload
# )

View file

@ -4,20 +4,20 @@ from typing import TYPE_CHECKING
if TYPE_CHECKING: if TYPE_CHECKING:
from core.observers.TicketObserver import TicketObserver from core.observers.TicketObserver import TicketObserver
from essentials.observers.ConnectionObserver import ConnectionObserver from essentials.observers.ConnectionObserver import ConnectionObserver
# services
from core.services.crypto.make_commitments import make_ALL_commitments from core.services.crypto.make_commitments import make_ALL_commitments
from core.services.prepare_tickets.unblind_all_tickets import unblind_ALL_tickets from core.services.prepare_tickets.unblind_all_tickets import unblind_ALL_tickets
from core.services.prepare_tickets.setup_ticket_tracker import setup_ticket_tracker from core.services.prepare_tickets.setup_ticket_tracker import setup_ticket_tracker
from core.services.prepare_tickets import ticket_tracker # from core.services.prepare_tickets import ticket_tracker
from core.services.prepare_tickets.send_blind_commitments import send_blind_commitments from core.services.prepare_tickets.send_blind_commitments import send_blind_commitments
from core.services.prepare_tickets.validate_blind_signatures import validate_blind_signatures from core.services.prepare_tickets.validate_blind_signatures import validate_blind_signatures
from core.services.prepare_tickets.save_ALL_blind_sigs import save_ALL_blind_sigs from core.services.prepare_tickets.save_ALL_blind_sigs import save_ALL_blind_sigs
from core.utils.basic_operations.does_file_exist import does_file_exist from core.utils.basic_operations.does_file_exist import does_file_exist
from core.controllers.ConfigurationController import ConfigurationController from core.controllers.ConfigurationController import ConfigurationController
from core.utils.type_checker import type_checker
# errors & constants from core.models.Result import Result, ResultError
from core.Constants import Constants from core.Constants import Constants
from core.errors.exceptions import MissingData, CriticalFailure
from core.errors.logger import logger from core.errors.logger import logger
from typing import Optional from typing import Optional
@ -34,156 +34,159 @@ TicketPrepController's respawn_billing_code_into_ticket
with which_ticket and billing_code. (and how_many_profiles is one) with which_ticket and billing_code. (and how_many_profiles is one)
""" """
REJECTED_INPUTS = [False, None, "error"]
"""
Called by:
TicketPrepController's prepare_tickets = For Bulk Tickets
TicketPrepController's respawn_billing_code_into_ticket = For a Single Ticket
"""
@type_checker
def ticket_prep_orchestrator( def ticket_prep_orchestrator(
how_many_profiles: int, how_many_profiles: int,
ticket_observer: TicketObserver, ticket_observer: TicketObserver,
connection_observer: ConnectionObserver, connection_observer: ConnectionObserver,
which_ticket: Optional[int] = None, which_ticket: Optional[int] = None,
billing_code: Optional[int] = None billing_code: Optional[int] = None
) -> dict: ) -> Result:
""" """Orchestrate the entire ticket preparation operation."""
Purpose:
Orchestrate the entire ticket preparation operation.
Rank:
Feature's Primary Cross-Module Orchestrator
Called by:
TicketPrepController's prepare_tickets = For Bulk Tickets
TicketPrepController's respawn_billing_code_into_ticket = For a Single Ticket
Returns:
Dicts. Shouldn't raise errors on it's own.
"""
if not billing_code and which_ticket: if not billing_code and which_ticket:
logger.error("Can't have no billing_code, if you have a ticket to replace") error_msg = "Can't have no billing_code, if you have a ticket to replace"
return { logger.error(error_msg)
"valid": False, return Result(valid=False, error_type=ResultError.INVALID_INPUT, message=error_msg)
"message": "missing_data",
}
try: #####################################################
##################################################### # MAKE BLIND COMMITMENTS
# MAKE BLIND COMMITMENTS #####################################################
##################################################### # make all commitments, but get the blinded ones:
# make all commitments, but get the blinded ones: list_of_all_blinded_commitments = make_ALL_commitments(how_many_profiles, which_ticket)
list_of_all_blinded_commitments = make_ALL_commitments(how_many_profiles, which_ticket)
# did we actually save them? if not, try again, if not list_of_all_blinded_commitments:
if list_of_all_blinded_commitments is None: error_msg = "We failed to make and save all the blinded commitments"
list_of_all_blinded_commitments = make_ALL_commitments(how_many_profiles, which_ticket) return Result(valid=False, error_type=ResultError.INVALID_INPUT, message=error_msg)
# still failed to save?!
if list_of_all_blinded_commitments is None:
return {
"valid": False,
"message": "cant_save",
}
##################################################### #####################################################
# SEND BLIND TO GET SIGNED # SEND BLIND TO GET SIGNED
##################################################### #####################################################
# assuming we actually saved the unblinding factors, # assuming we actually saved the unblinding factors,
notification = "Sending Blinded Package to the Server.." notification = "Sending Blinded Package to the Server.."
if ticket_observer is not None:
ticket_observer.notify("preparing", subject=notification)
# then send the entire blinded list to the server to sign:
bling_sig_result = send_blind_commitments(
list_of_all_blinded_commitments, ticket_observer, connection_observer, billing_code
)
if not bling_sig_result.valid:
return bling_sig_result
# Recieve signatures:
blind_signatures = bling_sig_result.data
#####################################################
# EVALUATE AND SAVE THE SERVER'S REPLIES
#####################################################
# regardless of the outcome of the verification, save all blind sigs, just in case, because the user can't get them again,
notification = "Saving the Server's Blind Replies"
if ticket_observer is not None:
ticket_observer.notify("preparing", subject=notification)
did_they_ALL_save = save_ALL_blind_sigs(blind_signatures, which_ticket)
if not did_they_ALL_save:
error_msg = "Failed to save the blind signatures locally! We continue with it in memory.."
logger.error(error_msg)
ticket_observer.notify("preparing", subject=error_msg)
#####################################################
# VERIFY BLIND SIGS
#####################################################
# verify the server's blind signatures against the public key,
notification = "Evaluating the Server's Blind Replies"
ticket_observer.notify("preparing", subject=notification)
failed_validations = validate_blind_signatures(
blind_signatures, ticket_observer, connection_observer, which_ticket
)
logger.debug(f"failed_validations is {failed_validations}")
#####################################################
# FAILED BLIND SIG VERIFICATION
#####################################################
# did verification of any of the blind sigs fail?
how_many_failed = len(failed_validations)
if how_many_failed >= 1:
notification = f"Verification failed for {how_many_failed} blind signatures. You still have to unblind them but its paused for the error."
if ticket_observer is not None: if ticket_observer is not None:
ticket_observer.notify("preparing", subject=notification) ticket_observer.notify("preparing", subject=notification)
logger.error(notification)
# then send the entire blinded list to the server to sign: return Result(
blind_signatures = send_blind_commitments( valid=False,
list_of_all_blinded_commitments, ticket_observer, connection_observer, billing_code error_type=ResultError.MATH,
data=failed_validations,
message=notification
) )
# Recieve signatures: #####################################################
if ( # PAID
blind_signatures == "error" #####################################################
or blind_signatures == None # saving on config that it's paid:
or blind_signatures == False config_marked = ConfigurationController.mark_as_paid(True)
): if not config_marked:
return { return Result(valid=False, error_type=ResultError.FILE_SYSTEM, message="Failed to mark as paid in the config. You still have the tickets locally, just can't use them till this file system error is resolved.")
"valid": False,
"message": f"The server's blind signature was blank or invalid. It said: {blind_signatures}",
"error_code": "invalid_reply"
}
else: #####################################################
##################################################### # UNBLIND
# EVALUATE AND SAVE THE SERVER'S REPLIES #####################################################
##################################################### # Unblind the signatures & combine with unblinded commitment:
# regardless of the outcome of the verification, save all blind sigs, just in case, because the user can't get them again, notification = "Unblinding Signatures & Preparing Tickets..."
notification = "Saving the Server's Blind Replies" ticket_observer.notify("preparing", subject=notification)
if ticket_observer is not None: did_prep_work = unblind_ALL_tickets(
ticket_observer.notify("preparing", subject=notification) ALL_signed_blind_signatures=blind_signatures,
did_they_ALL_save = save_ALL_blind_sigs(blind_signatures, which_ticket) ticket_observer=ticket_observer,
connection_observer=connection_observer,
which_ticket=which_ticket
)
# verify the server's blind signatures against the public key, #####################################################
notification = "Evaluating the Server's Blind Replies" # TICKET TRACKER & APP-WIDE CONFIG
ticket_observer.notify("preparing", subject=notification) #####################################################
failed_validations = validate_blind_signatures( if not does_file_exist(Constants.TICKET_TRACKER_PATH):
blind_signatures, ticket_observer, connection_observer, which_ticket setup_tracker = setup_ticket_tracker(how_many_profiles)
) if not setup_tracker:
return Result(valid=False, error_type=ResultError.FILE_SYSTEM, message="Failed to setup the ticket tracker. You still have the tickets locally, just can't use them till this file system error is resolved.")
logger.debug(f"failed_validations is {failed_validations}") #####################################################
# FINAL EVALUATION
#####################################################
if did_prep_work:
# this means it unblinded, & setup the tracker without erroring out,
# but it does NOT mean that verification of the blind sigs worked.
ConfigurationController.tickets_setup(True)
return Result(valid=True, message="Setup worked.")
# did verification of any of the blind sigs fail?
how_many_failed = len(failed_validations)
if how_many_failed >= 1:
notification = f"Verification failed for {how_many_failed} blind signatures."
if ticket_observer is not None:
ticket_observer.notify("preparing", subject=notification)
logger.debug(
f"Verification failed for {how_many_failed} blind signatures."
)
return {
"valid": False,
"message": "verification_failed",
"how_many_failed": how_many_failed,
"failed_validations": failed_validations,
}
##################################################### else:
# UNBLIND error_msg = "You have the blinded data, but we were unable to unblind it."
##################################################### logger.error(error_msg)
# Unblind the signatures & combine with unblinded commitment: return Result(valid=False, error_type=ResultError.MATH, message=error_msg)
notification = f"Unblinding Signatures & Preparing Tickets..."
ticket_observer.notify("preparing", subject=notification)
did_prep_work = unblind_ALL_tickets(
ALL_signed_blind_signatures=blind_signatures,
ticket_observer=ticket_observer,
connection_observer=connection_observer,
which_ticket=which_ticket
)
#####################################################
# TICKET TRACKER & APP-WIDE CONFIG
#####################################################
if not does_file_exist(Constants.TICKET_TRACKER_PATH):
setup_tracker = setup_ticket_tracker(how_many_profiles)
if not setup_tracker:
return {"valid": False, "message": "failed"}
# saving on config that it's setup:
config_marked = ConfigurationController.mark_as_paid(True)
if not config_marked:
return {'valid': False, 'message': 'file_system'}
#####################################################
# FINAL EVALUATION
#####################################################
if did_prep_work:
# this means it unblinded, & setup the tracker without erroring out,
# but it does NOT mean that verification of the blind sigs worked.
return {"valid": True, "message": "worked"}
else:
return {"valid": False, "message": "failed"}
except CriticalFailure as e:
error_msg = f"CriticalFailure error: {e}"
logger.error(error_msg, exc_info=True) # except CriticalFailure as e:
return {"valid": False, "message": error_msg} # error_msg = f"CriticalFailure error: {e}"
except MissingData as e: # logger.error(error_msg, exc_info=True)
error_msg = f"MissingData: {e}" # return {"valid": False, "message": error_msg}
logger.error(error_msg, exc_info=True) # except MissingData as e:
return {"valid": False, "message": error_msg} # error_msg = f"MissingData: {e}"
except: # logger.error(error_msg, exc_info=True)
error_msg = f"Generic Python Error" # return {"valid": False, "message": error_msg}
logger.error(error_msg, exc_info=True) # except:
return {"valid": False, "message": error_msg} # error_msg = f"Generic Python Error"
# logger.error(error_msg, exc_info=True)
# return {"valid": False, "message": error_msg}

View file

@ -314,13 +314,14 @@ def destroy_profile(
# RESPAWN TICKET # RESPAWN TICKET
#################################### ####################################
which_ticket = profile.ticket which_ticket = profile.ticket
respawn_result = respawn_profile( if which_ticket:
profile=profile, respawn_result = respawn_profile(
ticket_observer=ticket_observer, profile=profile,
connection_observer=connection_observer ticket_observer=ticket_observer,
) connection_observer=connection_observer
if which_ticket and not respawn_result.valid: )
logger.error(f"Unable to respawn the ticket. {respawn_result.message} {respawn_result.error_type}") if which_ticket and not respawn_result.valid:
logger.error(f"Unable to respawn the ticket. {respawn_result.message} {respawn_result.error_type}")
#################################### ####################################
# DESTROY PROFILE # DESTROY PROFILE

View file

@ -573,7 +573,7 @@ def wipe_subscription(profile_id: int) -> Result:
@type_checker @type_checker
def save_billing_code(profile_id: int, billing_code: str, expires_at: datetime) -> Result: def save_billing_code(profile_id: int, billing_code: str, expires_at: datetime, which_ticket: Optional[int] = None) -> Result:
"""Checks if the billing code is already saved, and if not, saves it.""" """Checks if the billing code is already saved, and if not, saves it."""
logger.info("Saving billing code..") logger.info("Saving billing code..")
@ -597,6 +597,8 @@ def save_billing_code(profile_id: int, billing_code: str, expires_at: datetime)
expires_at=expires_at, expires_at=expires_at,
billing_pending=True billing_pending=True
) )
if which_ticket:
profile.ticket = which_ticket
ProfileController.update(profile) ProfileController.update(profile)
return Result(valid=True) return Result(valid=True)
except Exception as e: except Exception as e:

View file

@ -3,135 +3,339 @@ from typing import TYPE_CHECKING
if TYPE_CHECKING: if TYPE_CHECKING:
from essentials.observers.ConnectionObserver import ConnectionObserver from essentials.observers.ConnectionObserver import ConnectionObserver
##############
from core.services.using_tickets.send_unblinded import send_unblinded_ticket_to_server from core.services.subscriptions.subscriptions_by_protocol import save_billing_code
from core.utils.basic_operations.write_string_to_text_file import write_string_to_text_file from core.utils.get_raw_string import get_raw_string
from core.services.prepare_tickets.ticket_tracker import update_ticket_tracker from core.observers.TicketObserver import TicketObserver
from core.models.Subscription import Subscription from core.services.helpers.does_ticket_file_exist import does_ticket_file_exist
from core.services.prepare_tickets.ticket_tracker import does_ticket_tracker_exist
from core.models.pydantic_models.ServerReply import ServerReply, ServerError
from core.services.payment_phase.payment_state import TicketPaymentData
from core.services.networking.make_url import make_url
from core.models.manage.server_reply_tools import api_call_and_extraction, ApiCall, translate_server_failure
from core.models.Result import Result, ResultError from core.models.Result import Result, ResultError
from core.errors.logger import logger from core.errors.logger import logger
from core.Constants import Constants
from core.models.enum_models import ProtocolChoice from core.models.enum_models import ProtocolChoice
from core.utils.type_checker import type_checker
from core.services.prepare_tickets.ticket_tracker import update_ticket_tracker
from core.controllers.ProfileController import ProfileController
# from core.models.BaseProfile import BaseProfile as Profile
# from core.services.using_tickets.send_unblinded import send_unblinded_ticket_to_server
# from core.models.Subscription import Subscription
# from core.utils.basic_operations.write_string_to_text_file import write_string_to_text_file
from datetime import datetime
CUSTOMER_SERVICE_MSG = "Please speak with customer service." CUSTOMER_SERVICE_MSG = "Please speak with customer service."
@type_checker
def save_ticket_use(
profile_id: int,
billing_code: str,
expires_at: datetime,
location_id: int,
which_ticket: int
) -> Result:
saved_for_profile = save_billing_code(
profile_id=profile_id,
billing_code=billing_code,
expires_at=expires_at,
which_ticket=which_ticket
)
# update the ticket tracker:
saved_ticket_tracker = update_ticket_tracker(
which_ticket=which_ticket,
status="used",
location=location_id,
subscription=billing_code
)
if not saved_for_profile.valid:
return saved_for_profile # result object
if saved_ticket_tracker:
return Result(valid=True)
else:
return Result(valid=False, error_type=ResultError.FILE_SYSTEM, message="Unable to update the ticket tracker that the ticket is used.")
def make_payload(which_ticket: int, which_location: int, protocol_choice: ProtocolChoice) -> Result:
"""Prepare the JSON to send to the billing server"""
# ticket
ticket_data = get_raw_string(str(which_ticket), "unblinded_final_ticket")
# key
which_key = TicketPaymentData().which_key()
if not which_key:
return Result(valid=False, error_type=ResultError.MISSING_DATA, message="Which ticket billing key is missing from your config file. Did you wipe it?")
# prep the JSON payload:
payload = {
"which_key": which_key,
"which_location": which_location,
"protocol_choice": protocol_choice.value,
"ticket_data": ticket_data,
}
return Result(valid=True, data=payload)
@type_checker
def use_ticket_orchestrator( def use_ticket_orchestrator(
profile_id: int,
which_ticket: int, which_ticket: int,
which_location: int, # Location.id ticket_observer: TicketObserver,
protocol_choice: ProtocolChoice,
connection_observer: ConnectionObserver, connection_observer: ConnectionObserver,
) -> Result: ) -> Result:
"""
Purpose:
Coordinates using a ticket.
Rank:
Orchestrator
Method:
1) Send to the server
2) Evaluate the response
3) updates the 'ticker_tracker' JSON with new data
4) Formats error messages for UI
Called by:
UseTicketController
Returns:
Return Objects
do NOT raise errors
"""
################################### ###################################
# SEND TO THE SERVER # PROFILE DATA
################################### ###################################
api_reply_object = send_unblinded_ticket_to_server( profile = ProfileController.get(profile_id)
which_ticket, which_location, protocol_choice, connection_observer if not profile:
error_msg = f"Invalid profile id of {profile_id}"
logger.error(error_msg)
return Result(valid=False, message=error_msg, error_type=ResultError.INVALID_INPUT)
if profile.ticket:
error_msg = f"This profile already has a ticket of {profile.ticket}."
logger.error(error_msg)
return Result(valid=False, error_type=ResultError.INVALID_INPUT, message=error_msg)
location_id = profile.location.id
protocol_choice = ProtocolChoice(profile.connection.code)
PRE_REQS = [profile_id, location_id, protocol_choice, which_ticket]
if None in PRE_REQS:
return Result(valid=False, error_type=ResultError.MISSING_DATA, message="You are missing the profile data to use a ticket with.")
###################################
# FILTER TICKET DATA
###################################
# does the ticket's file exist:
ticket_exists = does_ticket_file_exist(which_ticket)
if ticket_exists == False:
error_msg = "The ticket file does not exist in the correct folder."
ticket_observer.notify("failed_input", subject=error_msg)
return Result(valid=False, error_type=ResultError.INVALID_INPUT, message=error_msg)
# does the file keeping track of ALL tickets exist:
does_the_file_exist, path_of_file = does_ticket_tracker_exist()
if does_the_file_exist == False:
error_msg = f"The ticket tracker organizer file does not exist. Check the folder {path_of_file}"
ticket_observer.notify("failed_input", subject=error_msg)
return Result(valid=False, error_type=ResultError.MISSING_FILE, message=error_msg)
###################################
# PREP PAYLOAD
###################################
payload_result = make_payload(which_ticket=which_ticket, protocol_choice=protocol_choice)
if not payload_result.valid:
return payload_result
payload = payload_result.data
logger.info("Payload ready.")
###################################
# SEND TO API
###################################
which_endpoint = "validate"
url = make_url(which_endpoint)
ticket_observer.notify("connecting", "Connecting..")
server_reply = api_call_and_extraction(
desired_type=None,
method=ApiCall.POST,
url=url,
connection_observer=connection_observer,
payload=payload,
return_server_reply=True
) )
# NETWORK ERROR. # this is a connection error or invalid format itself:
if not api_reply_object.valid: if isinstance(server_reply, Result) or not isinstance(server_reply, ServerReply):
return Result(valid=False, error_type=ResultError.CONNECTION, message=f"Could not establish a connection to Ticketing Server. {api_reply_object.message}.") ticket_observer.notify("error", subject=server_reply.message)
return server_reply
################################### ###################################
# VERIFY DATA STRUCTURE # SUCCESS
################################### ###################################
raw_data_dict = api_reply_object.data if server_reply.valid or server_reply.error_type == ServerError.ALREADY_USED:
billing_code = server_reply.billing_code
expires_at = server_reply.expires_at
# INVALID STRUCTURE: if not billing_code or not expires_at:
if not isinstance(raw_data_dict, dict): return Result(valid=False, error_type=ResultError.SERVER_SIDE, message="Server Replied it worked, but failed to provide a valid code.")
error_msg = f"The Ticketing Server gave an invalid data format {type(raw_data_dict)} in it's reply."
logger.error(f"{error_msg} with raw value of {raw_data_dict}.")
return Result(valid=False, error_type=ResultError.INVALID_API_REPLY, message=f"{error_msg} {CUSTOMER_SERVICE_MSG}")
if "valid" not in raw_data_dict: return save_ticket_use(
error_msg = "The Ticketing Server gave an invalid data format that won't even tell us if it's valid or not." profile_id=profile_id,
logger.error(f"{error_msg} with a raw value of {raw_data_dict}")
return Result(valid=False, error_type=ResultError.INVALID_API_REPLY, message=f"{error_msg} {CUSTOMER_SERVICE_MSG}")
logger.debug("If we made it to this point, the API server connected and gave a correctly structured reply.")
###################################
# EXTRACT & ACT
###################################
valid = raw_data_dict.get("valid", None)
billing_code = raw_data_dict.get("billing_code", None)
expires_at = raw_data_dict.get("expires_at", None)
message = raw_data_dict.get("message", None)
logger.info(f"Was the ticket use valid? {valid}")
# GOOD OR ALREADY USED
if valid or message == "already_used":
logger.info("This is a valid use, or a previously valid use that's still good.")
# update the ticket tracker:
saved = update_ticket_tracker(
which_ticket=which_ticket,
status="used",
location=which_location,
subscription=billing_code
)
# Emergency:
if not saved:
emergency_save_billing(
which_ticket=which_ticket,
billing_code=billing_code
)
subscription = Subscription(
billing_code=billing_code, billing_code=billing_code,
expires_at=expires_at, expires_at=expires_at,
location_id=location_id,
which_ticket=which_ticket
) )
# even if not saved return it back, so it can be saved with the profile by other functions, ###################################
return Result(valid=True, data=subscription)
# EXPIRED # EXPIRED
if not valid and message == "expired": ###################################
logger.error(f"Expired Ticket! Wiping slot {which_ticket}") if server_reply.error_type == ServerError.EXPIRED:
saved = update_ticket_tracker( error_msg = "This ticket is expired. Do you want to wipe ALL your tickets? To start over."
which_ticket=which_ticket, return Result(valid=False, error_type=ResultError.TICKET_EXPIRED, message=error_msg)
status="unused",
location=None,
subscription=None
)
if not saved:
logger.error(f"Failed to save {which_ticket} is expired! That's okay the data is worthless to save.")
return Result(valid=False, error_type=ResultError.SUBSCRIPTION, message=f"This ticket {which_ticket}'s subscription is expired. Do you want to wipe it?")
# UNKNOWN ###################################
return Result(valid=False, error_type=ResultError.UNKNOWN, message=f"Unknown error with this data: {raw_data_dict}. {CUSTOMER_SERVICE_MSG}") # WRONG LOCATION
###################################
if server_reply.error_type == ServerError.WRONG_LOCATION:
error_msg = "The location entered is invalid. Do you want to sync location data now?"
return Result(valid=False, error_type=ResultError.NEED_SYNC, message=error_msg)
###################################
# NODE DOWN
###################################
if server_reply.error_type == ServerError.NODE_ERROR:
error_msg = f"{server_reply.message}. Do you want to switch locations for this profile?"
return Result(valid=False, error_type=ResultError.NODE_ERROR, message=error_msg) # Each GUI will navigate to edit.
###################################
# NO SOLUTION:
###################################
return translate_server_failure(server_reply=server_reply)
def emergency_save_billing(
which_ticket: int, # # # the actual work here, everything else is just handling:
billing_code: str # # ticket_observer.notify("connecting", "Connecting..")
) -> None: # # reply_object = use_ticket_orchestrator(
docstring = """ # # which_ticket=which_ticket,
If ticket tracker fails to save it to the JSON. Emergency save to a random text file. # # which_location=which_location,
""" # # protocol_choice=protocol_choice,
logger.error(docstring) # # connection_observer=connection_observer
which_ticket_as_str = str(which_ticket) # # )
write_string_to_text_file( # # return reply_object
billing_code,
f"{Constants.HV_TICKETING_CONFIG_HOME}/emergency_code_for_profile_{which_ticket_as_str}.txt",
) # @type_checker
# def use_ticket_orchestrator(
# which_ticket: int,
# which_location: int, # Location.id
# protocol_choice: ProtocolChoice,
# connection_observer: ConnectionObserver,
# ) -> Result:
# pass
# """
# Purpose:
# Coordinates using a ticket.
# Method:
# 1) Send to the server
# 2) Evaluate the response
# 3) updates the 'ticker_tracker' JSON with new data
# 4) Formats error messages for UI
# """
# save_billing_code(profile_id: int, billing_code: str, expires_at: datetime)
# ###################################
# # SEND TO THE SERVER
# ###################################
# api_reply_object = send_unblinded_ticket_to_server(
# which_ticket, which_location, protocol_choice, connection_observer
# )
# # NETWORK ERROR.
# if not api_reply_object.valid:
# return Result(valid=False, error_type=ResultError.CONNECTION, message=f"Could not establish a connection to Ticketing Server. {api_reply_object.message}.")
# ###################################
# # VERIFY DATA STRUCTURE
# ###################################
# raw_data_dict = api_reply_object.data
# # INVALID STRUCTURE:
# if not isinstance(raw_data_dict, dict):
# error_msg = f"The Ticketing Server gave an invalid data format {type(raw_data_dict)} in it's reply."
# logger.error(f"{error_msg} with raw value of {raw_data_dict}.")
# return Result(valid=False, error_type=ResultError.INVALID_API_REPLY, message=f"{error_msg} {CUSTOMER_SERVICE_MSG}")
# if "valid" not in raw_data_dict:
# error_msg = "The Ticketing Server gave an invalid data format that won't even tell us if it's valid or not."
# logger.error(f"{error_msg} with a raw value of {raw_data_dict}")
# return Result(valid=False, error_type=ResultError.INVALID_API_REPLY, message=f"{error_msg} {CUSTOMER_SERVICE_MSG}")
# logger.debug("If we made it to this point, the API server connected and gave a correctly structured reply.")
# ###################################
# # EXTRACT & ACT
# ###################################
# valid = raw_data_dict.get("valid", None)
# billing_code = raw_data_dict.get("billing_code", None)
# expires_at = raw_data_dict.get("expires_at", None)
# message = raw_data_dict.get("message", None)
# logger.info(f"Was the ticket use valid? {valid}")
# # GOOD OR ALREADY USED
# if valid or message == "already_used":
# logger.info("This is a valid use, or a previously valid use that's still good.")
# def save_billing(billing_code: str):
# # Emergency:
# if not saved:
# emergency_save_billing(
# which_ticket=which_ticket,
# billing_code=billing_code
# )
# subscription = Subscription(
# billing_code=billing_code,
# expires_at=expires_at,
# )
# # even if not saved return it back, so it can be saved with the profile by other functions,
# return Result(valid=True, data=subscription)
# # EXPIRED
# if not valid and message == "expired":
# logger.error(f"Expired Ticket! Wiping slot {which_ticket}")
# saved = update_ticket_tracker(
# which_ticket=which_ticket,
# status="unused",
# location=None,
# subscription=None
# )
# if not saved:
# logger.error(f"Failed to save {which_ticket} is expired! That's okay the data is worthless to save.")
# return Result(valid=False, error_type=ResultError.SUBSCRIPTION, message=f"This ticket {which_ticket}'s subscription is expired. Do you want to wipe it?")
# # UNKNOWN
# return Result(valid=False, error_type=ResultError.UNKNOWN, message=f"Unknown error with this data: {raw_data_dict}. {CUSTOMER_SERVICE_MSG}")
# def emergency_save_billing(
# which_ticket: int,
# billing_code: str
# ) -> None:
# docstring = """
# If ticket tracker fails to save it to the JSON. Emergency save to a random text file.
# """
# logger.error(docstring)
# which_ticket_as_str = str(which_ticket)
# write_string_to_text_file(
# billing_code,
# f"{Constants.HV_TICKETING_CONFIG_HOME}/emergency_code_for_profile_{which_ticket_as_str}.txt",
# )