From 5ab57ab28775e490b979a025e3ff610462f28b2d Mon Sep 17 00:00:00 2001 From: SimplifiedPrivacy Date: Sat, 15 Aug 2026 17:34:08 -0400 Subject: [PATCH] When a profile is deleted, it now automatically respawns the ticket if it's a ticket profile. Also fixed some bugs with fetching the server's public key with the APIResponse object being used, when it expected a regular dictionary. This is a left-over from the prior transition to API objects. --- change_log.md | 5 +++ core/controllers/ProfileController.py | 45 ++++++++++++++++++- .../tickets/TicketPrepController.py | 7 ++- core/services/prepare_tickets/get_pub_key.py | 21 ++++++--- .../prepare_tickets/send_blind_commitments.py | 2 +- .../prepare_tickets/ticket_tracker.py | 23 +++++++++- .../write_string_to_text_file.py | 6 ++- 7 files changed, 96 insertions(+), 13 deletions(-) diff --git a/change_log.md b/change_log.md index 5f0d44a..f0ec420 100644 --- a/change_log.md +++ b/change_log.md @@ -1,5 +1,10 @@ # Major Change Log: +# Respawn on Delete +### Aug 15, 2026 +When a profile is deleted, it now automatically respawns the ticket if it's a ticket profile. Also fixed some bugs with fetching the server's public key with the APIResponse object being used, when it expected a regular dictionary. This is a left-over from the prior transition to API objects. +
+ # Codes/Tickets ### Aug 15, 2026 The profile object now has a ticket field. It's now none by default. Also the profile object is passed in, for the use of tickets, both for GUI and core. diff --git a/core/controllers/ProfileController.py b/core/controllers/ProfileController.py index 048e09b..403f8ad 100644 --- a/core/controllers/ProfileController.py +++ b/core/controllers/ProfileController.py @@ -7,6 +7,10 @@ from core.services.networking.systemwide import killswitch from core.services.subscriptions import subscriptions from core.errors.exceptions import FirewallError from core.models.Result import Result, ResultError +from core.observers.TicketObserver import TicketObserver +from core.controllers.tickets.TicketPrepController import respawn_billing_code_into_ticket +from core.services.prepare_tickets.ticket_tracker import get_tickets_with +from core.errors.logger import logger from core.Errors import InvalidSubscriptionError, MissingSubscriptionError, ConnectionTerminationError, ProfileActivationError, ProfileDeactivationError, MissingLocationError, ConnectionUnprotectedError, EndpointVerificationError, ProfileStateConflictError from core.controllers.ApplicationController import ApplicationController @@ -182,8 +186,47 @@ class ProfileController: time.sleep(1.0) @staticmethod - def destroy(profile: Union[SessionProfile, SystemProfile], profile_observer: ProfileObserver = None): + def destroy(profile: Union[SessionProfile, SystemProfile], profile_observer: ProfileObserver = None, ticket_observer: TicketObserver = None, connection_observer: ConnectionObserver = None): + #################################### + # DESTROY TICKET + #################################### + which_ticket = profile.ticket + + # try via lookup: + if not which_ticket: + logger.info(f"Unable to find the ticket # in the profile's native config for {profile.id}. Checking the ticket tracker JSON") + target_subscription = profile.subscription.billing_code + ticket_list = get_tickets_with(target_subscription) + if len(ticket_list) >= 1: + which_ticket = ticket_list[0] + logger.info(f"We got the ticket {which_ticket} for profile {profile.id} from the ticket tracker JSON") + else: + logger.info(f"We were UNABLE to find a ticket with the subscription for profile {profile.id} from the ticket tracker JSON. Proceeding with delete regardless..") + + # Regardless of how it was acquired, + if which_ticket: + notification = f"Respawn Started for Ticket {which_ticket}" + logger.info(notification) + ticket_observer.notify("preparing", subject=notification) + respawn_result = respawn_billing_code_into_ticket( + profile=profile, + which_ticket=which_ticket, + ticket_observer=ticket_observer, + connection_observer=connection_observer + ) + if respawn_result.valid: + notification = f"Ticket Respawned!" + logger.info(notification) + ticket_observer.notify("preparing", subject=notification) + else: + notification = f"Error with Ticket Respawn!" + logger.error(f"{notification} {respawn_result.error_type} with message: {respawn_result.message}") + ticket_observer.notify("preparing", subject=notification) + + #################################### + # DESTROY PROFILE + #################################### ProfileController.disable(profile) profile.delete() diff --git a/core/controllers/tickets/TicketPrepController.py b/core/controllers/tickets/TicketPrepController.py index 0ae7fed..7abfb97 100644 --- a/core/controllers/tickets/TicketPrepController.py +++ b/core/controllers/tickets/TicketPrepController.py @@ -110,7 +110,7 @@ def respawn_billing_code_into_ticket( which_ticket: int, ticket_observer: TicketObserver, connection_observer: ConnectionObserver -) -> bool: +) -> Result: """ Converts a valid VPN billing code, into a valid unused ticket. At the cost of the server expiring the billing code immediately. @@ -128,7 +128,10 @@ def respawn_billing_code_into_ticket( return Result(valid=False, error_type=ResultError.MISSING_DATA, message="Missing which ticket slot is being respawned") if not isinstance(which_ticket, int): - return Result(valid=False, error_type=ResultError.INVALID_INPUT, message="Ticket slot must be a number.") + try: + which_ticket = int(which_ticket) + except: + return Result(valid=False, error_type=ResultError.INVALID_INPUT, message="Ticket slot must be a number.") ticket_result = prepare_tickets( how_many_profiles=1, diff --git a/core/services/prepare_tickets/get_pub_key.py b/core/services/prepare_tickets/get_pub_key.py index 6d8b467..91a9d6f 100644 --- a/core/services/prepare_tickets/get_pub_key.py +++ b/core/services/prepare_tickets/get_pub_key.py @@ -31,7 +31,6 @@ def get_from_server_and_save( url = make_url(string_form_of_key_name) - # public_key_results = get_data_from_server(url, connection_observer) public_key_results = connect.single_endpoint( method="get", url=url, @@ -39,14 +38,22 @@ def get_from_server_and_save( payload=None ) - if public_key_results.valid: - # extract: - public_key = public_key_results.data + if not public_key_results.valid: + error_msg = f"Error with API getting the Public Key: {public_key_results.error_type} and {public_key_results.message}" + logger.error(error_msg) + return {"valid": False, "message": error_msg} - # save it: - did_it_save = write_string_to_text_file(public_key, file_path) + # extract: + public_key = public_key_results.data - return public_key_results + # save it: + did_it_save = write_string_to_text_file(public_key, file_path) + + if not did_it_save: + logger.error(f"Error with saving the public key {public_key} with write_string_to_text_file, but still returning data..") + + # regardless of saving: + return {"valid": True, "data": public_key} def get_pub_key( diff --git a/core/services/prepare_tickets/send_blind_commitments.py b/core/services/prepare_tickets/send_blind_commitments.py index 93f2349..fd3d86f 100644 --- a/core/services/prepare_tickets/send_blind_commitments.py +++ b/core/services/prepare_tickets/send_blind_commitments.py @@ -16,7 +16,7 @@ from core.services.helpers.get_which_billing_key import get_which_billing_key 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.controllers.ProfileController import ProfileController +# from core.controllers.ProfileController import ProfileController # utils from core.utils.basic_operations.write_or_read_from_json import get_value_from_json_file diff --git a/core/services/prepare_tickets/ticket_tracker.py b/core/services/prepare_tickets/ticket_tracker.py index bd4f251..2a2f3b6 100644 --- a/core/services/prepare_tickets/ticket_tracker.py +++ b/core/services/prepare_tickets/ticket_tracker.py @@ -1,8 +1,10 @@ from core.utils.basic_operations.write_or_read_from_json import read_entire_json, update_json +from core.utils.basic_operations.does_file_exist import does_file_exist from core.errors.exceptions import * from core.errors.logger import logger from core.Constants import Constants +# generic import json import os @@ -88,4 +90,23 @@ def wipe_one_ticket_sub(which_ticket: int) -> bool: filepath=Constants.TICKET_TRACKER_PATH, key_to_add=which_ticket, value_to_update=wipe_payload - ) \ No newline at end of file + ) + + +def get_tickets_with(target_subscription: str) -> list: + + if not does_file_exist(Constants.TICKET_TRACKER_PATH): + return [] + + whole_json = read_entire_json(Constants.TICKET_TRACKER_PATH) + if not whole_json: + return [] + + list_of_matches = [] + + for each_number in whole_json: + if whole_json[each_number]['subscription'] == target_subscription: + list_of_matches.append(each_number) + + return list_of_matches + diff --git a/core/utils/basic_operations/write_string_to_text_file.py b/core/utils/basic_operations/write_string_to_text_file.py index be981a0..7ecac0e 100644 --- a/core/utils/basic_operations/write_string_to_text_file.py +++ b/core/utils/basic_operations/write_string_to_text_file.py @@ -1,3 +1,5 @@ +import os + """ Note: this function will accept EITHER raw dicts or string, WITHOUT converting to JSON, and literally convert to str to save. At a first glance, this seems ridiculous. @@ -13,16 +15,18 @@ Therefore, we opted for this data storage format for some data types. """ + def write_string_to_text_file(content_to_write: str | dict, file_path: str) -> bool: if content_to_write is None: return False try: + os.makedirs(os.path.dirname(file_path), exist_ok=True) with open(file_path, "w") as file: file.write(str(content_to_write)) return True except FileNotFoundError: - print(f"Error: The file '{file_path}' was not found.") + print(f"Error inside of write_string_to_text: The file '{file_path}' was not found.") return False except IOError: print(