Assassin mode Introduced! The assassin_tools allows for creation/use of a session profile with random attributes, ephemeral wireguard config, and coordinates using a ticket for it. This tools module also allows for wiping the assassin's ticket & wireguard config on disable. Additionally, this git commit sees ProfileController using these assassin_tools functions for a working implementation if the profile is marked assassin=True. Also the associated eco-system of ApplicationController starting apps, the existing non-ticket legacy subscription handlers, and even GUI needed to be adjusted to allow for this new assassin flow. Finally, the generic JSON utilities have been improved to make this feature more robust and reliable.

This commit is contained in:
SimplifiedPrivacy 2026-08-18 12:54:54 -04:00
parent a78747e8e5
commit 6dac98835f
26 changed files with 897 additions and 342 deletions

View file

@ -1,5 +1,11 @@
# Major Change Log: # Major Change Log:
# Assassin Introduced
### Aug 18, 2026
Assassin mode Introduced! The assassin_tools allows for creation/use of a session profile with random attributes, ephemeral wireguard config, and coordinates using a ticket for it. This tools module also allows for wiping the assassin's ticket & wireguard config on disable. Additionally, this git commit sees ProfileController using these assassin_tools functions for a working implementation if the profile is marked "assassin=True". Also the associated eco-system of ApplicationController starting apps, the existing non-ticket legacy subscription handlers, and even GUI needed to be adjusted to allow for this new assassin flow. Finally, the generic JSON utilities have been improved to make this feature more robust and reliable.
<br/>
# Respawn on Delete # Respawn on Delete
### Aug 15, 2026 ### 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. 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.

View file

@ -2,8 +2,9 @@ from core.Constants import Constants
from core.Errors import CommandNotFoundError from core.Errors import CommandNotFoundError
from core.controllers.SessionStateController import SessionStateController from core.controllers.SessionStateController import SessionStateController
from core.models.session.Application import Application from core.models.session.Application import Application
# from core.models.session.ApplicationVersion import ApplicationVersion
from core.models.orm_models.ApplicationVersion import ApplicationVersion from core.models.orm_models.ApplicationVersion import ApplicationVersion
from core.observers.ConnectionObserver import ConnectionObserver
from core.observers.TicketObserver import TicketObserver
from core.models.session.SessionProfile import SessionProfile from core.models.session.SessionProfile import SessionProfile
from core.models.session.SessionState import SessionState from core.models.session.SessionState import SessionState
@ -32,7 +33,7 @@ class ApplicationController:
return Application.all() return Application.all()
@staticmethod @staticmethod
def launch(version: ApplicationVersion, profile: SessionProfile, port_number: int = None, asynchronous: bool = False, profile_observer: Optional[ProfileObserver] = None): def launch(version: ApplicationVersion, profile: SessionProfile, port_number: int = None, asynchronous: bool = False, profile_observer: Optional[ProfileObserver] = None, connection_observer: ConnectionObserver = None, ticket_observer: TicketObserver = None):
from core.controllers.ProfileController import ProfileController from core.controllers.ProfileController import ProfileController
@ -98,7 +99,7 @@ class ApplicationController:
if not fork_process_id: if not fork_process_id:
ApplicationController.__run_process(initialization_file_path, profile, display, session_state) ApplicationController.__run_process(initialization_file_path, profile, display, session_state)
ProfileController.disable(profile, False, profile_observer=profile_observer) ProfileController.disable(profile, False, profile_observer=profile_observer, ticket_observer=ticket_observer, connection_observer=connection_observer)
time.sleep(1.0) time.sleep(1.0)
sys.exit() sys.exit()
@ -106,7 +107,7 @@ class ApplicationController:
else: else:
ApplicationController.__run_process(initialization_file_path, profile, display, session_state) ApplicationController.__run_process(initialization_file_path, profile, display, session_state)
ProfileController.disable(profile, False, profile_observer=profile_observer) ProfileController.disable(profile, False, profile_observer=profile_observer, ticket_observer=ticket_observer, connection_observer=connection_observer)
@staticmethod @staticmethod
def _sync(proxies: Optional[dict] = None): def _sync(proxies: Optional[dict] = None):

View file

@ -4,6 +4,7 @@ from core.errors.logger import logger
from core.services.networking.general_connection_tools.testing_evaluating import await_connection, system_uses_wireguard_interface, await_network_interface from core.services.networking.general_connection_tools.testing_evaluating import await_connection, system_uses_wireguard_interface, await_network_interface
from core.services.networking.systemwide.systemwide_wireguard import establish_system_connection, terminate_system_connection from core.services.networking.systemwide.systemwide_wireguard import establish_system_connection, terminate_system_connection
from core.services.keys_and_verifications.endpoint_verification import verify_wireguard_endpoint from core.services.keys_and_verifications.endpoint_verification import verify_wireguard_endpoint
from core.observers.TicketObserver import TicketObserver
from collections.abc import Callable from collections.abc import Callable
@ -60,7 +61,7 @@ class ConnectionController:
@staticmethod @staticmethod
def establish_session_connection(profile: SessionProfile, ignore: tuple[type[Exception]] = (), connection_observer: Optional[ConnectionObserver] = None): def establish_session_connection(profile: SessionProfile, ignore: tuple[type[Exception]] = (), connection_observer: Optional[ConnectionObserver] = None, ticket_observer: Optional[TicketObserver] = None):
session_directory = tempfile.mkdtemp(prefix='hv-') session_directory = tempfile.mkdtemp(prefix='hv-')
session_state = SessionStateController.get_or_new(profile.id) session_state = SessionStateController.get_or_new(profile.id)
@ -77,7 +78,7 @@ class ConnectionController:
raise ConnectionUnprotectedError('Connection unprotected while the system is not using a WireGuard interface.') raise ConnectionUnprotectedError('Connection unprotected while the system is not using a WireGuard interface.')
else: else:
from core.controllers.ProfileController import ProfileController from core.controllers.ProfileController import ProfileController
ProfileController.disable(profile) ProfileController.disable(profile=profile, connection_observer=connection_observer, ticket_observer=ticket_observer)
if profile.connection.code == 'tor': if profile.connection.code == 'tor':

View file

@ -8,8 +8,8 @@ from core.services.subscriptions import subscriptions
from core.errors.exceptions import FirewallError from core.errors.exceptions import FirewallError
from core.models.Result import Result, ResultError from core.models.Result import Result, ResultError
from core.observers.TicketObserver import TicketObserver from core.observers.TicketObserver import TicketObserver
from core.controllers.tickets.TicketPrepController import respawn_billing_code_into_ticket from core.services.assassin.ticket_respawn import respawn_profile
from core.services.prepare_tickets.ticket_tracker import get_tickets_with from core.services.assassin import assassin_tools
from core.errors.logger import logger from core.errors.logger import logger
from core.Errors import InvalidSubscriptionError, MissingSubscriptionError, ConnectionTerminationError, ProfileActivationError, ProfileDeactivationError, MissingLocationError, ConnectionUnprotectedError, EndpointVerificationError, ProfileStateConflictError from core.Errors import InvalidSubscriptionError, MissingSubscriptionError, ConnectionTerminationError, ProfileActivationError, ProfileDeactivationError, MissingLocationError, ConnectionUnprotectedError, EndpointVerificationError, ProfileStateConflictError
@ -58,7 +58,10 @@ class ProfileController:
asynchronous: bool = False, asynchronous: bool = False,
profile_observer: ProfileObserver = None, profile_observer: ProfileObserver = None,
application_version_observer: ApplicationVersionObserver = None, application_version_observer: ApplicationVersionObserver = None,
connection_observer: ConnectionObserver = None): connection_observer: ConnectionObserver = None,
ticket_observer: TicketObserver = None,
max_resolution: Optional[str] = None
):
from core.controllers.ConnectionController import ConnectionController from core.controllers.ConnectionController import ConnectionController
@ -78,13 +81,29 @@ class ProfileController:
# ============================================================================ # ============================================================================
if profile.is_session_profile(): if profile.is_session_profile():
# ASSASSIN MODE
if profile.assassin:
assassin_result = assassin_tools.create(
profile=profile,
max_resolution=max_resolution,
ticket_observer=ticket_observer,
connection_observer=connection_observer
)
if not assassin_result.valid:
error_msg = f"Error with setting up Assassin: {assassin_result.error_type} & {assassin_result.message}"
logger.error(error_msg)
raise ProfileActivationError(f'Assassin NOT enabled: {assassin_result.message}')
# random assassin values become the main profile:
profile = assassin_result.data
# CONCLUSION OF ASSASSIN CODE
application_version = profile.application_version application_version = profile.application_version
if not application_version.is_installed(): if not application_version.is_installed():
ApplicationVersionController.install(application_version, application_version_observer=application_version_observer, connection_observer=connection_observer) ApplicationVersionController.install(application_version, application_version_observer=application_version_observer, connection_observer=connection_observer)
try: try:
port_number = establish_connection(profile, ignore=ignore, connection_observer=connection_observer) port_number = establish_connection(profile, ignore=ignore, connection_observer=connection_observer, ticket_observer=ticket_observer)
except ConnectionError: except ConnectionError:
raise ProfileActivationError('The profile could not be enabled.') raise ProfileActivationError('The profile could not be enabled.')
except ValueError: except ValueError:
@ -93,7 +112,7 @@ class ProfileController:
if profile_observer is not None: if profile_observer is not None:
profile_observer.notify('enabled', profile) profile_observer.notify('enabled', profile)
ApplicationController.launch(application_version, profile, port_number, asynchronous=asynchronous, profile_observer=profile_observer) ApplicationController.launch(application_version, profile, port_number, asynchronous=asynchronous, profile_observer=profile_observer, ticket_observer=ticket_observer, connection_observer=connection_observer)
# ============================================================================ # ============================================================================
# SYSTEMWIDE # SYSTEMWIDE
@ -112,11 +131,31 @@ class ProfileController:
@staticmethod @staticmethod
def disable(profile: Union[SessionProfile, SystemProfile], explicitly: bool = True, ignore: tuple[type[Exception]] = (), profile_observer: ProfileObserver = None): def disable(
profile: Union[SessionProfile, SystemProfile],
explicitly: bool = True,
ignore: tuple[type[Exception]] = (),
profile_observer: ProfileObserver = None,
ticket_observer: TicketObserver = None,
connection_observer: ConnectionObserver = None,
wipe_assassin: bool = False
):
from core.controllers.ConnectionController import ConnectionController from core.controllers.ConnectionController import ConnectionController
if profile.is_session_profile(): if profile.is_session_profile():
print(f"wipe_assassin is {wipe_assassin}")
if profile.assassin and wipe_assassin:
print("triggering to WIPE assassin")
assassin_result = assassin_tools.wipe(
profile=profile,
ticket_observer=ticket_observer,
connection_observer=connection_observer
)
if not assassin_result.valid:
error_msg = f"Error with wiping respawn of Assassin: {assassin_result.error_type} & {assassin_result.message}"
logger.error(error_msg)
# raise ProfileDeactivationError(f'Assassin NOT disabled: {assassin_result.message}')
if SessionStateController.exists(profile.id): if SessionStateController.exists(profile.id):
@ -192,37 +231,13 @@ class ProfileController:
# DESTROY TICKET # DESTROY TICKET
#################################### ####################################
which_ticket = profile.ticket which_ticket = profile.ticket
respawned = respawn_profile(
# 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, profile=profile,
which_ticket=which_ticket,
ticket_observer=ticket_observer, ticket_observer=ticket_observer,
connection_observer=connection_observer connection_observer=connection_observer
) )
if respawn_result.valid: if which_ticket and not respawned:
notification = f"Ticket Respawned!" logger.error("Serious issue with respawning the ticket. We might raise an error here.")
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 # DESTROY PROFILE

View file

@ -10,9 +10,10 @@ from core.services.prepare_tickets.get_pub_key import get_pub_key
from core.observers.BaseObserver import BaseObserver from core.observers.BaseObserver import BaseObserver
from core.services.payment_phase.save_and_send_intitial_billing import save_and_send_intitial_billing from core.services.payment_phase.save_and_send_intitial_billing import save_and_send_intitial_billing
from core.services.networking.api_requests.ApiResponseModel import ApiResponse, ErrorType from core.services.networking.api_requests.ApiResponseModel import ApiResponse, ErrorType
from core.models.Result import Result, ResultError
from core.services.networking.api_requests.step5_solve_api_problems import solve_api_problems from core.services.networking.api_requests.step5_solve_api_problems import solve_api_problems
from core.errors.logger import logger from core.errors.logger import logger
from core.errors.exceptions import ServerSideError, NetworkingError # from core.errors.exceptions import NetworkingError
from core.services.prepare_tickets.ticket_tracker import does_ticket_tracker_exist from core.services.prepare_tickets.ticket_tracker import does_ticket_tracker_exist
from core.services.prepare_tickets.setup_ticket_tracker import setup_ticket_tracker from core.services.prepare_tickets.setup_ticket_tracker import setup_ticket_tracker
@ -24,7 +25,6 @@ from core.services.networking.make_url import make_url
# from core.utils.confirm_its_a_valid_key_choice import confirm_its_a_valid_key_choice # from core.utils.confirm_its_a_valid_key_choice import confirm_its_a_valid_key_choice
from core.services.helpers.valid_profile_quantity import valid_profile_quantity from core.services.helpers.valid_profile_quantity import valid_profile_quantity
from core.errors.exceptions import * from core.errors.exceptions import *
from core.errors.logger import logger
from core.controllers.tickets.TicketSyncController import sync_ticket_prices from core.controllers.tickets.TicketSyncController import sync_ticket_prices
from core.services.payment_phase.do_we_have_billing_id import do_we_have_billing_id from core.services.payment_phase.do_we_have_billing_id import do_we_have_billing_id
@ -46,68 +46,63 @@ def initiate_payment(
) -> TicketInvoice: ) -> TicketInvoice:
############### ###############
invoice_data_object = TicketInvoice()
try:
if bypass_existing == False: if bypass_existing == False:
tickets_exist_already, path = does_ticket_tracker_exist() tickets_exist_already, path = does_ticket_tracker_exist()
logger.debug(f"tickets_exist_already is {tickets_exist_already}") logger.debug(f"tickets_exist_already is {tickets_exist_already}")
if tickets_exist_already: if tickets_exist_already:
invoice_data_object.add_error_code("already_exists") return Result(valid=False, error_type=ResultError.ALREADY_EXISTS, message="There's already a ticket billing session in progress. Do you want to wipe it?")
return invoice_data_object
else: else:
made_ticket_tracker = setup_ticket_tracker(how_many_profiles=how_many_profiles) made_ticket_tracker = setup_ticket_tracker(how_many_profiles=how_many_profiles)
logger.debug(f"Ticket Tracker doesn't exist, so we made it: {made_ticket_tracker} for {how_many_profiles} profiles") logger.debug(f"Ticket Tracker doesn't exist, so we made it: {made_ticket_tracker} for {how_many_profiles} profiles")
billing_id = do_we_have_billing_id() billing_id = do_we_have_billing_id()
if billing_id: if billing_id:
invoice_data_object.add_error_code("billing_code_exists") error_msg = "Billing code exists already"
invoice_data_object.temp_billing_code = billing_id logger.error(error_msg)
return invoice_data_object return Result(valid=False, error_type=ResultError.BILLING_CODE_EXISTS, data=billing_id, message=error_msg)
rejected_choices = [None, "", False] rejected_choices = [None, "", False]
if how_many_profiles in rejected_choices: if how_many_profiles in rejected_choices:
notification = "Missing profile quantity, to initiate payment" notification = "Missing profile quantity, to initiate payment"
logger.error(notification)
ticket_observer.notify("failed_input", subject=notification) ticket_observer.notify("failed_input", subject=notification)
invoice_data_object.add_error_code("invalid_quantity") return Result(valid=False, error_type=ResultError.INVALID_INPUT, message=notification)
return invoice_data_object
if not valid_profile_quantity(how_many_profiles): if not valid_profile_quantity(how_many_profiles):
notification = "Invalid profile quantity" notification = "Invalid profile quantity"
logger.error(notification)
ticket_observer.notify("failed_input", subject=notification) ticket_observer.notify("failed_input", subject=notification)
invoice_data_object.add_error_code("invalid_quantity") return Result(valid=False, error_type=ResultError.INVALID_INPUT, message="You've picked a currently unsupported profile quantity")
return invoice_data_object
if which_key in rejected_choices: if which_key in rejected_choices:
notification = "Missing key plan, to initiate payment" notification = "Missing key plan, to initiate payment"
logger.error(notification)
ticket_observer.notify("failed_input", subject=notification) ticket_observer.notify("failed_input", subject=notification)
invoice_data_object.add_error_code("no_keyplan") return Result(valid=False, error_type=ResultError.INVALID_INPUT, message="You didn't pick a key plan. It's either blank, none, or invalid.")
return invoice_data_object
# get & save the public key: # get & save the public key:
public_key_results = get_pub_key(which_key, connection_observer, "local") public_key_results = get_pub_key(which_key, connection_observer, "local")
if isinstance(public_key_results, dict): if not isinstance(public_key_results, dict):
notification = f"Invalid Key chosen or the server is down for that key."
logger.error(notification)
ticket_observer.notify("failed_input", subject=notification)
# invoice_data_object.add_error_code("no_pub_key")
# return invoice_data_object
status = public_key_results.get("valid", False) status = public_key_results.get("valid", False)
if status == False: if status == False:
message = public_key_results.get( message = public_key_results.get(
"message", "Please fix before you continue" "message", "Please fix before you continue"
) )
error_code = public_key_results.get("error_code", "No error_code") error_code = public_key_results.get("error_code", "No error_code")
logger.debug(f"error_code: {error_code}") notification = f"Connection Issues or Invalid Key chosen. {error_code}"
invoice_data_object.add_error_code(error_code) logger.error(notification)
notification = f"Connection Issues or Invalid Key chosen. {message}"
ticket_observer.notify("failed_input", subject=notification) ticket_observer.notify("failed_input", subject=notification)
return Result(valid=False, error_type=ResultError.CONNECTION, message="There were connection issues with getting the right key.")
return invoice_data_object
else:
notification = f"Invalid Key chosen or the server is down for that key."
ticket_observer.notify("failed_input", subject=notification)
invoice_data_object.add_error_code("no_pub_key")
return invoice_data_object
# In this case, the controller is going to send the whole JSON payload to the service, # In this case, the controller is going to send the whole JSON payload to the service,
# instead of passing 4 values seperately. # instead of passing 4 values seperately.
@ -118,51 +113,40 @@ def initiate_payment(
"how_many_profiles": how_many_profiles, "how_many_profiles": how_many_profiles,
} }
# controller sends to the service: try:
result = save_and_send_intitial_billing( return save_and_send_intitial_billing(
payload, connection_observer, invoice_data_object payload=payload,
connection_observer=connection_observer
) )
if isinstance(result, ApiResponse):
logger.error(result.message)
if not result.valid:
invoice_data_object.add_error_code("connection_error")
return invoice_data_object
else:
logger.error("Critical Error with TicketPayController recieving a valid ApiResponse object.")
if result == False or result == None:
invoice_data_object.add_error_code("failed_save")
return invoice_data_object
except ValueError as e: except ValueError as e:
error_msg = "Invalid Data." if ticket_observer:
ticket_observer.notify("failed_input", subject=error_msg) ticket_observer.notify("failed_input", subject=str(e))
invoice_data_object.add_error_code("invalid_data") # invoice_data_object.add_error_code("invalid_data")
return invoice_data_object # return invoice_data_object
return Result(valid=False, error_type=ResultError.INVALID_INPUT, message=str(e))
except NetworkingError as e: except ConnectionError as e:
error_msg = f"NetworkingError: {e}" error_msg = "There were connection issues with getting the right key."
logger.error(error_msg, exc_info=True) logger.error(error_msg, exc_info=True)
ticket_observer.notify("connection_error", subject=error_msg) if ticket_observer:
invoice_data_object.add_error_code("connection_error") ticket_observer.notify("connection_error", subject=str(e))
return invoice_data_object # invoice_data_object.add_error_code("connection_error")
return Result(valid=False, error_type=ResultError.CONNECTION, message=error_msg)
except ServerSideError as e: # except ServerSideError as e:
error_msg = f"ServerSideError: {e}" # error_msg = f"ServerSideError: {e}"
logger.error(error_msg, exc_info=True) # logger.error(error_msg, exc_info=True)
ticket_observer.notify("failed_output", subject=error_msg) # ticket_observer.notify("failed_output", subject=error_msg)
invoice_data_object.add_error_code("server_error") # invoice_data_object.add_error_code("server_error")
return invoice_data_object # return invoice_data_object
except Exception as e:
error_msg = f"Error: {e}"
logger.error(error_msg, exc_info=True)
ticket_observer.notify("unknown_error", subject=error_msg)
invoice_data_object.add_error_code("unknown_error")
return invoice_data_object
# except Exception as e:
# error_msg = f"Error: {e}"
# logger.error(error_msg, exc_info=True)
# ticket_observer.notify("unknown_error", subject=error_msg)
# invoice_data_object.add_error_code("unknown_error")
# return invoice_data_object
############### ###############
@ -188,8 +172,6 @@ def check_if_paid(
url = make_url(which_endpoint) url = make_url(which_endpoint)
# literally send: # literally send:
# api_reply_object = send_data_to_server(payload, url, connection_observer)
api_reply_object = connect.single_endpoint( api_reply_object = connect.single_endpoint(
method="post", method="post",
url=url, url=url,
@ -202,16 +184,6 @@ def check_if_paid(
logger.error(f"[TICKET PayController] 2nd Post Request inside ticketpay controller had a {error_msg}") logger.error(f"[TICKET PayController] 2nd Post Request inside ticketpay controller had a {error_msg}")
return {"valid": False, "message": error_msg} return {"valid": False, "message": error_msg}
# if not api_reply_object.valid:
# api_reply_object = solve_api_problems(
# api_reply_object=api_reply_object,
# get_or_post="post",
# url=url,
# payload=payload,
# connection_observer=connection_observer,
# client_observer=None
# )
# return the payload with GUI/CLI to interpret results: # return the payload with GUI/CLI to interpret results:
reply_dict = api_reply_object.data reply_dict = api_reply_object.data
logger.debug(f"[TICKET PayController] We have a valid reply from the API inside ticketpay controller of {reply_dict}") logger.debug(f"[TICKET PayController] We have a valid reply from the API inside ticketpay controller of {reply_dict}")

View file

@ -7,13 +7,12 @@ if TYPE_CHECKING:
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.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.models.session.SessionProfile import SessionProfile
from core.models.system.SystemProfile import SystemProfile
from core.errors.logger import logger from core.errors.logger import logger
# generic # generic
@ -49,6 +48,7 @@ def prepare_tickets(
# make sure it's a number: # make sure it's a number:
if not isinstance(how_many_profiles, int): if not isinstance(how_many_profiles, int):
if ticket_observer is not None:
ticket_observer.notify("failed_input", None) ticket_observer.notify("failed_input", None)
return {"valid": False, "error_code": "failed_input"} return {"valid": False, "error_code": "failed_input"}
@ -58,16 +58,19 @@ def prepare_tickets(
# (which is based on locally saved data from the previous step): # (which is based on locally saved data from the previous step):
how_many_ordered = get_how_many_profiles_were_ordered() how_many_ordered = get_how_many_profiles_were_ordered()
if how_many_profiles != how_many_ordered: if how_many_profiles != how_many_ordered:
if ticket_observer is not None:
ticket_observer.notify("failed_input", None) ticket_observer.notify("failed_input", None)
return {"valid": False, "error_code": "failed_input"} return {"valid": False, "error_code": "failed_input"}
# make sure this guy has a public key to verify against: # make sure this guy has a public key to verify against:
does_he_have_public_key = make_sure_pub_key_exists(connection_observer) does_he_have_public_key = make_sure_pub_key_exists(connection_observer)
if does_he_have_public_key == False: if does_he_have_public_key == False:
if ticket_observer is not None:
ticket_observer.notify("failed_input", None) ticket_observer.notify("failed_input", None)
return {"valid": False, "error_code": "failed_input"} return {"valid": False, "error_code": "failed_input"}
notification = "Preparing Cryptography Locally" notification = "Preparing Cryptography Locally"
if ticket_observer is not None:
ticket_observer.notify("preparing", subject=notification) ticket_observer.notify("preparing", subject=notification)
# ok now we have the pre-reqs, let's use this high level orchestrator, # ok now we have the pre-reqs, let's use this high level orchestrator,
@ -89,6 +92,7 @@ def prepare_tickets(
if prep_results["valid"] == True: if prep_results["valid"] == True:
notification = f"Done! All Tickets Ready!" notification = f"Done! All Tickets Ready!"
if ticket_observer is not None:
ticket_observer.notify("preparing", subject=notification) ticket_observer.notify("preparing", subject=notification)
return prep_results return prep_results
@ -97,33 +101,43 @@ def prepare_tickets(
failed_validations = prep_results.get("failed_validations", None) failed_validations = prep_results.get("failed_validations", None)
if failed_validations: if failed_validations:
notification = f"Error with Ticket Preparation or Verification!" notification = f"Error with Ticket Preparation or Verification!"
if ticket_observer is not None:
ticket_observer.notify("preparing", subject=notification) ticket_observer.notify("preparing", subject=notification)
return prep_results return prep_results
notification = f"Error with Ticket Preparation or Verification!" notification = f"Error with Ticket Preparation or Verification!"
if ticket_observer is not None:
ticket_observer.notify("preparing", subject=notification) ticket_observer.notify("preparing", subject=notification)
return prep_results return prep_results
# No profile required
def respawn_billing_code_into_ticket( def respawn_billing_code_into_ticket(
profile: Union[SessionProfile, SystemProfile], billing_code: str,
which_ticket: int, which_ticket: int,
ticket_observer: TicketObserver, ticket_observer: TicketObserver,
connection_observer: ConnectionObserver connection_observer: ConnectionObserver
) -> Result: ) -> Result:
""" """
Purpose:
Invalidates a billing code, & produces a valid unused ticket
Requires:
Valid Billing Code.
Any Ticket slot folder, (but not an actual ticket there).
No profile required.
Method:
Converts a valid VPN billing code, into a valid unused ticket. Converts a valid VPN billing code, into a valid unused ticket.
At the cost of the server expiring the billing code immediately. At the cost of the server expiring the billing code immediately.
And the ticket slot is used (wipes existing used ticket)
This has the effect of "wiping" the billing code, and starting over. This has the effect of "wiping" the billing code, and starting over.
The new ticket can be applied to any other profile now. The new ticket can be applied to any other profile now.
Called by:
ticket_respawn's respawn_profile
""" """
# extract billing code: #################################################
billing_code = profile.subscription.billing_code # PREP PRE-REQS
#################################################
if not billing_code:
return Result(valid=False, error_type=ResultError.MISSING_DATA, message="You lack a valid subscription on the profile slot is being respawned")
if not which_ticket: if not which_ticket:
return Result(valid=False, error_type=ResultError.MISSING_DATA, message="Missing which ticket slot is being respawned") return Result(valid=False, error_type=ResultError.MISSING_DATA, message="Missing which ticket slot is being respawned")
@ -133,6 +147,9 @@ def respawn_billing_code_into_ticket(
except: except:
return Result(valid=False, error_type=ResultError.INVALID_INPUT, message="Ticket slot must be a number.") return Result(valid=False, error_type=ResultError.INVALID_INPUT, message="Ticket slot must be a number.")
#################################################
# SEND TO SERVER
#################################################
ticket_result = prepare_tickets( ticket_result = prepare_tickets(
how_many_profiles=1, how_many_profiles=1,
ticket_observer=ticket_observer, ticket_observer=ticket_observer,
@ -141,6 +158,9 @@ def respawn_billing_code_into_ticket(
billing_code=billing_code billing_code=billing_code
) )
#################################################
# EVALUATE RESULTS
#################################################
if not isinstance(ticket_result, dict): if not isinstance(ticket_result, dict):
return Result(valid=False, error_type=ResultError.UNKNOWN, message=f"Error! The prepare_tickets function returned an invalid format of {type(ticket_result)} when it should be a dict. This is being recieved in the respawn_ticket function.") return Result(valid=False, error_type=ResultError.UNKNOWN, message=f"Error! The prepare_tickets function returned an invalid format of {type(ticket_result)} when it should be a dict. This is being recieved in the respawn_ticket function.")
@ -148,21 +168,26 @@ def respawn_billing_code_into_ticket(
if not valid: if not valid:
notification = f"Error in Respawn Prep!" notification = f"Error in Respawn Prep!"
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)
logger.info("Got a valid result from the respawn's prepare_tickets. Now wiping the profile's subscription.") #################################################
# WIPE LOCAL TICKET DATA
#################################################
wiped_sub = ticket_tracker.wipe_one_ticket_sub(which_ticket)
logger.info(f"Did local ticket tracker wipe? {wiped_sub}")
# Now that the billing code has been wiped by the server, wipe it locally: #################################################
profile.subscription = None # NOTIFY & RETURN
#################################################
# save the ticket, notification = f"Respawn Done for Ticket Slot {which_ticket}!"
profile.ticket = which_ticket if ticket_observer is not None:
profile.save()
notification = f"Respawn Done for Profile {profile.id}!"
ticket_observer.notify("preparing", subject=notification) ticket_observer.notify("preparing", subject=notification)
return Result(valid=True, message=f"It worked. Respawn Done for Profile {profile.id}! It's completely wiped and you now have a valid ticket at slot {which_ticket}")
update_msg = f"It worked. Respawn Done! The billing code is no longer valid and you now have a valid ticket at slot {which_ticket}"
logger.info(update_msg)
return Result(valid=True, message=update_msg)
def prep_error_result(ticket_result: dict): def prep_error_result(ticket_result: dict):

View file

@ -14,6 +14,8 @@ from core.services.prepare_tickets.ticket_tracker import (
get_data_for_a_single_ticket, get_data_for_a_single_ticket,
does_ticket_tracker_exist, does_ticket_tracker_exist,
) )
from core.services.prepare_tickets import setup_ticket_tracker
from core.services.helpers.does_ticket_file_exist import does_ticket_file_exist from core.services.helpers.does_ticket_file_exist import does_ticket_file_exist
from core.services.helpers.get_value_from_config import get_value_from_config from core.services.helpers.get_value_from_config import get_value_from_config
from core.utils.basic_operations.does_file_exist import does_file_exist from core.utils.basic_operations.does_file_exist import does_file_exist
@ -146,8 +148,15 @@ def use_ticket(
profile: Optional[Union[SessionProfile, SystemProfile]] = None, profile: Optional[Union[SessionProfile, SystemProfile]] = None,
) -> dict: ) -> dict:
print(f"Calling the use_ticket function! with which_ticket as {which_ticket}")
which_ticket = str(which_ticket) # type: ignore which_ticket = str(which_ticket) # type: ignore
print(f"were in use_ticket with ticket {which_ticket}")
if profile:
print(f"we have a profile of {profile}")
# does the ticket's file exist: # does the ticket's file exist:
ticket_exists = does_ticket_file_exist(which_ticket) ticket_exists = does_ticket_file_exist(which_ticket)
if ticket_exists == False: if ticket_exists == False:
@ -163,17 +172,19 @@ def use_ticket(
return {"valid": False, "message": error_msg} return {"valid": False, "message": error_msg}
# is the ticket used? # is the ticket used?
try:
status, location, subscription = get_data_for_a_single_ticket(which_ticket) # first, get values,
if status == "used": # try:
error_msg = f"Ticket is already tied to {location} with the subscription {subscription}" # status, location, subscription = get_data_for_a_single_ticket(which_ticket)
ticket_observer.notify("failed_input", subject=error_msg) # if status == "used":
return {"valid": False, "message": error_msg} # error_msg = f"Ticket is already tied to {location} with the subscription {subscription}"
except ValueError as e: # ticket_observer.notify("failed_input", subject=error_msg)
error_msg = f"Your local ticket tracker has no value for ticket {which_ticket}" # return {"valid": False, "message": error_msg}
logger.error(error_msg) # except ValueError as e:
logger.error(str(e)) # error_msg = f"Your local ticket tracker has no value for ticket {which_ticket}"
return {"valid": False, "message": error_msg} # logger.error(error_msg)
# logger.error(str(e))
# return {"valid": False, "message": error_msg}
# the actual work here, everything else is just handling: # the actual work here, everything else is just handling:
ticket_observer.notify("connecting", "Connecting..") ticket_observer.notify("connecting", "Connecting..")

View file

@ -12,11 +12,15 @@ class ResultError(Enum):
MISSING_FILE = "missing_file" MISSING_FILE = "missing_file"
MISSING_DEPENDENCY = "missing_dependency" MISSING_DEPENDENCY = "missing_dependency"
MISSING_DATA = "missing_data" MISSING_DATA = "missing_data"
MISSING_SOFTWARE = "missing_software"
FILE_SYSTEM = "filesystem" FILE_SYSTEM = "filesystem"
CONNECTION = "connection" CONNECTION = "connection"
DATABASE = "database" DATABASE = "database"
PERMISSION = "permission" PERMISSION = "permission"
SUBSCRIPTION = "subscription" SUBSCRIPTION = "subscription"
TICKET = "ticket"
ALREADY_EXISTS = "already_exists"
BILLING_CODE_EXISTS = "billing_code_exists"
PROCESS_GOT_KILLED = "process_got_killed" PROCESS_GOT_KILLED = "process_got_killed"
PROCESS_WONT_START = "process_wont_start" PROCESS_WONT_START = "process_wont_start"
PROCESS_MISMATCH = "process_mismatch" PROCESS_MISMATCH = "process_mismatch"

View file

@ -1,13 +1,13 @@
from pydantic import BaseModel from pydantic import BaseModel
from core.errors.get_error_msg import get_error_msg from core.errors.get_error_msg import get_error_msg
class TicketInvoice(BaseModel): class TicketInvoice(BaseModel):
temp_billing_code: str | None = None valid: bool = True
payment_type: str | None = None temp_billing_code: str
selected_currency: str | None = None payment_type: str = "crypto"
due_amount: float | None = None selected_currency: str = None
address: str | None = None due_amount: float
address: str
final_error_msg: str | None = None final_error_msg: str | None = None
error_code: str | None = None error_code: str | None = None

View file

@ -7,6 +7,7 @@ from core.errors.logger import logger
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from sqlalchemy import select from sqlalchemy import select
from typing import Optional from typing import Optional
import random
@safe_db_operation @safe_db_operation
def execute_application_sql(application_code: str, version_number: str, session: Session) -> DatabaseOperation: def execute_application_sql(application_code: str, version_number: str, session: Session) -> DatabaseOperation:
@ -37,3 +38,34 @@ def execute_get_all(application: Optional[Application] = None, session: Session
result = session.execute(query).scalars().all() result = session.execute(query).scalars().all()
return DatabaseOperation(valid=True, data=result) return DatabaseOperation(valid=True, data=result)
@safe_db_operation
def get_all_supported_apps(session: Session) -> DatabaseOperation:
"""Get all supported apps"""
data = session.query(ApplicationVersion).filter(
ApplicationVersion.format_revision == 2
).all()
return DatabaseOperation(valid=True, data=data)
def get_random_installed_app() -> Optional[ApplicationVersion]:
database_object = get_all_supported_apps()
if not database_object.valid:
logger.error(f"Critical Database error with fetching all supported apps. {database_object.error_type} with {database_object.message}")
return None
list_of_data = list(database_object.data)
while len(list_of_data) >= 1:
random_item = random.choice(list_of_data)
print(f"random_item is {random_item}")
if random_item.installed:
print("it is installed")
return random_item
else:
id_of_random_item = random_item.id
list_of_data.remove(random_item)
return None

View file

@ -8,6 +8,8 @@ from core.errors.logger import logger
from sqlalchemy import select from sqlalchemy import select
from sqlalchemy.orm import joinedload from sqlalchemy.orm import joinedload
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from typing import Optional
import random
@safe_db_operation @safe_db_operation
def execute_location_sql(country_code: str, city_code: str, session: Session) -> DatabaseOperation: def execute_location_sql(country_code: str, city_code: str, session: Session) -> DatabaseOperation:
@ -24,8 +26,35 @@ def get_profile_location_data(country_code: str, city_code: str) -> Location:
if location_object.valid: if location_object.valid:
return location_object.data return location_object.data
else: else:
critical_error = f"[BaseProfile] Got invalid SQL Query which could not be solved by the wrapper, with error message {location_object.message} and type {location_object.error_type}" critical_error = f"Got invalid SQL Query which could not be solved by the wrapper, with error message {location_object.message} and type {location_object.error_type}"
logger.error(critical_error) logger.error(critical_error)
print(critical_error) print(critical_error)
return None return None
@safe_db_operation
def execute_all_locations(session: Session) -> DatabaseOperation:
return session.query(Location).all()
def get_random_location(filter_out_list: Optional[list] = None) -> Location:
location_object = execute_all_locations()
if not location_object.valid:
critical_error = f"Got invalid SQL Query which could not be solved by the wrapper, with error message {location_object.message} and type {location_object.error_type}"
logger.error(critical_error)
return None
list_of_choices = location_object.data
while True:
random_pick = random.choice(list_of_choices)
# SPOOF: to force assassin to be a certain location, this can be implemented as a feature later:
# if random_pick.id == 4:
# return random_pick
if not filter_out_list:
return random_pick
if random_pick not in filter_out_list:
return random_pick

View file

@ -22,6 +22,7 @@ class SessionProfile(BaseProfile):
application_version: Optional[ApplicationVersion] application_version: Optional[ApplicationVersion]
connection: Optional[SessionConnection] = None connection: Optional[SessionConnection] = None
ticket: Optional[int] = None ticket: Optional[int] = None
assassin: Optional[bool] = False
def has_connection(self): def has_connection(self):
return self.connection is not None return self.connection is not None
@ -39,7 +40,7 @@ class SessionProfile(BaseProfile):
if 'location' in self._get_dirty_keys(): if 'location' in self._get_dirty_keys():
self.__delete_proxy_configuration() self.__delete_proxy_configuration()
self.__delete_wireguard_configuration() self.delete_wireguard_configuration()
# === APPLICATION === # === APPLICATION ===
app_version_dict = self.application_version.convert_to_dict() app_version_dict = self.application_version.convert_to_dict()
@ -95,7 +96,7 @@ class SessionProfile(BaseProfile):
def address_security_incident(self): def address_security_incident(self):
super().address_security_incident() super().address_security_incident()
self.__delete_wireguard_configuration() self.delete_wireguard_configuration()
def determine_timezone(self): def determine_timezone(self):
@ -124,5 +125,5 @@ class SessionProfile(BaseProfile):
def __delete_proxy_configuration(self): def __delete_proxy_configuration(self):
Path(self.get_proxy_configuration_path()).unlink(missing_ok=True) Path(self.get_proxy_configuration_path()).unlink(missing_ok=True)
def __delete_wireguard_configuration(self): def delete_wireguard_configuration(self):
Path(self.get_wireguard_configuration_path()).unlink(missing_ok=True) Path(self.get_wireguard_configuration_path()).unlink(missing_ok=True)

View file

@ -13,6 +13,7 @@ import subprocess
class SystemProfile(BaseProfile): class SystemProfile(BaseProfile):
connection: Optional[SystemConnection] connection: Optional[SystemConnection]
ticket: Optional[int] = None ticket: Optional[int] = None
assassin: Optional[bool] = False
def get_system_config_path(self): def get_system_config_path(self):
filepath = self.__get_system_config_path(self.id) filepath = self.__get_system_config_path(self.id)

View file

@ -0,0 +1,166 @@
from core.models.orm_calls.application_version_calls import get_random_installed_app
from core.models.orm_calls.location_calls import get_random_location
from core.services.assassin.screen_size import pick_random_resolution
from core.services.assassin import ticket_respawn
from core.services.assassin.location_tools import get_systemwide_location
from core.controllers.tickets.UseTicketController import use_ticket
from core.models.Result import Result, ResultError
from core.models.session.SessionProfile import SessionProfile
from core.models.Subscription import Subscription
from core.models.BaseProfile import ProfileType
from core.models.orm_models.Location import Location
from core.observers.ConnectionObserver import ConnectionObserver
from core.observers.TicketObserver import TicketObserver
from core.Errors import MissingSubscriptionError
from core.errors.logger import logger
def create(
profile: SessionProfile,
max_resolution: tuple,
ticket_observer: TicketObserver,
connection_observer: ConnectionObserver
) -> Result:
"""
Purpose:
Gets the requirements for an assassin profile, which is returned as an object.
Rank:
Feature's King Orchestrator
Called by:
ProfileController
"""
if not profile:
return Result(valid=False, error_type=ResultError.INVALID_INPUT, message="Requires a profile")
if not profile.assassin:
return Result(valid=False, error_type=ResultError.INVALID_INPUT, message="Requires an assassin profile")
# We print for the Assassin to avoid logs.
print(f"Begun creating Assassin for profile id {profile.id}")
# APPLICATION
random_app = get_random_installed_app()
print(f"random_app is {random_app}")
if not random_app:
return Result(valid=False, error_type=ResultError.MISSING_SOFTWARE, message="You need to install more browsers to have available choices for the Assassin. Please sync & download..")
# LOCATION
systemwide_location = get_systemwide_location()
filter_out_list = [systemwide_location]
random_location = get_random_location(filter_out_list=filter_out_list)
print(f"random_location is {random_location}")
if not random_app:
return Result(valid=False, error_type=ResultError.NEED_SYNC, message="You need to sync to get location choices for the Assassin.")
# SCREEN SIZE:
if not max_resolution:
return Result(valid=False, error_type=ResultError.INVALID_INPUT, message="Could not create a screen size without a max_resolution")
random_screen_size = pick_random_resolution(max_resolution)
print(f"random_screen_size is {random_screen_size}")
if not random_screen_size:
return Result(valid=False, error_type=ResultError.INVALID_INPUT, message=f"Could not create a screen size with the given inputs of {str(max_resolution)}")
# use underlying profile's connection:
assassins_connection = profile.connection
print(f"assassins_connection is {assassins_connection}")
# SETUP ASSASSIN:
assassin = SessionProfile(
id=profile.id,
name="assassin_ON",
type=ProfileType.SESSION,
location=random_location,
resolution=random_screen_size,
application_version=random_app,
connection=assassins_connection,
subscription=None,
assassin=True
)
# DELETE the pre-existing WG config. Even though this is a redundant duplicate of the turn off function,
# we don't know if assassin mode was added to a profile that already had a wg config for non-assassin use.
profile.delete_wireguard_configuration()
print("deleted the assassin's pre-existing wg config.")
# returns Result after using the sub:
return use_ticket_get_sub(
profile=assassin,
random_location=random_location,
connection_observer=connection_observer,
ticket_observer=ticket_observer
)
def use_ticket_get_sub(
profile: SessionProfile,
random_location: Location,
connection_observer: ConnectionObserver,
ticket_observer: TicketObserver
) -> Result:
print("using a ticket for the assassin")
# GET TICKET FROM LOCAL FILES
which_ticket = ticket_respawn.get_by_any_means(profile)
print(f"assassin's which_ticket is {which_ticket}")
if not which_ticket:
raise MissingSubscriptionError
# USE TICKET
print("using ticket..")
ticket_result = use_ticket(
which_ticket=which_ticket,
which_location=random_location.id,
ticket_observer=ticket_observer,
connection_observer=connection_observer
)
print(f"ticket_result is {ticket_result}")
valid = ticket_result.get("valid", False)
if not valid:
return Result(valid=False, error_type=ResultError.INVALID_API_REPLY)
billing_code = ticket_result.get("billing_code", False)
if not billing_code:
return Result(valid=False, error_type=ResultError.INVALID_API_REPLY)
assassins_subscription = Subscription(billing_code=billing_code)
profile.subscription = assassins_subscription
profile.ticket = which_ticket
profile.save()
return Result(valid=True, data=profile)
def wipe(
profile: SessionProfile,
ticket_observer: TicketObserver,
connection_observer: ConnectionObserver
) -> Result:
####################################
# RESPAWN TICKET
####################################
print("respawning assassin")
respawned = ticket_respawn.respawn_profile(
profile=profile,
ticket_observer=ticket_observer,
connection_observer=connection_observer
)
if not respawned:
logger.error("Serious issue with respawning the ticket!")
return Result(valid=False, error_type=ResultError.TICKET, message=f"Could NOT respawn the ticket for profile {profile.id}")
print("respawn worked, now saving..")
profile.name = "assassin"
profile.subscription = None
profile.ticket = None
profile.save()
# DELETE the pre-existing WG config:
profile.delete_wireguard_configuration()
print("deleted the pre-existing wg config")
return Result(valid=True)

View file

@ -0,0 +1,20 @@
from core.controllers.SystemStateController import SystemStateController
from core.models.BaseProfile import BaseProfile as Profile
def get_systemwide_location() -> int:
current_state = SystemStateController.get()
if not current_state:
return None
systemwide_id = current_state.profile_id
if not systemwide_id:
return None
systemwide_profile = Profile.find_by_id(systemwide_id)
if not systemwide_profile:
return None
return systemwide_profile

View file

@ -0,0 +1,21 @@
from core.errors.logger import logger
import random
def pick_random_resolution(max_resolution: str) -> str:
if "x" in max_resolution:
max_width, max_height = max_resolution.split("x")
else:
logger.error(f"Developer Error: Invalid format of max_resolution being {max_resolution}, going with the defaults")
max_height = 800
max_width = 800
min_resolution = 650
width = random.randint(min_resolution, int(max_width))
height = random.randint(min_resolution, int(max_height))
final_resolution = f"{width}x{height}"
return final_resolution

View file

@ -0,0 +1,132 @@
from core.controllers.tickets.TicketPrepController import respawn_billing_code_into_ticket
from core.services.prepare_tickets.ticket_tracker import find_ticket_by_sub
from core.services.prepare_tickets.ticket_tracker import get_all_unused_tickets
from core.models.session.SessionProfile import SessionProfile
from core.models.system.SystemProfile import SystemProfile
from core.observers.ConnectionObserver import ConnectionObserver
from core.observers.TicketObserver import TicketObserver
from core.models.Result import Result, ResultError
from core.errors.logger import logger
# generic
from typing import Union
import random
# This is in the Assassin folder, but it can be used for anything (such as respawn on a systemwide non-assassin profile).
# Why: We keep it in Assassin, to remind us to do print statements instead of log. Assassin is ephemeral.
def respawn_profile(
profile: Union[SessionProfile, SystemProfile],
ticket_observer: TicketObserver = None,
connection_observer: ConnectionObserver = None,
) -> Result:
"""
Purpose:
Voids a subscription for a profile and respawns the ticket for it.
Requires:
A profile. This is NOT just a ticket.
Rank:
Controller
Called by:
ProfileController.destroy
"""
which_ticket = profile.ticket # which slot is to be used for this.
try:
billing_code = profile.subscription.billing_code
except Exception as e:
logger.error(f"FAILED to get the billing code: {str(e)}")
billing_code = None
# 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")
if not billing_code:
return Result(valid=False, error_type=ResultError.SUBSCRIPTION, message="You lack a valid subscription on the profile slot is being respawned")
which_ticket = find_ticket_by_sub(billing_code)
# Regardless of how it was acquired,
if not which_ticket:
return Result(valid=False, error_type=ResultError.TICKET, message="This profile is not associated with a ticket. And/or you lack a ticket slot to respawn.")
notification = f"Respawn Started for Ticket {which_ticket}"
logger.info(notification)
if ticket_observer is not None:
ticket_observer.notify("preparing", subject=notification)
print(f"Sending to respawn_billing_code_into_ticket with billing_code: {billing_code} and ticket: {which_ticket}")
respawn_result = respawn_billing_code_into_ticket(
billing_code=billing_code,
which_ticket=which_ticket,
ticket_observer=ticket_observer,
connection_observer=connection_observer
)
if not respawn_result.valid:
notification = "Error with Ticket Respawn!"
logger.error(f"{notification} {respawn_result.error_type} with message: {respawn_result.message}")
if ticket_observer is not None:
ticket_observer.notify("preparing", subject=notification)
return respawn_result
logger.info(f"We have a valid ticket respawn, but not yet dealt with profile {profile.id}'s billing id..")
notification = "Ticket Respawned!"
logger.info(notification)
if ticket_observer is not None:
ticket_observer.notify("preparing", subject=notification)
# Now that the billing code has been wiped by the server, wipe it locally:
print("We are now wiping the billing code for this profile.")
profile.subscription = None
# save the ticket,
profile.ticket = which_ticket
profile.save()
return Result(valid=True, message="This profile fully respawned and had it's billing code wiped.")
def get_by_any_means(profile: SessionProfile) -> int | None:
"""
Purpose:
Get the Ticket for a Profile by any means.
1) Try the profile itself
2) Search the ticket tracker
3) See if ANY unused tickets are available
Rank:
Mini-Orchestrator
Called by:
respawn_profile
"""
which_ticket = None
# Ticket tied to profile?
if profile:
which_ticket = profile.ticket
if which_ticket:
print(f"we found a ticket for this profile of {which_ticket}")
return which_ticket
# Ticket used with it?
if profile.subscription:
target_subscription = profile.subscription.billing_code
which_ticket = find_ticket_by_sub(target_subscription)
if which_ticket:
return which_ticket
else:
print("skipping there is no sub yet.")
# Any available ticket?
print("checking ANY available tickets.")
availablity_dict = get_all_unused_tickets()
valid = availablity_dict.get("valid", False)
if not valid:
return False
unused_tickets = availablity_dict.get("data", False)
print(f"all unused_tickets is {unused_tickets}")
if not unused_tickets or len(unused_tickets) == 0:
return False
print("picking a random ticket")
which_ticket = random.choice(unused_tickets)
return which_ticket

View file

@ -6,6 +6,8 @@ from core.services.networking.systemwide.encrypted_proxy.configure_singbox impor
from core.services.networking.systemwide.encrypted_proxy.singbox_runner import start_singbox from core.services.networking.systemwide.encrypted_proxy.singbox_runner import start_singbox
from core.models.Result import Result, ResultError from core.models.Result import Result, ResultError
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
from core.observers.TicketObserver import TicketObserver
from core.services.assassin import assassin_tools
# If refactored to enums: # If refactored to enums:
# from core.models.session.SessionConnection import SessionConnectionTypes # from core.models.session.SessionConnection import SessionConnectionTypes
@ -24,6 +26,7 @@ from core.controllers.ConnectionController import ConnectionController
from core.models.BaseProfile import ProfileType from core.models.BaseProfile import ProfileType
from core.observers.ConnectionObserver import ConnectionObserver from core.observers.ConnectionObserver import ConnectionObserver
from core.controllers.SystemStateController import SystemStateController from core.controllers.SystemStateController import SystemStateController
from core.observers.TicketObserver import TicketObserver
import os import os
@ -31,11 +34,13 @@ def establish_connection(
profile: Union[SessionProfile, SystemProfile], profile: Union[SessionProfile, SystemProfile],
ignore: tuple[type[Exception]] = (), ignore: tuple[type[Exception]] = (),
connection_observer: Optional[ConnectionObserver] = None, connection_observer: Optional[ConnectionObserver] = None,
ticket_observer: Optional[TicketObserver] = None # Passed in only for assassin's link to tickets in ConnectionController.establish_session_connection
): ):
"""Establish a connection for the given profile.""" """Establish a connection for the given profile."""
logger.info(f"[CONNECTION] Checking subscription..") logger.info(f"[CONNECTION] Checking subscription..")
activate_subscription(profile, connection_observer) if not profile.assassin: # Note: Assassin does it's own billing. This would be redundant
activate_subscription(profile=profile, connection_observer=connection_observer)
# ========================================= # =========================================
# HYSTERIA2 & VLESS # HYSTERIA2 & VLESS
@ -59,13 +64,13 @@ def establish_connection(
ProfileType.SYSTEM: establish_system_connection, ProfileType.SYSTEM: establish_system_connection,
}[profile.type] }[profile.type]
return _establish_with_renegotiation(profile, establish_fn, ignore, connection_observer) return _establish_with_renegotiation(profile, establish_fn, ignore, connection_observer, ticket_observer)
def launch_encrypted_proxy( def launch_encrypted_proxy(
profile: Union[SessionProfile, SystemProfile], profile: Union[SessionProfile, SystemProfile],
connection_observer: Optional[ConnectionObserver], connection_observer: Optional[ConnectionObserver]
): ):
""" """
Launch & Setup an Encrypted proxy. Launch & Setup an Encrypted proxy.
@ -130,6 +135,7 @@ def _ensure_proxy_configured(
if not profile.has_proxy_configuration(): if not profile.has_proxy_configuration():
logger.info(f"[CONNECTION] You need a new proxy configuration") logger.info(f"[CONNECTION] You need a new proxy configuration")
if not profile.assassin:
activate_subscription(profile, connection_observer) activate_subscription(profile, connection_observer)
logger.info(f"[CONNECTION] Getting a proxy config from web service..") logger.info(f"[CONNECTION] Getting a proxy config from web service..")
@ -147,7 +153,7 @@ def _ensure_proxy_configured(
def _ensure_wireguard_configured( def _ensure_wireguard_configured(
profile: Union[SessionProfile, SystemProfile], profile: Union[SessionProfile, SystemProfile],
connection_observer: Optional[ConnectionObserver], connection_observer: Optional[ConnectionObserver]
): ):
"""Setup WireGuard config if needed.""" """Setup WireGuard config if needed."""
@ -164,6 +170,7 @@ def _ensure_wireguard_configured(
except ConnectionTerminationError as e: except ConnectionTerminationError as e:
logger.error(f"[CONNECTION] Previous Systemwide Wireguard connection could not be disabled. {e}") logger.error(f"[CONNECTION] Previous Systemwide Wireguard connection could not be disabled. {e}")
if not profile.assassin:
activate_subscription(profile, connection_observer) activate_subscription(profile, connection_observer)
logger.info(f"[CONNECTION] Checking if the profile already has a wg config..") logger.info(f"[CONNECTION] Checking if the profile already has a wg config..")
if not profile.has_wireguard_configuration(): if not profile.has_wireguard_configuration():
@ -176,6 +183,7 @@ def _establish_with_renegotiation(
establish_fn: Callable, establish_fn: Callable,
ignore: tuple[type[Exception]], ignore: tuple[type[Exception]],
connection_observer: Optional[ConnectionObserver], connection_observer: Optional[ConnectionObserver],
ticket_observer: Optional[TicketObserver] = None
): ):
"""Attempt connection, renegotiate WireGuard once if needed.""" """Attempt connection, renegotiate WireGuard once if needed."""
try: try:
@ -185,7 +193,7 @@ def _establish_with_renegotiation(
if __should_renegotiate(profile): if __should_renegotiate(profile):
logger.info(f"[CONNECTION] Renegotiating a new wg key session with API..") logger.info(f"[CONNECTION] Renegotiating a new wg key session with API..")
register_wireguard_session(profile, connection_observer=connection_observer) register_wireguard_session(profile, connection_observer=connection_observer)
return establish_fn(profile, ignore=ignore, connection_observer=connection_observer) return establish_fn(profile, ignore=ignore, connection_observer=connection_observer, ticket_observer=ticket_observer)
raise ConnectionError('The connection could not be established.') raise ConnectionError('The connection could not be established.')
except FirewallError: except FirewallError:
logger.error(f"Failed to turn on the firewall, passing the error up..") logger.error(f"Failed to turn on the firewall, passing the error up..")
@ -194,6 +202,9 @@ def _establish_with_renegotiation(
def __should_renegotiate(profile: Union[SessionProfile, SystemProfile]): def __should_renegotiate(profile: Union[SessionProfile, SystemProfile]):
if profile.assassin:
return False
if not profile.has_subscription(): if not profile.has_subscription():
raise MissingSubscriptionError() raise MissingSubscriptionError()

View file

@ -32,10 +32,11 @@ def make_request(
if initial_result.valid: if initial_result.valid:
return initial_result return initial_result
logger.error(f"{method.upper()} request failed: {initial_result.error_type}") logger.error(f"{method.upper()} request failed: {initial_result.error_type} with retry strategy of {initial_result.backoff_strategy}")
# Immediate Retry # Immediate Retry
if initial_result.backoff_strategy == BackoffStrategy.RETRY_IMMEDIATE: if initial_result.backoff_strategy == BackoffStrategy.RETRY_IMMEDIATE:
logger.info("Trying again RETRY_IMMEDIATE")
time.sleep(3) time.sleep(3)
second_result = _make_request( second_result = _make_request(
method=method, method=method,
@ -43,8 +44,8 @@ def make_request(
client=client, client=client,
payload=payload, payload=payload,
billing_code=billing_code billing_code=billing_code
), )
logger.info(f"second_result validity is {second_result.valid} and type {str(second_result)}")
if second_result.valid: if second_result.valid:
return second_result return second_result

View file

@ -1,63 +1,69 @@
from core.models.invoice.TicketInvoice import TicketInvoice from core.models.invoice.TicketInvoice import TicketInvoice
from core.errors.exceptions import * # from core.errors.exceptions import ServerSideError
from core.errors.logger import logger from core.errors.logger import logger
from core.utils.basic_operations.write_or_read_from_json import update_json from core.utils.basic_operations.write_or_read_from_json import update_json
from core.Constants import Constants from core.Constants import Constants
from pydantic import ValidationError
# billing_folder = Constants.HV_TICKETING_CONFIG_HOME
filepath = f"{Constants.HV_TICKETING_CONFIG_HOME}/billing_choices.json"
def extract_payment_details( def extract_payment_details(server_reply: dict) -> TicketInvoice:
server_reply: dict, invoice_data_object: TicketInvoice print(f"server_reply is {server_reply}")
) -> TicketInvoice:
# Validate and create the Pydantic model
try: try:
temp_billing_code = server_reply.get("temp_billing_code", None) ticket_invoice = TicketInvoice.model_validate(server_reply)
except ValidationError as e:
field_mappings = { error_msg = f"Server's Reply is Missing {str(e)}"
"temp_billing_code": temp_billing_code, logger.error(error_msg)
"payment_type": server_reply.get("payment_type", "crypto"), raise ValueError(error_msg)
"selected_currency": server_reply.get("currency", None),
"due_amount": server_reply.get("crypto_amount", None),
"address": server_reply.get("crypto_address", None),
}
for field, value in field_mappings.items():
if value != None:
try:
setattr(invoice_data_object, field, value)
except TypeError as e:
invoice_data_object.add_validation_error(field, str(e))
else:
invoice_data_object.is_valid = False
invoice_data_object.add_validation_error(
field,
f"Server gave you a value of None for {field}, which is invalid.",
)
return invoice_data_object
# save the temp_billing_code: # save the temp_billing_code:
billing_folder = Constants.HV_TICKETING_CONFIG_HOME did_it_save = update_json(filepath, "temp_billing_code", ticket_invoice.temp_billing_code)
filepath = f"{billing_folder}/billing_choices.json"
did_it_save = update_json(filepath, "temp_billing_code", temp_billing_code) if did_it_save == True:
return ticket_invoice
if did_it_save == False:
invoice_data_object.is_valid = False
error_msg = ( error_msg = (
f"Error!!! It did not save the temp_billing_code {temp_billing_code}" f"Error!!! It did not save the temp_billing_code {ticket_invoice.temp_billing_code}"
) )
logger.error(error_msg, exc_info=True) logger.error(error_msg, exc_info=True)
print(error_msg) return False
raise MissingData(error_msg)
return invoice_data_object
except ServerSideError as e:
logger.error(f"ServerSideError: {e}", exc_info=True) # except ServerSideError as e:
invoice_data_object.add_error_code( # logger.error(f"ServerSideError: {e}", exc_info=True)
"Unable to extract payment details from the server's reply." # logger.error("Unable to extract payment details from the server's reply.")
) # return None
return invoice_data_object # except Exception:
except: # error_msg = "Unable to extract payment details from the server's reply."
error_msg = "Unable to extract payment details from the server's reply." # logger.error(f"From inside the extract payment details module, {error_msg}")
logger.error(f"From inside the extract payment details module, {error_msg}") # invoice_data_object.add_error_code(error_msg)
invoice_data_object.add_error_code(error_msg) # return None
return invoice_data_object
# temp_billing_code = server_reply.get("temp_billing_code", None)
# field_mappings = {
# "temp_billing_code": temp_billing_code,
# "payment_type": server_reply.get("payment_type", "crypto"),
# "selected_currency": server_reply.get("currency", None),
# "due_amount": server_reply.get("crypto_amount", None),
# "address": server_reply.get("crypto_address", None),
# }
# for field, value in field_mappings.items():
# if value != None:
# try:
# setattr(invoice_data_object, field, value)
# except TypeError as e:
# invoice_data_object.add_validation_error(field, str(e))
# else:
# invoice_data_object.is_valid = False
# invoice_data_object.add_validation_error(
# field,
# f"Server gave you a value of None for {field}, which is invalid.",
# )
# return invoice_data_object

View file

@ -4,77 +4,87 @@ 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.networking.api_requests.step1_get_or_post import send_data_to_server
# from core.services.networking.api_requests.ApiResponseModel import ApiResponse, ErrorType
# from core.services.networking.api_requests.step5_solve_api_problems import solve_api_problems
from core.models.Result import Result, ResultError
from core.models.invoice.TicketInvoice import TicketInvoice from core.models.invoice.TicketInvoice import TicketInvoice
from core.utils.basic_operations.write_or_read_from_json import update_json
from core.services.networking.api_requests.step1_get_or_post import send_data_to_server from core.Constants import Constants
from core.services.networking.api_requests.ApiResponseModel import ApiResponse, ErrorType
from core.services.networking.api_requests.step5_solve_api_problems import solve_api_problems
from core.services.networking.httpx import connect from core.services.networking.httpx import connect
from core.services.networking.make_url import make_url from core.services.networking.make_url import make_url
from core.services.payment_phase.extract_payment_details import extract_payment_details # from core.services.payment_phase.extract_payment_details import extract_payment_details
from core.services.payment_phase.save_billing_choices import save_billing_choices from core.services.payment_phase.save_billing_choices import save_billing_choices
from core.errors.exceptions import *
from core.errors.logger import logger from core.errors.logger import logger
# generic:
from pydantic import ValidationError
# the controller already filtered the payload prior to this function. filepath = f"{Constants.HV_TICKETING_CONFIG_HOME}/billing_choices.json"
# the controller already filtered an invalid API connection issue prior to this function.
def save_and_send_intitial_billing( def save_and_send_intitial_billing(
payload: dict, payload: dict,
connection_observer: ConnectionObserver, connection_observer: ConnectionObserver
invoice_data_object: TicketInvoice, ) -> Result:
) -> TicketInvoice | ApiResponse:
""" """
Purpose: Purpose:
Does a POST request to get updated billing information. Does a POST request to get updated billing information.
Returns:
TicketInvoice on Success, with the modified values.
ApiResponse on Failure, with why the API failed.
Called by: Called by:
TicketPayController TicketPayController
Errors: Errors:
No. Does NOT raise errors. No. Does NOT raise errors.
""" """
# SAVE CHOICES:
# Save choices:
saved_choices = save_billing_choices(payload) saved_choices = save_billing_choices(payload)
if not saved_choices: if not saved_choices:
error_msg = "Unable to save billing choices locally. Check file permissions and storage space." error_msg = "Unable to save billing choices locally. Check file permissions and storage space."
logger.error(error_msg) logger.error(error_msg)
return TicketInvoice(is_valid=False, final_error_msg=error_msg) return Result(valid=False, error_type=ResultError.FILE_SYSTEM, message=error_msg)
# send them: # SEND CHOICES:
which_endpoint = "start_payment" which_endpoint = "start_payment"
url = make_url(which_endpoint) url = make_url(which_endpoint)
# api_reply_object = send_data_to_server(payload, url, connection_observer) print(f"saved, now sending choices to {url}")
api_reply_object = connect.single_endpoint( api_reply_object = connect.single_endpoint(
method="post", method="post",
url=url, url=url,
observer=connection_observer, observer=connection_observer,
payload=payload payload=payload
) )
# if not api_reply_object.valid:
# api_reply_object = solve_api_problems(
# api_reply_object=api_reply_object,
# get_or_post="post",
# url=url,
# payload=payload,
# connection_observer=connection_observer,
# client_observer=None
# )
if not api_reply_object.valid: if not api_reply_object.valid:
return api_reply_object error_msg = f"API Error! {api_reply_object.message}"
logger.error(f"{api_reply_object.error_type} {error_msg}")
raise ConnectionError(error_msg)
reply_dict_data = api_reply_object.data # Extract from API Response into Invoice Object
server_reply = api_reply_object.data
# extract values from server's reply: # Validate and create the Pydantic model
modified_data_object = extract_payment_details(reply_dict_data, invoice_data_object) try:
ticket_invoice = TicketInvoice.model_validate(server_reply)
except ValidationError as e:
error_msg = f"Server's Reply is Missing {str(e)}"
logger.error(error_msg)
raise ValueError(error_msg)
return modified_data_object # save the temp_billing_code:
print("saving to ticket tracker json..")
did_it_save = update_json(
filepath=filepath,
key_to_add="temp_billing_code",
value_to_update=ticket_invoice.temp_billing_code
)
print(f"did_it_save is {did_it_save}")
if did_it_save == True:
return Result(valid=True, data=ticket_invoice)
else:
error_msg = f"Error: could NOT save the temp_billing_code {ticket_invoice.temp_billing_code}. Check your local file permissions and storage."
logger.error(error_msg, exc_info=True)
return Result(valid=False, error_type=ResultError.FILE_SYSTEM, message=error_msg)

View file

@ -82,6 +82,7 @@ def ticket_prep_orchestrator(
##################################################### #####################################################
# 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) ticket_observer.notify("preparing", subject=notification)
# then send the entire blinded list to the server to sign: # then send the entire blinded list to the server to sign:
@ -107,6 +108,7 @@ def ticket_prep_orchestrator(
##################################################### #####################################################
# regardless of the outcome of the verification, save all blind sigs, just in case, because the user can't get them again, # 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" notification = "Saving the Server's Blind Replies"
if ticket_observer is not None:
ticket_observer.notify("preparing", subject=notification) ticket_observer.notify("preparing", subject=notification)
did_they_ALL_save = save_ALL_blind_sigs(blind_signatures, which_ticket) did_they_ALL_save = save_ALL_blind_sigs(blind_signatures, which_ticket)
@ -123,6 +125,7 @@ def ticket_prep_orchestrator(
how_many_failed = len(failed_validations) how_many_failed = len(failed_validations)
if how_many_failed >= 1: if how_many_failed >= 1:
notification = f"Verification failed for {how_many_failed} blind signatures." notification = f"Verification failed for {how_many_failed} blind signatures."
if ticket_observer is not None:
ticket_observer.notify("preparing", subject=notification) ticket_observer.notify("preparing", subject=notification)
logger.debug( logger.debug(
f"Verification failed for {how_many_failed} blind signatures." f"Verification failed for {how_many_failed} blind signatures."
@ -150,21 +153,10 @@ def ticket_prep_orchestrator(
##################################################### #####################################################
# TICKET TRACKER # TICKET TRACKER
##################################################### #####################################################
# I'm checking how many profiles here, because if it's multiple tickets then,
# we wipe the file if it already exists. But for one ticket, we just update it
if how_many_profiles > 1:
# make a json to keep track of which tickets are used:
setup_tracker = setup_ticket_tracker(how_many_profiles)
if not setup_tracker:
return {"valid": False, "message": "failed"}
else:
# for a single ticket, we only want to setup the ticket tracker if it doesn't exist yet:
if not does_file_exist(Constants.TICKET_TRACKER_PATH): if not does_file_exist(Constants.TICKET_TRACKER_PATH):
setup_tracker = setup_ticket_tracker(how_many_profiles) setup_tracker = setup_ticket_tracker(how_many_profiles)
if not setup_tracker: if not setup_tracker:
return {"valid": False, "message": "failed"} return {"valid": False, "message": "failed"}
# assuming ticket tracker exists, let's update it:
wiped_sub = ticket_tracker.wipe_one_ticket_sub(which_ticket)
##################################################### #####################################################
# FINAL EVALUATION # FINAL EVALUATION

View file

@ -1,4 +1,4 @@
from core.utils.basic_operations.write_or_read_from_json import read_entire_json, update_json from core.utils.basic_operations.write_or_read_from_json import read_entire_json, update_json, write_json_to_file
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.errors.exceptions import * from core.errors.exceptions import *
from core.errors.logger import logger from core.errors.logger import logger
@ -8,10 +8,6 @@ from core.Constants import Constants
import json import json
import os import os
# folders:
# billing_folder = Constants.HV_TICKETING_CONFIG_HOME
# ticket_tracker_path = f"{billing_folder}/ticket_tracker.json"
def get_all_unused_tickets() -> dict: def get_all_unused_tickets() -> dict:
data = read_entire_json(Constants.TICKET_TRACKER_PATH) data = read_entire_json(Constants.TICKET_TRACKER_PATH)
@ -80,6 +76,17 @@ def get_data_for_a_single_ticket(which_ticket_as_int: int) -> tuple:
def wipe_one_ticket_sub(which_ticket: int) -> bool: def wipe_one_ticket_sub(which_ticket: int) -> bool:
"""
Purpose:
Wipe a single ticket in the ticket tracker.
Rank:
Mini-Helper
Called by:
respawn_billing_code_into_ticket
"""
if not does_file_exist(Constants.TICKET_TRACKER_PATH):
return False
wipe_payload = { wipe_payload = {
"status": "unused", "status": "unused",
"location": None, "location": None,
@ -88,13 +95,20 @@ def wipe_one_ticket_sub(which_ticket: int) -> bool:
return update_json( return update_json(
filepath=Constants.TICKET_TRACKER_PATH, filepath=Constants.TICKET_TRACKER_PATH,
key_to_add=which_ticket, key_to_add=int(which_ticket),
value_to_update=wipe_payload value_to_update=wipe_payload
) )
def get_tickets_with(target_subscription: str) -> list: def _get_tickets_with(target_subscription: str) -> list:
"""
Purpose:
Searches the ticket tracker for a given subscription code
Rank:
Mini-Helper
Called by:
find_ticket_by_sub
"""
if not does_file_exist(Constants.TICKET_TRACKER_PATH): if not does_file_exist(Constants.TICKET_TRACKER_PATH):
return [] return []
@ -110,3 +124,74 @@ def get_tickets_with(target_subscription: str) -> list:
return list_of_matches return list_of_matches
def find_ticket_by_sub(target_subscription: str) -> int | None:
"""
Purpose:
Returns a single ticket int id if it's been used for a given subscription code,
by looking through the ticket tracker json.
Rank:
Cross-Module Helper
Called by:
respawn_profile
"""
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} from the ticket tracker JSON")
return which_ticket
else:
logger.info(f"We were UNABLE to find a ticket with the subscription {target_subscription} from the ticket tracker JSON.")
return None
# This is currently not used, but it could be. Useful tool for explicitly changing subvalues.
def change_ticket(which_ticket: str, status: str, location=None, subscription=None) -> bool:
"""
Purpose:
Update a specific which_ticket entry in the JSON.
Rank:
Cross-Module Helper
Args:
which_ticket: The key number (e.g., "1", "2")
status: New status ("unused" or "used")
location: Value for location field (default None)
subscription: Value for subscription field (default None)
Returns:
True if successful, False otherwise
"""
# Force string, to avoid duplicate keys:
which_ticket = str(which_ticket)
try:
data = read_entire_json(Constants.TICKET_TRACKER_PATH)
except (FileNotFoundError, json.JSONDecodeError):
data = {}
logger.info(f"DEBUG: Keys in file after read: {list(data.keys())}")
logger.info(f"DEBUG: Looking for which_ticket: '{which_ticket}' (type: {type(which_ticket).__name__})")
logger.info(f"DEBUG: Full data structure: {json.dumps(data, indent=2)}")
# STEP 1: Check if which_ticket exists (and should exist if file was read correctly)
if which_ticket not in data:
logger.warning(f"⚠️ which_ticket '{which_ticket}' NOT FOUND. Available keys: {list(data.keys())}")
logger.warning(f"Creating new entry for '{which_ticket}'")
data[which_ticket] = {}
else:
logger.info(f"✓ Found existing entry for '{which_ticket}'")
# STEP 2: Update ONLY the nested fields (not appending, just overwriting)
data[which_ticket]["status"] = status
data[which_ticket]["location"] = location
data[which_ticket]["subscription"] = subscription
logger.info(f"Updated which_ticket '{which_ticket}': status={status}, location={location}, subscription={subscription}")
# STEP 3: Write to file
try:
return write_json_to_file(data=data, filepath=Constants.TICKET_TRACKER_PATH)
except Exception as e:
logger.error(f"Failed to write JSON: {e}")
return False

View file

@ -9,9 +9,11 @@ from core.Errors import MissingSubscriptionError, InvalidSubscriptionError
# from core.services.WebServiceApiService import WebServiceApiService # from core.services.WebServiceApiService import WebServiceApiService
from core.controllers.ConnectionController import ConnectionController from core.controllers.ConnectionController import ConnectionController
from core.observers.ConnectionObserver import ConnectionObserver from core.observers.ConnectionObserver import ConnectionObserver
from core.observers.TicketObserver import TicketObserver
from core.Constants import Constants from core.Constants import Constants
from core.models.Subscription import Subscription from core.models.Subscription import Subscription
from core.models.SubscriptionPlan import SubscriptionPlan from core.models.SubscriptionPlan import SubscriptionPlan
from core.models.Result import Result, ResultError
from core.errors.logger import logger from core.errors.logger import logger
from typing import Union, Optional from typing import Union, Optional
@ -32,6 +34,9 @@ def activate_subscription(
Confusion: Confusion:
This returns True both if was already active or just activated. This returns True both if was already active or just activated.
Unrelated Note:
This does NOT work for assassin. Assassin doesn't use legacy subs. it uses tickets only.
Returns: Returns:
Returns True if subscription was already active. Returns True if subscription was already active.
Returns True if subscription was just activated. Returns True if subscription was just activated.
@ -42,10 +47,12 @@ def activate_subscription(
""" """
if not profile.has_subscription(): if not profile.has_subscription():
logger.info("No Subscription Error being raised")
raise MissingSubscriptionError() raise MissingSubscriptionError()
# Already activated—nothing to do # Already activated—nothing to do
if profile.subscription.has_been_activated(): if profile.subscription.has_been_activated():
logger.info("sub has already been activated, returnining true")
return True return True
# ================================================== # ==================================================

View file

@ -165,7 +165,6 @@ def coordinate_cache_sync(
""" """
full_request_url = f"{Constants.SP_API_BASE_URL}/cachedsync" full_request_url = f"{Constants.SP_API_BASE_URL}/cachedsync"
# api_result = get_data_from_api(full_request_url, client_observer, connection_observer)
api_result = connect.single_endpoint( api_result = connect.single_endpoint(
method="get", method="get",
url=full_request_url, url=full_request_url,

View file

@ -3,7 +3,9 @@ from core.errors.logger import logger
import json import json
import os import os
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any, Optional
import tempfile
import shutil
def write_json_to_file(data: dict, filepath: str) -> bool: def write_json_to_file(data: dict, filepath: str) -> bool:
try: try:
@ -12,9 +14,13 @@ def write_json_to_file(data: dict, filepath: str) -> bool:
if directory and not os.path.exists(directory): if directory and not os.path.exists(directory):
os.makedirs(directory) os.makedirs(directory)
with open(filepath, "w") as f: # Write to a temporary file first
with tempfile.NamedTemporaryFile(mode='w', dir=directory, delete=False, suffix='.json') as f:
json.dump(data, f, indent=4) json.dump(data, f, indent=4)
temp_path = f.name
# Only replace the original if successful
shutil.move(temp_path, filepath)
return True return True
except TypeError as e: except TypeError as e:
@ -209,18 +215,19 @@ def update_value_in_json_with_two_values(
return False return False
def update_json(filepath, key_to_add, value_to_update): def update_json(filepath: str, key_to_add: str, value_to_update: str) -> bool:
try: try:
data = read_entire_json(filepath) data = read_entire_json(filepath)
except: except (FileNotFoundError, json.JSONDecodeError):
data = {} data = {}
finally: finally:
# update the value: # update the value:
key_to_add = str(key_to_add)
data[key_to_add] = value_to_update data[key_to_add] = value_to_update
try: try:
# update the file: # update the file:
write_json_to_file(data, filepath) write_json_to_file(data=data, filepath=filepath)
return True return True
except FileNotFoundError: except FileNotFoundError: