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.

This commit is contained in:
SimplifiedPrivacy 2026-08-15 17:34:08 -04:00
parent 829feb5939
commit 5ab57ab287
7 changed files with 96 additions and 13 deletions

View file

@ -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.
<br/>
# 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.

View file

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

View file

@ -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,6 +128,9 @@ 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):
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(

View file

@ -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:
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}
# extract:
public_key = public_key_results.data
# save it:
did_it_save = write_string_to_text_file(public_key, file_path)
return public_key_results
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(

View file

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

View file

@ -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
@ -89,3 +91,22 @@ def wipe_one_ticket_sub(which_ticket: int) -> bool:
key_to_add=which_ticket,
value_to_update=wipe_payload
)
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

View file

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