diff --git a/core/controllers/tickets/TicketPrepController.py b/core/controllers/tickets/TicketPrepController.py index eaa9a0f..ddcd2c7 100644 --- a/core/controllers/tickets/TicketPrepController.py +++ b/core/controllers/tickets/TicketPrepController.py @@ -5,13 +5,14 @@ if TYPE_CHECKING: from core.essentials.observers.ConnectionObserver import ConnectionObserver 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.make_sure_pub_key_exists import make_sure_pub_key_exists from core.services.prepare_tickets import ticket_tracker from core.services.helpers.get_how_many_profiles_were_ordered import ( 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.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: + +@type_checker def prepare_tickets( how_many_profiles: int, ticket_observer: TicketObserver, connection_observer: ConnectionObserver, which_ticket: Optional[int] = None, billing_code: Optional[int] = None -) -> dict: +) -> Result: # these two are for preparation of a single ticket to a profile. # 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 ) - # rest of this function is evaluating the 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 prep_results.valid: + notification = "Done! All Tickets Ready!" + if ticket_observer is not None: + ticket_observer.notify("preparing", subject=notification) + else: + notification = "Ticket Preparation Failed" 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 + + + +# 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 def respawn_billing_code_into_ticket( billing_code: str, @@ -167,7 +183,7 @@ def respawn_billing_code_into_ticket( valid = ticket_result.get('valid', False) if not valid: - notification = f"Error in Respawn Prep!" + notification = "Error in Respawn Prep!" if ticket_observer is not None: ticket_observer.notify("preparing", subject=notification) return prep_error_result(ticket_result) diff --git a/core/models/manage/server_reply_tools.py b/core/models/manage/server_reply_tools.py index bd54956..733fcc6 100644 --- a/core/models/manage/server_reply_tools.py +++ b/core/models/manage/server_reply_tools.py @@ -18,7 +18,7 @@ class ApiCall(Enum): TRANSLATION_DICT = { ServerError.WRONG_LOCATION: 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.MISSING_DATA: ResultError.INVALID_INPUT, 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=error_type, message=server_reply.message) 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( - desired_type: str, + desired_type: Optional[str], method: ApiCall, url: str, connection_observer: ConnectionObserver, payload: dict, - billing_code: Optional[str] = None + billing_code: Optional[str] = None, + return_server_reply: bool = False, ) -> Result: """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) 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..") 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}" @@ -169,12 +174,3 @@ def api_call_and_extraction( data = getattr(server_reply, desired_type) logger.info(f"Successfully returning the data of {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) - - diff --git a/core/models/pydantic_models/ServerReply.py b/core/models/pydantic_models/ServerReply.py index a37af6d..b607d03 100644 --- a/core/models/pydantic_models/ServerReply.py +++ b/core/models/pydantic_models/ServerReply.py @@ -16,10 +16,11 @@ class ServerError(str, Enum): QUANTITY = "quantity" UNPAID = "unpaid" INVALID = "invalid" + ALREADY_USED = "already_used" MISSING_DATA = "missing_data" WRONG_LOCATION = "wrong_location" NODE_ERROR = "node_error" - BTCPAY = "btcpay" + CRYPTO_NODE = "crypto_node" SERVER_SIDE = "server_side" @@ -37,6 +38,8 @@ class ServerReply(): boolean_result: Optional[bool] = None conf: Optional[str] = None payment_status: Optional[bool] = None + expires_at: Optional[datetime] = None + @dataclass_json diff --git a/core/services/prepare_tickets/send_blind_commitments.py b/core/services/prepare_tickets/send_blind_commitments.py index 5884edc..1eab670 100644 --- a/core/services/prepare_tickets/send_blind_commitments.py +++ b/core/services/prepare_tickets/send_blind_commitments.py @@ -4,22 +4,25 @@ from typing import TYPE_CHECKING if TYPE_CHECKING: from essentials.observers.ConnectionObserver import ConnectionObserver from core.observers.TicketObserver import TicketObserver + # services -from core.services.networking.httpx import connect -from core.services.networking.api_requests.ApiResponseModel import ApiResponse, ErrorType +# from core.services.networking.httpx import connect +# from core.services.networking.api_requests.ApiResponseModel import ApiResponse, ErrorType from core.services.networking.make_url import make_url # from core.services.payment_phase.ticket_config_tools import get_which_billing_key # models & their controllers -from core.models.session.SessionProfile import SessionProfile -from core.models.system.SystemProfile import SystemProfile -from core.models.Subscription import Subscription # not sure if needed +from core.models.manage.server_reply_tools import api_call_and_extraction, ApiCall, translate_server_failure +from core.models.pydantic_models.ServerReply import ServerReply, ServerError +# from core.models.session.SessionProfile import SessionProfile +# from core.models.system.SystemProfile import SystemProfile # utils from core.utils.basic_operations.write_or_read_from_json import get_value_from_json_file # constants & errors 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 typing import Optional @@ -76,12 +79,24 @@ def prep_single_ticket_payload(billing_code: str, list_of_all_blinded_data: list 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( list_of_all_blinded_data: list, ticket_observer: TicketObserver, connection_observer: ConnectionObserver, billing_code: Optional[int] = None -) -> list | None: +) -> Result: """ Purpose: 1) Oversee payload prep @@ -105,61 +120,121 @@ def send_blind_commitments( # send it: 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: - reply = api_reply_object.data + server_reply = api_call_and_extraction( + 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? - 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 + # this is a connection error or invalid format itself: + if isinstance(server_reply, Result) or not isinstance(server_reply, ServerReply): + ticket_observer.notify("error", subject=server_reply.message) + return server_reply - 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 + # Worked: + if server_reply.valid: + return confirmed_data_result(blind_signatures=server_reply.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}" + error_type = server_reply.error_type + message = server_reply.message + ticket_observer.notify("error", subject=message) + + 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: - 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 + logger.info(f"Retrying with a different size of {quantity_required}.") + resized_list = list_of_all_blinded_data[:quantity_required] + resized_payload = prep_payload_for_bulk(resized_list) + second_server_reply = api_call_and_extraction( + desired_type="signed_data", + method=ApiCall.POST, + url=url, + 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 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 + + return translate_server_failure(server_reply=server_reply) + + + + + +# # # did API call work? +# 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 +# ) diff --git a/core/services/prepare_tickets/ticket_prep_orchestrator.py b/core/services/prepare_tickets/ticket_prep_orchestrator.py index 62f6758..c8bdc8a 100644 --- a/core/services/prepare_tickets/ticket_prep_orchestrator.py +++ b/core/services/prepare_tickets/ticket_prep_orchestrator.py @@ -4,20 +4,20 @@ from typing import TYPE_CHECKING if TYPE_CHECKING: from core.observers.TicketObserver import TicketObserver from essentials.observers.ConnectionObserver import ConnectionObserver -# services + + 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.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.validate_blind_signatures import validate_blind_signatures 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.controllers.ConfigurationController import ConfigurationController - -# errors & constants +from core.utils.type_checker import type_checker +from core.models.Result import Result, ResultError from core.Constants import Constants -from core.errors.exceptions import MissingData, CriticalFailure from core.errors.logger import logger 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) """ +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( how_many_profiles: int, ticket_observer: TicketObserver, connection_observer: ConnectionObserver, which_ticket: Optional[int] = None, billing_code: Optional[int] = None -) -> dict: - """ - Purpose: - Orchestrate the entire ticket preparation operation. +) -> Result: + """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: - logger.error("Can't have no billing_code, if you have a ticket to replace") - return { - "valid": False, - "message": "missing_data", - } + error_msg = "Can't have no billing_code, if you have a ticket to replace" + logger.error(error_msg) + return Result(valid=False, error_type=ResultError.INVALID_INPUT, message=error_msg) - try: - ##################################################### - # MAKE BLIND COMMITMENTS - ##################################################### - # make all commitments, but get the blinded ones: - list_of_all_blinded_commitments = make_ALL_commitments(how_many_profiles, which_ticket) + ##################################################### + # MAKE BLIND COMMITMENTS + ##################################################### + # make all commitments, but get the blinded ones: + list_of_all_blinded_commitments = make_ALL_commitments(how_many_profiles, which_ticket) - # did we actually save them? if not, try again, - if list_of_all_blinded_commitments is None: - list_of_all_blinded_commitments = make_ALL_commitments(how_many_profiles, which_ticket) - # still failed to save?! - if list_of_all_blinded_commitments is None: - return { - "valid": False, - "message": "cant_save", - } + if not list_of_all_blinded_commitments: + error_msg = "We failed to make and save all the blinded commitments" + return Result(valid=False, error_type=ResultError.INVALID_INPUT, message=error_msg) - ##################################################### - # SEND BLIND TO GET SIGNED - ##################################################### - # assuming we actually saved the unblinding factors, - notification = "Sending Blinded Package to the Server.." + ##################################################### + # SEND BLIND TO GET SIGNED + ##################################################### + # assuming we actually saved the unblinding factors, + 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: ticket_observer.notify("preparing", subject=notification) - - # then send the entire blinded list to the server to sign: - blind_signatures = send_blind_commitments( - list_of_all_blinded_commitments, ticket_observer, connection_observer, billing_code + logger.error(notification) + return Result( + valid=False, + error_type=ResultError.MATH, + data=failed_validations, + message=notification ) - # Recieve signatures: - if ( - blind_signatures == "error" - or blind_signatures == None - or blind_signatures == False - ): - return { - "valid": False, - "message": f"The server's blind signature was blank or invalid. It said: {blind_signatures}", - "error_code": "invalid_reply" - } + ##################################################### + # PAID + ##################################################### + # saving on config that it's paid: + config_marked = ConfigurationController.mark_as_paid(True) + if not config_marked: + 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.") - else: - ##################################################### - # 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) + ##################################################### + # UNBLIND + ##################################################### + # Unblind the signatures & combine with unblinded commitment: + notification = "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 + ) - # 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 - ) + ##################################################### + # 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 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, - } - ##################################################### - # UNBLIND - ##################################################### - # Unblind the signatures & combine with unblinded commitment: - 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 - ) + else: + error_msg = "You have the blinded data, but we were unable to unblind it." + logger.error(error_msg) + return Result(valid=False, error_type=ResultError.MATH, message=error_msg) - ##################################################### - # 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) - return {"valid": False, "message": error_msg} - except MissingData as e: - error_msg = f"MissingData: {e}" - logger.error(error_msg, exc_info=True) - return {"valid": False, "message": error_msg} - except: - error_msg = f"Generic Python Error" - logger.error(error_msg, exc_info=True) - return {"valid": False, "message": error_msg} + + +# except CriticalFailure as e: +# error_msg = f"CriticalFailure error: {e}" +# logger.error(error_msg, exc_info=True) +# return {"valid": False, "message": error_msg} +# except MissingData as e: +# error_msg = f"MissingData: {e}" +# logger.error(error_msg, exc_info=True) +# return {"valid": False, "message": error_msg} +# except: +# error_msg = f"Generic Python Error" +# logger.error(error_msg, exc_info=True) +# return {"valid": False, "message": error_msg} diff --git a/core/services/profiles/profile_connections.py b/core/services/profiles/profile_connections.py index 6c506f8..e7427be 100644 --- a/core/services/profiles/profile_connections.py +++ b/core/services/profiles/profile_connections.py @@ -314,13 +314,14 @@ def destroy_profile( # RESPAWN TICKET #################################### which_ticket = profile.ticket - respawn_result = respawn_profile( - profile=profile, - 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: + respawn_result = respawn_profile( + profile=profile, + 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}") #################################### # DESTROY PROFILE diff --git a/core/services/subscriptions/subscriptions_by_protocol.py b/core/services/subscriptions/subscriptions_by_protocol.py index 3d665eb..11f62f9 100644 --- a/core/services/subscriptions/subscriptions_by_protocol.py +++ b/core/services/subscriptions/subscriptions_by_protocol.py @@ -573,7 +573,7 @@ def wipe_subscription(profile_id: int) -> Result: @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.""" 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, billing_pending=True ) + if which_ticket: + profile.ticket = which_ticket ProfileController.update(profile) return Result(valid=True) except Exception as e: diff --git a/core/services/using_tickets/use_ticket_orchestrator.py b/core/services/using_tickets/use_ticket_orchestrator.py index 3f4ce02..f143dc7 100644 --- a/core/services/using_tickets/use_ticket_orchestrator.py +++ b/core/services/using_tickets/use_ticket_orchestrator.py @@ -3,135 +3,339 @@ from typing import TYPE_CHECKING if TYPE_CHECKING: from essentials.observers.ConnectionObserver import ConnectionObserver -############## -from core.services.using_tickets.send_unblinded import send_unblinded_ticket_to_server -from core.utils.basic_operations.write_string_to_text_file import write_string_to_text_file -from core.services.prepare_tickets.ticket_tracker import update_ticket_tracker -from core.models.Subscription import Subscription + +from core.services.subscriptions.subscriptions_by_protocol import save_billing_code +from core.utils.get_raw_string import get_raw_string +from core.observers.TicketObserver import TicketObserver +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.errors.logger import logger -from core.Constants import Constants 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." +@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( + profile_id: int, which_ticket: int, - which_location: int, # Location.id - protocol_choice: ProtocolChoice, + ticket_observer: TicketObserver, connection_observer: ConnectionObserver, ) -> 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( - which_ticket, which_location, protocol_choice, connection_observer + profile = ProfileController.get(profile_id) + 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. - 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}.") + # this is a connection error or invalid format itself: + if isinstance(server_reply, Result) or not isinstance(server_reply, ServerReply): + 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 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 not billing_code or not expires_at: + return Result(valid=False, error_type=ResultError.SERVER_SIDE, message="Server Replied it worked, but failed to provide a valid code.") - 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.") - - # 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( + return save_ticket_use( + profile_id=profile_id, billing_code=billing_code, 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 - 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?") + ################################### + if server_reply.error_type == ServerError.EXPIRED: + error_msg = "This ticket is expired. Do you want to wipe ALL your tickets? To start over." + return Result(valid=False, error_type=ResultError.TICKET_EXPIRED, message=error_msg) - # 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) + + + +# # # the actual work here, everything else is just handling: +# # ticket_observer.notify("connecting", "Connecting..") +# # reply_object = use_ticket_orchestrator( +# # which_ticket=which_ticket, +# # which_location=which_location, +# # protocol_choice=protocol_choice, +# # connection_observer=connection_observer +# # ) +# # return reply_object -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", - ) +# @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", +# )