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:
parent
a78747e8e5
commit
6dac98835f
26 changed files with 897 additions and 342 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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':
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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}")
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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..")
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
166
core/services/assassin/assassin_tools.py
Normal file
166
core/services/assassin/assassin_tools.py
Normal 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)
|
||||
20
core/services/assassin/location_tools.py
Normal file
20
core/services/assassin/location_tools.py
Normal 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
|
||||
21
core/services/assassin/screen_size.py
Normal file
21
core/services/assassin/screen_size.py
Normal 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
|
||||
|
||||
|
||||
132
core/services/assassin/ticket_respawn.py
Normal file
132
core/services/assassin/ticket_respawn.py
Normal 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
|
||||
|
||||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
@ -40,12 +45,14 @@ def activate_subscription(
|
|||
True.
|
||||
Raises if subscription is missing or invalid. (not false)
|
||||
"""
|
||||
|
||||
|
||||
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
|
||||
|
||||
# ==================================================
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
Loading…
Reference in a new issue