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:
# 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
### 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.

View file

@ -2,8 +2,9 @@ from core.Constants import Constants
from core.Errors import CommandNotFoundError
from core.controllers.SessionStateController import SessionStateController
from core.models.session.Application import Application
# from core.models.session.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.SessionState import SessionState
@ -32,7 +33,7 @@ class ApplicationController:
return Application.all()
@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
@ -98,7 +99,7 @@ class ApplicationController:
if not fork_process_id:
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)
sys.exit()
@ -106,7 +107,7 @@ class ApplicationController:
else:
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
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.systemwide.systemwide_wireguard import establish_system_connection, terminate_system_connection
from core.services.keys_and_verifications.endpoint_verification import verify_wireguard_endpoint
from core.observers.TicketObserver import TicketObserver
from collections.abc import Callable
@ -60,7 +61,7 @@ class ConnectionController:
@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_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.')
else:
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':

View file

@ -8,8 +8,8 @@ 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.services.assassin.ticket_respawn import respawn_profile
from core.services.assassin import assassin_tools
from core.errors.logger import logger
from core.Errors import InvalidSubscriptionError, MissingSubscriptionError, ConnectionTerminationError, ProfileActivationError, ProfileDeactivationError, MissingLocationError, ConnectionUnprotectedError, EndpointVerificationError, ProfileStateConflictError
@ -58,7 +58,10 @@ class ProfileController:
asynchronous: bool = False,
profile_observer: ProfileObserver = 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
@ -78,13 +81,29 @@ class ProfileController:
# ============================================================================
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
if not application_version.is_installed():
ApplicationVersionController.install(application_version, application_version_observer=application_version_observer, connection_observer=connection_observer)
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:
raise ProfileActivationError('The profile could not be enabled.')
except ValueError:
@ -93,7 +112,7 @@ class ProfileController:
if profile_observer is not None:
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
@ -112,11 +131,31 @@ class ProfileController:
@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
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):
@ -192,37 +231,13 @@ class ProfileController:
# 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)
respawned = respawn_profile(
profile=profile,
ticket_observer=ticket_observer,
connection_observer=connection_observer
)
if which_ticket and not respawned:
logger.error("Serious issue with respawning the ticket. We might raise an error here.")
####################################
# 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.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.models.Result import Result, ResultError
from core.services.networking.api_requests.step5_solve_api_problems import solve_api_problems
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.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.services.helpers.valid_profile_quantity import valid_profile_quantity
from core.errors.exceptions import *
from core.errors.logger import logger
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
@ -46,123 +46,107 @@ def initiate_payment(
) -> TicketInvoice:
###############
invoice_data_object = TicketInvoice()
if bypass_existing == False:
tickets_exist_already, path = does_ticket_tracker_exist()
logger.debug(f"tickets_exist_already is {tickets_exist_already}")
if tickets_exist_already:
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?")
else:
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")
billing_id = do_we_have_billing_id()
if billing_id:
error_msg = "Billing code exists already"
logger.error(error_msg)
return Result(valid=False, error_type=ResultError.BILLING_CODE_EXISTS, data=billing_id, message=error_msg)
rejected_choices = [None, "", False]
if how_many_profiles in rejected_choices:
notification = "Missing profile quantity, to initiate payment"
logger.error(notification)
ticket_observer.notify("failed_input", subject=notification)
return Result(valid=False, error_type=ResultError.INVALID_INPUT, message=notification)
if not valid_profile_quantity(how_many_profiles):
notification = "Invalid profile quantity"
logger.error(notification)
ticket_observer.notify("failed_input", subject=notification)
return Result(valid=False, error_type=ResultError.INVALID_INPUT, message="You've picked a currently unsupported profile quantity")
if which_key in rejected_choices:
notification = "Missing key plan, to initiate payment"
logger.error(notification)
ticket_observer.notify("failed_input", subject=notification)
return Result(valid=False, error_type=ResultError.INVALID_INPUT, message="You didn't pick a key plan. It's either blank, none, or invalid.")
# get & save the public key:
public_key_results = get_pub_key(which_key, connection_observer, "local")
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)
if status == False:
message = public_key_results.get(
"message", "Please fix before you continue"
)
error_code = public_key_results.get("error_code", "No error_code")
notification = f"Connection Issues or Invalid Key chosen. {error_code}"
logger.error(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.")
# In this case, the controller is going to send the whole JSON payload to the service,
# instead of passing 4 values seperately.
payload = {
"which_key": which_key,
"payment_type": "crypto",
"which_cryptocurrency": which_cryptocurrency,
"how_many_profiles": how_many_profiles,
}
try:
if bypass_existing == False:
tickets_exist_already, path = does_ticket_tracker_exist()
logger.debug(f"tickets_exist_already is {tickets_exist_already}")
if tickets_exist_already:
invoice_data_object.add_error_code("already_exists")
return invoice_data_object
else:
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")
billing_id = do_we_have_billing_id()
if billing_id:
invoice_data_object.add_error_code("billing_code_exists")
invoice_data_object.temp_billing_code = billing_id
return invoice_data_object
rejected_choices = [None, "", False]
if how_many_profiles in rejected_choices:
notification = "Missing profile quantity, to initiate payment"
ticket_observer.notify("failed_input", subject=notification)
invoice_data_object.add_error_code("invalid_quantity")
return invoice_data_object
if not valid_profile_quantity(how_many_profiles):
notification = "Invalid profile quantity"
ticket_observer.notify("failed_input", subject=notification)
invoice_data_object.add_error_code("invalid_quantity")
return invoice_data_object
if which_key in rejected_choices:
notification = "Missing key plan, to initiate payment"
ticket_observer.notify("failed_input", subject=notification)
invoice_data_object.add_error_code("no_keyplan")
return invoice_data_object
# get & save the public key:
public_key_results = get_pub_key(which_key, connection_observer, "local")
if isinstance(public_key_results, dict):
status = public_key_results.get("valid", False)
if status == False:
message = public_key_results.get(
"message", "Please fix before you continue"
)
error_code = public_key_results.get("error_code", "No error_code")
logger.debug(f"error_code: {error_code}")
invoice_data_object.add_error_code(error_code)
notification = f"Connection Issues or Invalid Key chosen. {message}"
ticket_observer.notify("failed_input", subject=notification)
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,
# instead of passing 4 values seperately.
payload = {
"which_key": which_key,
"payment_type": "crypto",
"which_cryptocurrency": which_cryptocurrency,
"how_many_profiles": how_many_profiles,
}
# controller sends to the service:
result = save_and_send_intitial_billing(
payload, connection_observer, invoice_data_object
return save_and_send_intitial_billing(
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:
error_msg = "Invalid Data."
ticket_observer.notify("failed_input", subject=error_msg)
invoice_data_object.add_error_code("invalid_data")
return invoice_data_object
if ticket_observer:
ticket_observer.notify("failed_input", subject=str(e))
# invoice_data_object.add_error_code("invalid_data")
# return invoice_data_object
return Result(valid=False, error_type=ResultError.INVALID_INPUT, message=str(e))
except NetworkingError as e:
error_msg = f"NetworkingError: {e}"
except ConnectionError as e:
error_msg = "There were connection issues with getting the right key."
logger.error(error_msg, exc_info=True)
ticket_observer.notify("connection_error", subject=error_msg)
invoice_data_object.add_error_code("connection_error")
return invoice_data_object
if ticket_observer:
ticket_observer.notify("connection_error", subject=str(e))
# invoice_data_object.add_error_code("connection_error")
return Result(valid=False, error_type=ResultError.CONNECTION, message=error_msg)
except ServerSideError as e:
error_msg = f"ServerSideError: {e}"
logger.error(error_msg, exc_info=True)
ticket_observer.notify("failed_output", subject=error_msg)
invoice_data_object.add_error_code("server_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
# except ServerSideError as e:
# error_msg = f"ServerSideError: {e}"
# logger.error(error_msg, exc_info=True)
# ticket_observer.notify("failed_output", subject=error_msg)
# invoice_data_object.add_error_code("server_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)
# literally send:
# api_reply_object = send_data_to_server(payload, url, connection_observer)
api_reply_object = connect.single_endpoint(
method="post",
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}")
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:
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}")

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.make_sure_pub_key_exists import make_sure_pub_key_exists
from core.services.prepare_tickets import ticket_tracker
from core.services.helpers.get_how_many_profiles_were_ordered import (
get_how_many_profiles_were_ordered,
)
from core.observers.BaseObserver import BaseObserver
from core.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
# generic
@ -49,7 +48,8 @@ def prepare_tickets(
# make sure it's a number:
if not isinstance(how_many_profiles, int):
ticket_observer.notify("failed_input", None)
if ticket_observer is not None:
ticket_observer.notify("failed_input", None)
return {"valid": False, "error_code": "failed_input"}
# allow single prep:
@ -58,17 +58,20 @@ def prepare_tickets(
# (which is based on locally saved data from the previous step):
how_many_ordered = get_how_many_profiles_were_ordered()
if how_many_profiles != how_many_ordered:
ticket_observer.notify("failed_input", None)
if ticket_observer is not None:
ticket_observer.notify("failed_input", None)
return {"valid": False, "error_code": "failed_input"}
# make sure this guy has a public key to verify against:
does_he_have_public_key = make_sure_pub_key_exists(connection_observer)
if does_he_have_public_key == False:
ticket_observer.notify("failed_input", None)
if ticket_observer is not None:
ticket_observer.notify("failed_input", None)
return {"valid": False, "error_code": "failed_input"}
notification = "Preparing Cryptography Locally"
ticket_observer.notify("preparing", subject=notification)
if ticket_observer is not None:
ticket_observer.notify("preparing", subject=notification)
# ok now we have the pre-reqs, let's use this high level orchestrator,
prep_results = ticket_prep_orchestrator(
@ -89,7 +92,8 @@ def prepare_tickets(
if prep_results["valid"] == True:
notification = f"Done! All Tickets Ready!"
ticket_observer.notify("preparing", subject=notification)
if ticket_observer is not None:
ticket_observer.notify("preparing", subject=notification)
return prep_results
if "how_many_failed" in prep_results:
@ -97,33 +101,43 @@ def prepare_tickets(
failed_validations = prep_results.get("failed_validations", None)
if failed_validations:
notification = f"Error with Ticket Preparation or Verification!"
ticket_observer.notify("preparing", subject=notification)
if ticket_observer is not None:
ticket_observer.notify("preparing", subject=notification)
return prep_results
notification = f"Error with Ticket Preparation or Verification!"
ticket_observer.notify("preparing", subject=notification)
if ticket_observer is not None:
ticket_observer.notify("preparing", subject=notification)
return prep_results
# No profile required
def respawn_billing_code_into_ticket(
profile: Union[SessionProfile, SystemProfile],
billing_code: str,
which_ticket: int,
ticket_observer: TicketObserver,
connection_observer: ConnectionObserver
) -> Result:
"""
Converts a valid VPN billing code, into a valid unused ticket.
At the cost of the server expiring the billing code immediately.
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.
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.
The new ticket can be applied to any other profile now.
This has the effect of "wiping" the billing code, and starting over.
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
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")
#################################################
# PREP PRE-REQS
#################################################
if not which_ticket:
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:
return Result(valid=False, error_type=ResultError.INVALID_INPUT, message="Ticket slot must be a number.")
#################################################
# SEND TO SERVER
#################################################
ticket_result = prepare_tickets(
how_many_profiles=1,
ticket_observer=ticket_observer,
@ -141,6 +158,9 @@ def respawn_billing_code_into_ticket(
billing_code=billing_code
)
#################################################
# EVALUATE RESULTS
#################################################
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.")
@ -148,21 +168,26 @@ def respawn_billing_code_into_ticket(
if not valid:
notification = f"Error in Respawn Prep!"
ticket_observer.notify("preparing", subject=notification)
if ticket_observer is not None:
ticket_observer.notify("preparing", subject=notification)
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
#################################################
notification = f"Respawn Done for Ticket Slot {which_ticket}!"
if ticket_observer is not None:
ticket_observer.notify("preparing", subject=notification)
# save the ticket,
profile.ticket = which_ticket
profile.save()
notification = f"Respawn Done for Profile {profile.id}!"
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):

View file

@ -14,6 +14,8 @@ from core.services.prepare_tickets.ticket_tracker import (
get_data_for_a_single_ticket,
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.get_value_from_config import get_value_from_config
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,
) -> dict:
print(f"Calling the use_ticket function! with which_ticket as {which_ticket}")
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:
ticket_exists = does_ticket_file_exist(which_ticket)
if ticket_exists == False:
@ -163,17 +172,19 @@ def use_ticket(
return {"valid": False, "message": error_msg}
# is the ticket used?
try:
status, location, subscription = get_data_for_a_single_ticket(which_ticket)
if status == "used":
error_msg = f"Ticket is already tied to {location} with the subscription {subscription}"
ticket_observer.notify("failed_input", subject=error_msg)
return {"valid": False, "message": error_msg}
except ValueError as e:
error_msg = f"Your local ticket tracker has no value for ticket {which_ticket}"
logger.error(error_msg)
logger.error(str(e))
return {"valid": False, "message": error_msg}
# first, get values,
# try:
# status, location, subscription = get_data_for_a_single_ticket(which_ticket)
# if status == "used":
# error_msg = f"Ticket is already tied to {location} with the subscription {subscription}"
# ticket_observer.notify("failed_input", subject=error_msg)
# return {"valid": False, "message": error_msg}
# except ValueError as e:
# error_msg = f"Your local ticket tracker has no value for ticket {which_ticket}"
# logger.error(error_msg)
# logger.error(str(e))
# return {"valid": False, "message": error_msg}
# the actual work here, everything else is just handling:
ticket_observer.notify("connecting", "Connecting..")

View file

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

View file

@ -1,13 +1,13 @@
from pydantic import BaseModel
from core.errors.get_error_msg import get_error_msg
class TicketInvoice(BaseModel):
temp_billing_code: str | None = None
payment_type: str | None = None
selected_currency: str | None = None
due_amount: float | None = None
address: str | None = None
valid: bool = True
temp_billing_code: str
payment_type: str = "crypto"
selected_currency: str = None
due_amount: float
address: str
final_error_msg: 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 import select
from typing import Optional
import random
@safe_db_operation
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()
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.orm import joinedload
from sqlalchemy.orm import Session
from typing import Optional
import random
@safe_db_operation
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:
return location_object.data
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)
print(critical_error)
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]
connection: Optional[SessionConnection] = None
ticket: Optional[int] = None
assassin: Optional[bool] = False
def has_connection(self):
return self.connection is not None
@ -39,7 +40,7 @@ class SessionProfile(BaseProfile):
if 'location' in self._get_dirty_keys():
self.__delete_proxy_configuration()
self.__delete_wireguard_configuration()
self.delete_wireguard_configuration()
# === APPLICATION ===
app_version_dict = self.application_version.convert_to_dict()
@ -95,7 +96,7 @@ class SessionProfile(BaseProfile):
def address_security_incident(self):
super().address_security_incident()
self.__delete_wireguard_configuration()
self.delete_wireguard_configuration()
def determine_timezone(self):
@ -124,5 +125,5 @@ class SessionProfile(BaseProfile):
def __delete_proxy_configuration(self):
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)

View file

@ -13,6 +13,7 @@ import subprocess
class SystemProfile(BaseProfile):
connection: Optional[SystemConnection]
ticket: Optional[int] = None
assassin: Optional[bool] = False
def get_system_config_path(self):
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.models.Result import Result, ResultError
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:
# 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.observers.ConnectionObserver import ConnectionObserver
from core.controllers.SystemStateController import SystemStateController
from core.observers.TicketObserver import TicketObserver
import os
@ -31,11 +34,13 @@ def establish_connection(
profile: Union[SessionProfile, SystemProfile],
ignore: tuple[type[Exception]] = (),
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."""
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
@ -59,13 +64,13 @@ def establish_connection(
ProfileType.SYSTEM: establish_system_connection,
}[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(
profile: Union[SessionProfile, SystemProfile],
connection_observer: Optional[ConnectionObserver],
connection_observer: Optional[ConnectionObserver]
):
"""
Launch & Setup an Encrypted proxy.
@ -130,7 +135,8 @@ def _ensure_proxy_configured(
if not profile.has_proxy_configuration():
logger.info(f"[CONNECTION] You need a new proxy configuration")
activate_subscription(profile, connection_observer)
if not profile.assassin:
activate_subscription(profile, connection_observer)
logger.info(f"[CONNECTION] Getting a proxy config from web service..")
proxy_config = ConnectionController.with_preferred_connection(
@ -147,7 +153,7 @@ def _ensure_proxy_configured(
def _ensure_wireguard_configured(
profile: Union[SessionProfile, SystemProfile],
connection_observer: Optional[ConnectionObserver],
connection_observer: Optional[ConnectionObserver]
):
"""Setup WireGuard config if needed."""
@ -164,7 +170,8 @@ def _ensure_wireguard_configured(
except ConnectionTerminationError as e:
logger.error(f"[CONNECTION] Previous Systemwide Wireguard connection could not be disabled. {e}")
activate_subscription(profile, connection_observer)
if not profile.assassin:
activate_subscription(profile, connection_observer)
logger.info(f"[CONNECTION] Checking if the profile already has a wg config..")
if not profile.has_wireguard_configuration():
logger.info(f"[CONNECTION] Profile does NOT have WG keys and it needs it. Registering a NEW wg session..")
@ -176,6 +183,7 @@ def _establish_with_renegotiation(
establish_fn: Callable,
ignore: tuple[type[Exception]],
connection_observer: Optional[ConnectionObserver],
ticket_observer: Optional[TicketObserver] = None
):
"""Attempt connection, renegotiate WireGuard once if needed."""
try:
@ -185,7 +193,7 @@ def _establish_with_renegotiation(
if __should_renegotiate(profile):
logger.info(f"[CONNECTION] Renegotiating a new wg key session with API..")
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.')
except FirewallError:
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]):
if profile.assassin:
return False
if not profile.has_subscription():
raise MissingSubscriptionError()

View file

@ -32,10 +32,11 @@ def make_request(
if initial_result.valid:
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
if initial_result.backoff_strategy == BackoffStrategy.RETRY_IMMEDIATE:
logger.info("Trying again RETRY_IMMEDIATE")
time.sleep(3)
second_result = _make_request(
method=method,
@ -43,8 +44,8 @@ def make_request(
client=client,
payload=payload,
billing_code=billing_code
),
)
logger.info(f"second_result validity is {second_result.valid} and type {str(second_result)}")
if second_result.valid:
return second_result

View file

@ -1,63 +1,69 @@
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.utils.basic_operations.write_or_read_from_json import update_json
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(
server_reply: dict, invoice_data_object: TicketInvoice
) -> TicketInvoice:
def extract_payment_details(server_reply: dict) -> TicketInvoice:
print(f"server_reply is {server_reply}")
# Validate and create the Pydantic model
try:
temp_billing_code = server_reply.get("temp_billing_code", None)
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)
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),
}
# save the temp_billing_code:
did_it_save = update_json(filepath, "temp_billing_code", ticket_invoice.temp_billing_code)
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
if did_it_save == True:
return ticket_invoice
# save the temp_billing_code:
billing_folder = Constants.HV_TICKETING_CONFIG_HOME
filepath = f"{billing_folder}/billing_choices.json"
error_msg = (
f"Error!!! It did not save the temp_billing_code {ticket_invoice.temp_billing_code}"
)
logger.error(error_msg, exc_info=True)
return False
did_it_save = update_json(filepath, "temp_billing_code", temp_billing_code)
if did_it_save == False:
invoice_data_object.is_valid = False
error_msg = (
f"Error!!! It did not save the temp_billing_code {temp_billing_code}"
)
logger.error(error_msg, exc_info=True)
print(error_msg)
raise MissingData(error_msg)
return invoice_data_object
# except ServerSideError as e:
# logger.error(f"ServerSideError: {e}", exc_info=True)
# logger.error("Unable to extract payment details from the server's reply.")
# return None
# except Exception:
# error_msg = "Unable to extract payment details from the server's reply."
# logger.error(f"From inside the extract payment details module, {error_msg}")
# invoice_data_object.add_error_code(error_msg)
# return None
except ServerSideError as e:
logger.error(f"ServerSideError: {e}", exc_info=True)
invoice_data_object.add_error_code(
"Unable to extract payment details from the server's reply."
)
return invoice_data_object
except:
error_msg = "Unable to extract payment details from the server's reply."
logger.error(f"From inside the extract payment details module, {error_msg}")
invoice_data_object.add_error_code(error_msg)
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:
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.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.utils.basic_operations.write_or_read_from_json import update_json
from core.Constants import Constants
from core.services.networking.httpx import connect
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.errors.exceptions import *
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(
payload: dict,
connection_observer: ConnectionObserver,
invoice_data_object: TicketInvoice,
) -> TicketInvoice | ApiResponse:
connection_observer: ConnectionObserver
) -> Result:
"""
Purpose:
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:
TicketPayController
Errors:
No. Does NOT raise errors.
"""
# Save choices:
# SAVE CHOICES:
saved_choices = save_billing_choices(payload)
if not saved_choices:
error_msg = "Unable to save billing choices locally. Check file permissions and storage space."
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"
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(
method="post",
url=url,
observer=connection_observer,
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:
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:
modified_data_object = extract_payment_details(reply_dict_data, invoice_data_object)
# Validate and create the Pydantic model
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,7 +82,8 @@ def ticket_prep_orchestrator(
#####################################################
# assuming we actually saved the unblinding factors,
notification = "Sending Blinded Package to the Server.."
ticket_observer.notify("preparing", subject=notification)
if ticket_observer is not None:
ticket_observer.notify("preparing", subject=notification)
# then send the entire blinded list to the server to sign:
blind_signatures = send_blind_commitments(
@ -107,7 +108,8 @@ 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,
notification = "Saving the Server's Blind Replies"
ticket_observer.notify("preparing", subject=notification)
if ticket_observer is not None:
ticket_observer.notify("preparing", subject=notification)
did_they_ALL_save = save_ALL_blind_sigs(blind_signatures, which_ticket)
# verify the server's blind signatures against the public key,
@ -123,7 +125,8 @@ def ticket_prep_orchestrator(
how_many_failed = len(failed_validations)
if how_many_failed >= 1:
notification = f"Verification failed for {how_many_failed} blind signatures."
ticket_observer.notify("preparing", subject=notification)
if ticket_observer is not None:
ticket_observer.notify("preparing", subject=notification)
logger.debug(
f"Verification failed for {how_many_failed} blind signatures."
)
@ -150,21 +153,10 @@ def ticket_prep_orchestrator(
#####################################################
# 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:
if not does_file_exist(Constants.TICKET_TRACKER_PATH):
setup_tracker = setup_ticket_tracker(how_many_profiles)
if not setup_tracker:
return {"valid": False, "message": "failed"}
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):
setup_tracker = setup_ticket_tracker(how_many_profiles)
if not setup_tracker:
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

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.errors.exceptions import *
from core.errors.logger import logger
@ -8,10 +8,6 @@ from core.Constants import Constants
import json
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:
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:
"""
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 = {
"status": "unused",
"location": None,
@ -88,13 +95,20 @@ def wipe_one_ticket_sub(which_ticket: int) -> bool:
return update_json(
filepath=Constants.TICKET_TRACKER_PATH,
key_to_add=which_ticket,
key_to_add=int(which_ticket),
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):
return []
@ -110,3 +124,74 @@ def get_tickets_with(target_subscription: str) -> list:
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.controllers.ConnectionController import ConnectionController
from core.observers.ConnectionObserver import ConnectionObserver
from core.observers.TicketObserver import TicketObserver
from core.Constants import Constants
from core.models.Subscription import Subscription
from core.models.SubscriptionPlan import SubscriptionPlan
from core.models.Result import Result, ResultError
from core.errors.logger import logger
from typing import Union, Optional
@ -32,6 +34,9 @@ def activate_subscription(
Confusion:
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 True if subscription was already active.
Returns True if subscription was just activated.
@ -42,10 +47,12 @@ def activate_subscription(
"""
if not profile.has_subscription():
logger.info("No Subscription Error being raised")
raise MissingSubscriptionError()
# Already activated—nothing to do
if profile.subscription.has_been_activated():
logger.info("sub has already been activated, returnining true")
return True
# ==================================================

View file

@ -165,7 +165,6 @@ def coordinate_cache_sync(
"""
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(
method="get",
url=full_request_url,

View file

@ -3,7 +3,9 @@ from core.errors.logger import logger
import json
import os
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:
try:
@ -12,9 +14,13 @@ def write_json_to_file(data: dict, filepath: str) -> bool:
if directory and not os.path.exists(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)
temp_path = f.name
# Only replace the original if successful
shutil.move(temp_path, filepath)
return True
except TypeError as e:
@ -209,18 +215,19 @@ def update_value_in_json_with_two_values(
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:
data = read_entire_json(filepath)
except:
except (FileNotFoundError, json.JSONDecodeError):
data = {}
finally:
# update the value:
key_to_add = str(key_to_add)
data[key_to_add] = value_to_update
try:
# update the file:
write_json_to_file(data, filepath)
write_json_to_file(data=data, filepath=filepath)
return True
except FileNotFoundError: