forked from Support/sp-hydra-veil-gui
update: updated menu page
This commit is contained in:
parent
83fd1e08ec
commit
80407f636d
16 changed files with 1136 additions and 1539 deletions
|
|
@ -35,14 +35,20 @@ def mark_fast_mode_prompt_shown(gui_config_file):
|
|||
save_gui_config(gui_config_file, config)
|
||||
|
||||
|
||||
def is_fast_registration_enabled(gui_config_file):
|
||||
def is_fast_registration_enabled(gui_config_file: str | None) -> bool:
|
||||
"""Read fast-registration state here so pages do not inspect configuration sections themselves."""
|
||||
if not gui_config_file:
|
||||
return False
|
||||
config = load_gui_config(gui_config_file)
|
||||
if not config:
|
||||
return False
|
||||
return config.get("registrations", {}).get("fast_registration_enabled", False)
|
||||
|
||||
|
||||
def is_auto_sync_enabled(gui_config_file):
|
||||
def is_auto_sync_enabled(gui_config_file: str | None) -> bool:
|
||||
"""Read auto-sync policy here so edit actions can orchestrate synchronization without window state."""
|
||||
if not gui_config_file:
|
||||
return False
|
||||
config = load_gui_config(gui_config_file)
|
||||
if not config:
|
||||
return False
|
||||
|
|
|
|||
358
gui/v2/actions/menu_profiles.py
Executable file
358
gui/v2/actions/menu_profiles.py
Executable file
|
|
@ -0,0 +1,358 @@
|
|||
import os
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any, Callable, Iterable, Mapping
|
||||
|
||||
from core.controllers.ConfigurationController import ConfigurationController
|
||||
from core.controllers.ProfileController import ProfileController
|
||||
from core.errors.logger import logger
|
||||
from core.models.BaseProfile import BaseProfile
|
||||
from core.models.session.SessionProfile import SessionProfile
|
||||
|
||||
from gui.v2.actions.database_health import GuiStorageDatabaseError
|
||||
from gui.v2.actions.profile_order import normalize_profile_order
|
||||
from gui.v2.actions.profile_status import is_profile_enabled_for_gui
|
||||
from gui.v2.actions.settings_data import build_verification_view, truncate_key
|
||||
|
||||
|
||||
StatusCallback = Callable[[str], None]
|
||||
LocationLookup = Callable[[str], Any]
|
||||
|
||||
ENCRYPTED_PROTOCOLS = ("wireguard", "hysteria2", "vless")
|
||||
|
||||
|
||||
def _report_load_error(error: Exception, storage_message: str, fallback_message: str, on_error: StatusCallback) -> None:
|
||||
"""Report database failures consistently so every menu operation presents the same recovery advice."""
|
||||
logger.error(f"[MENU] {error}")
|
||||
message = storage_message if isinstance(error, GuiStorageDatabaseError) else fallback_message
|
||||
on_error(message)
|
||||
|
||||
|
||||
def load_profile(profile_id: int, on_error: StatusCallback) -> BaseProfile | None:
|
||||
"""Load one profile here so UI methods do not duplicate controller error handling."""
|
||||
try:
|
||||
return ProfileController.get(int(profile_id))
|
||||
except Exception as error:
|
||||
_report_load_error(
|
||||
error,
|
||||
"Local storage database could not be read. Restart and recover storage.db.",
|
||||
"Could not load profile data. Sync or restart and try again.",
|
||||
on_error,
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
def load_profiles(on_error: StatusCallback) -> dict[int, BaseProfile]:
|
||||
"""Load all profiles here so list rendering is not responsible for controller failures."""
|
||||
try:
|
||||
return ProfileController.get_all()
|
||||
except Exception as error:
|
||||
_report_load_error(
|
||||
error,
|
||||
"Local storage database could not be read. Restart and recover storage.db.",
|
||||
"Could not load profiles. Sync or restart and try again.",
|
||||
on_error,
|
||||
)
|
||||
return {}
|
||||
|
||||
|
||||
def connection_code(profile: BaseProfile | None) -> str:
|
||||
"""Normalize missing connection data once so menu rules do not repeatedly guard model attributes."""
|
||||
connection = getattr(profile, "connection", None)
|
||||
return getattr(connection, "code", None) or "unknown"
|
||||
|
||||
|
||||
def location_key(profile: BaseProfile | None) -> str:
|
||||
"""Convert both legacy dictionaries and current location models into the asset key expected by the menu."""
|
||||
location = getattr(profile, "location", None)
|
||||
if isinstance(location, dict):
|
||||
return f'{location.get("country_code") or "na"}_{location.get("code") or "na"}'
|
||||
|
||||
country_code = getattr(location, "country_code", None)
|
||||
code = getattr(location, "code", None)
|
||||
return f"{country_code}_{code}" if country_code and code else "na_na"
|
||||
|
||||
|
||||
def browser_info(profile: BaseProfile | None) -> tuple[str, str, bool]:
|
||||
"""Keep browser compatibility interpretation outside Qt because it depends only on core profile data."""
|
||||
if not isinstance(profile, SessionProfile):
|
||||
return "unknown browser", "", False
|
||||
|
||||
application_version = getattr(profile, "application_version", None)
|
||||
if isinstance(application_version, dict):
|
||||
return (
|
||||
application_version.get("application_code") or "unknown browser",
|
||||
application_version.get("version_number") or "",
|
||||
False,
|
||||
)
|
||||
|
||||
return (
|
||||
getattr(application_version, "application_code", None) or "unknown browser",
|
||||
getattr(application_version, "version_number", None) or "",
|
||||
bool(getattr(application_version, "supported", False)),
|
||||
)
|
||||
|
||||
|
||||
def profile_incomplete_reason(profile: BaseProfile | None) -> str | None:
|
||||
"""Centralize profile readiness rules so the menu and enable worker cannot disagree."""
|
||||
if profile is None:
|
||||
return "Profile data could not be loaded. Sync or restart and try again."
|
||||
if connection_code(profile) == "unknown":
|
||||
return "Profile connection data is incomplete. Sync the database and try again."
|
||||
|
||||
location = getattr(profile, "location", None)
|
||||
if location is None or isinstance(location, dict):
|
||||
return "Profile location data is incomplete. Sync the database and try again."
|
||||
if not isinstance(profile, SessionProfile):
|
||||
return None
|
||||
|
||||
application_version = getattr(profile, "application_version", None)
|
||||
if isinstance(application_version, dict) or application_version is None:
|
||||
return "Profile browser data is incomplete. Sync the database and try again."
|
||||
if not getattr(application_version, "application_code", None) or not getattr(application_version, "version_number", None):
|
||||
return "Profile browser data is incomplete. Sync the database and try again."
|
||||
return None
|
||||
|
||||
|
||||
def location_operator(profile: BaseProfile | None) -> Any | None:
|
||||
"""Hide legacy location shapes so rendering only receives a usable operator object."""
|
||||
location = getattr(profile, "location", None)
|
||||
return None if isinstance(location, dict) or location is None else getattr(location, "operator", None)
|
||||
|
||||
|
||||
def profile_is_session(profile: BaseProfile | None) -> bool:
|
||||
"""Expose profile type as a plain decision so pages do not inspect core model classes."""
|
||||
return isinstance(profile, SessionProfile)
|
||||
|
||||
|
||||
def _matched_profile(profile_id: int, profile: BaseProfile) -> tuple[str, dict[str, Any]]:
|
||||
"""Build one normalized menu record so ordering does not contain profile-shape branches."""
|
||||
protocol = connection_code(profile)
|
||||
display_protocol = protocol if protocol in ENCRYPTED_PROTOCOLS else "hidetor"
|
||||
connection_type = "browser-only" if profile_is_session(profile) else "system-wide"
|
||||
connection_labels = {
|
||||
"tor": "tor",
|
||||
"wireguard": connection_type,
|
||||
"hysteria2": connection_type,
|
||||
"vless": connection_type,
|
||||
}
|
||||
browser, browser_version, browser_supported = browser_info(profile)
|
||||
return f"Profile_{profile_id}", {
|
||||
"id": profile_id,
|
||||
"location": location_key(profile),
|
||||
"protocol": display_protocol,
|
||||
"connection": connection_labels.get(protocol, "just proxy"),
|
||||
"browser": browser,
|
||||
"browser_version": browser_version,
|
||||
"browser_supported": browser_supported,
|
||||
"dimentions": getattr(profile, "resolution", "None"),
|
||||
"name": getattr(profile, "name", None) or f"Profile {profile_id}",
|
||||
"incomplete_reason": profile_incomplete_reason(profile),
|
||||
}
|
||||
|
||||
|
||||
def match_core_profiles(profiles: Mapping[int, BaseProfile], gui_config_file: str | None) -> dict[str, dict[str, Any]]:
|
||||
"""Translate core profiles into stable menu data before any Qt widgets are created."""
|
||||
profile_ids = normalize_profile_order(gui_config_file, profiles.keys())
|
||||
matched_profiles = (
|
||||
_matched_profile(profile_id, profiles[profile_id])
|
||||
for profile_id in profile_ids
|
||||
if profile_id in profiles
|
||||
)
|
||||
return dict(matched_profiles)
|
||||
|
||||
|
||||
def _enabled_profile_kind(profile: BaseProfile, enabled: bool | None = None) -> str | None:
|
||||
"""Collapse enabled-state and profile-type checks so menu loading stays declarative."""
|
||||
try:
|
||||
is_enabled = is_profile_enabled_for_gui(profile) if enabled is None else enabled
|
||||
except Exception:
|
||||
return None
|
||||
if not is_enabled:
|
||||
return None
|
||||
if not profile_is_session(profile):
|
||||
return "system"
|
||||
return "session_tor" if connection_code(profile) == "tor" else "session"
|
||||
|
||||
|
||||
def load_menu_profiles(
|
||||
gui_config_file: str | None, on_error: StatusCallback,
|
||||
selected_id: int | None = None, state_change: Mapping[str, Any] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Provide one menu-loading action so the page does not separately fetch, order, and classify profiles."""
|
||||
profiles = load_profiles(on_error)
|
||||
state_change = state_change or {}
|
||||
changed_id = state_change.get("profile_id")
|
||||
enabled_kinds = {
|
||||
profile_id: kind
|
||||
for profile_id, profile in profiles.items()
|
||||
if (kind := _enabled_profile_kind(
|
||||
profile, state_change.get("is_enabled") if profile_id == changed_id else None)) is not None
|
||||
}
|
||||
|
||||
summaries = match_core_profiles(profiles, gui_config_file)
|
||||
selected = profiles.get(selected_id)
|
||||
details = None if selected is None else _profile_detail_data(
|
||||
selected, summaries[f"Profile_{selected_id}"], selected_id in enabled_kinds)
|
||||
return {
|
||||
"display_profiles": summaries,
|
||||
"selected_details": details,
|
||||
"background": "background_connected" if "system" in enabled_kinds.values() else "original",
|
||||
"enabled_kinds": enabled_kinds,
|
||||
}
|
||||
|
||||
|
||||
def _profile_detail_data(profile: BaseProfile | None, summary: Mapping[str, Any], is_connected: bool) -> dict[str, Any]:
|
||||
"""Resolve profile display choices once so Qt code only renders the selected presentation."""
|
||||
protocol = str(summary.get("protocol", "")).lower()
|
||||
operator = location_operator(profile)
|
||||
operator_name = getattr(operator, "name", "") if operator else ""
|
||||
connection = connection_code(profile)
|
||||
show_verification = protocol == "wireguard" and is_connected and connection == "wireguard"
|
||||
show_operator_details = operator_name != "Simplified Privacy" and protocol in ENCRYPTED_PROTOCOLS
|
||||
detail_modes = {
|
||||
(True, True): "verification",
|
||||
(True, False): "verification",
|
||||
(False, True): "operator_details",
|
||||
(False, False): "protocol_image",
|
||||
}
|
||||
display_mode = detail_modes[(show_verification, show_operator_details)]
|
||||
profile_location = getattr(profile, "location", None)
|
||||
profile_location = None if isinstance(profile_location, dict) else profile_location
|
||||
|
||||
return {
|
||||
**summary,
|
||||
"profile_loaded": profile is not None,
|
||||
"message": None if profile is None else profile_status_message(profile, True, not is_connected),
|
||||
"use_marquee": is_connected,
|
||||
"selection": {"profile_number": str(summary["id"]), "name": summary["name"], "protocol": protocol},
|
||||
"verification_values": [
|
||||
(label, value, value if label == "Operator Name" else truncate_key(value))
|
||||
for label, value in verification_values(profile)
|
||||
],
|
||||
"location_text": f"{profile_location.country_name}, {profile_location.name}" if profile_location else "",
|
||||
"background_asset": _detail_background(summary, display_mode, profile_is_session(profile)),
|
||||
"primary_asset": f"{protocol}.png" if protocol in ("hysteria2", "vless") else f"{protocol}_{summary['location']}.png",
|
||||
"location_overlay": protocol in ("hysteria2", "vless"),
|
||||
"overlay_y": {"hysteria2": 0.35}.get(protocol, 0.30),
|
||||
"raised_primary": protocol == "hidetor" or (protocol == "wireguard" and summary["connection"] == "browser-only"),
|
||||
"operator_name": operator_name,
|
||||
"nostr_public_key": getattr(operator, "nostr_public_key", "") if operator else "",
|
||||
"hydraveil_public_key": getattr(operator, "public_key", "") if operator else "",
|
||||
"is_session": profile_is_session(profile),
|
||||
"is_connected": is_connected,
|
||||
"display_mode": display_mode,
|
||||
"show_verified_icon": (
|
||||
protocol == "wireguard"
|
||||
and is_connected
|
||||
and connection == "wireguard"
|
||||
and ConfigurationController.get_endpoint_verification_enabled()
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def _detail_background(summary: Mapping[str, Any], mode: str, is_session: bool) -> str | None:
|
||||
"""Select background assets using profile policy before Qt starts drawing."""
|
||||
if mode == "verification":
|
||||
return None
|
||||
protocol = summary["protocol"]
|
||||
connection = summary["connection"]
|
||||
if protocol == "hidetor":
|
||||
prefix = "icon" if connection == "just proxy" else "hdtor"
|
||||
return f"{prefix}_{summary['location']}.png"
|
||||
if protocol == "wireguard" and connection == "browser-only":
|
||||
return "browser only.png" if summary["browser_supported"] else "unsupported_browser_only.png"
|
||||
return "browser only.png" if mode == "operator_details" and is_session else None
|
||||
|
||||
|
||||
def menu_icons(btn_path: str, summary: Mapping[str, Any]) -> list[dict[str, Any]]:
|
||||
"""Prepare icon paths and compatibility warnings without exposing protocol rules to Qt."""
|
||||
connection = summary["connection"]
|
||||
warning = connection not in ("tor", "just proxy") and not summary["browser_supported"] and summary["browser"] != "unknown browser"
|
||||
return [{"kind": kind, "path": menu_icon_path(btn_path, kind, summary, connection),
|
||||
"warning": kind == "browser" and warning} for kind in ("protocol", "location", "browser")]
|
||||
|
||||
|
||||
def get_profile_details(
|
||||
profile_id: int,
|
||||
summary: Mapping[str, Any],
|
||||
is_connected: bool,
|
||||
on_error: StatusCallback,
|
||||
) -> dict[str, Any]:
|
||||
"""Load and prepare profile details as one task so the page never coordinates core model helpers."""
|
||||
profile = load_profile(profile_id, on_error)
|
||||
current_summary = _matched_profile(profile_id, profile)[1] if profile is not None else summary
|
||||
return _profile_detail_data(profile, current_summary, is_connected)
|
||||
|
||||
|
||||
def verification_values(profile: BaseProfile | None) -> list[tuple[str, str]]:
|
||||
"""Prepare full verification values once so copy controls and displayed text cannot diverge."""
|
||||
verification = build_verification_view(profile)
|
||||
values = {
|
||||
"Operator Name": verification["operator_name"],
|
||||
"Nostr Key": verification["nostr_public_key"],
|
||||
"HydraVeil Key": verification["hydraveil_public_key"],
|
||||
}
|
||||
return [(label, value or "N/A") for label, value in values.items()]
|
||||
|
||||
|
||||
def menu_icon_path(btn_path: str, label_name: str, value: Mapping[str, Any], connection_type: str) -> str:
|
||||
"""Choose menu asset paths outside Qt so protocol mapping remains declarative and reusable."""
|
||||
protocol = str(value.get("protocol", ""))
|
||||
protocol_icons = {
|
||||
"hysteria2": "hysteria2_mini.png",
|
||||
"vless": "vless_mini.png",
|
||||
"tor": "toricon_mini.png",
|
||||
"just proxy": "just proxy_mini.png",
|
||||
}
|
||||
if label_name == "protocol":
|
||||
filename = protocol_icons.get(protocol) or protocol_icons.get(connection_type) or "wireguard_mini.png"
|
||||
return os.path.join(btn_path, filename)
|
||||
if label_name == "browser" and connection_type == "system-wide":
|
||||
return os.path.join(btn_path, "wireguard_system_wide.png")
|
||||
if label_name == "browser":
|
||||
return os.path.join(btn_path, f'{value.get(label_name, "unknown browser")} latest_mini.png')
|
||||
return os.path.join(btn_path, f'icon_mini_{value.get(label_name, "na_na")}.png')
|
||||
|
||||
|
||||
def profile_status_message(profile: BaseProfile, is_enabled: bool, idle: bool = False) -> str:
|
||||
"""Generate status text outside Qt so workers and pages report identical subscription state."""
|
||||
subscription = getattr(profile, "subscription", None)
|
||||
profile_date = getattr(subscription, "expires_at", None)
|
||||
if profile_date is None:
|
||||
return "Offline. No subscription found."
|
||||
|
||||
expiration_date = profile_date.replace(tzinfo=timezone.utc)
|
||||
time_left = expiration_date - datetime.now(timezone.utc)
|
||||
days_left = time_left.days
|
||||
hours_left, _ = divmod(time_left.seconds, 3600)
|
||||
if expiration_date < datetime.now(timezone.utc):
|
||||
return "Offline. Subscription has expired."
|
||||
if idle:
|
||||
return f"Offline. Expires in {days_left} days."
|
||||
if not is_enabled:
|
||||
return f"Profile {int(profile.id)} disabled"
|
||||
|
||||
formatted_expiration = expiration_date.strftime("%Y-%m-%d %H:%M:%S")
|
||||
return f"Profile {int(profile.id)} enabled. Expires on {formatted_expiration}. Time left: {days_left} days, {hours_left} hours."
|
||||
|
||||
def find_verification_icon(profile_id: int, btn_path: str, location_lookup: LocationLookup, on_error: StatusCallback) -> str | None:
|
||||
"""Find the matching verification asset before navigation so the page does not scan files or compare locations."""
|
||||
profile = load_profile(profile_id, on_error)
|
||||
profile_location = getattr(profile, "location", None)
|
||||
if profile is None or profile_location is None or isinstance(profile_location, dict):
|
||||
return None
|
||||
|
||||
target = location_lookup(f"{profile_location.country_code}_{profile_location.code}")
|
||||
if target is None:
|
||||
return None
|
||||
|
||||
icon_files: Iterable[str] = os.listdir(btn_path)
|
||||
icon_names = [name.removeprefix("button_").removesuffix(".png") for name in icon_files if name.startswith("button_") and name.endswith(".png")]
|
||||
matches = [
|
||||
name
|
||||
for name in icon_names
|
||||
if (candidate := location_lookup(name))
|
||||
and candidate.country_code == target.country_code
|
||||
and candidate.code == target.code
|
||||
]
|
||||
return matches[0] if matches else None
|
||||
26
gui/v2/actions/menu_results.py
Executable file
26
gui/v2/actions/menu_results.py
Executable file
|
|
@ -0,0 +1,26 @@
|
|||
from typing import Any, Callable, TypedDict
|
||||
|
||||
|
||||
class MenuResult(TypedDict, total=False):
|
||||
status: str
|
||||
message: str
|
||||
request: dict[str, Any]
|
||||
destination: str
|
||||
button_text: str
|
||||
profile_id: int
|
||||
refresh: bool
|
||||
clear_selection: bool
|
||||
is_enabled: bool
|
||||
busy: bool
|
||||
package_name: str
|
||||
ticket_number: str
|
||||
billing_code: str
|
||||
sync_data: tuple[list, list, bool, bool, list, list]
|
||||
|
||||
|
||||
MenuCallback = Callable[[MenuResult], None]
|
||||
|
||||
|
||||
def failure(message: str, request: dict[str, Any]) -> MenuResult:
|
||||
"""Keep failures explicit so the GUI never infers success from status text."""
|
||||
return {"status": "error", "message": message, "request": dict(request)}
|
||||
80
gui/v2/actions/menu_tasks.py
Executable file
80
gui/v2/actions/menu_tasks.py
Executable file
|
|
@ -0,0 +1,80 @@
|
|||
from functools import partial
|
||||
from typing import Any, Iterable, Mapping
|
||||
|
||||
from PyQt6.QtCore import Qt
|
||||
|
||||
from gui.v2.actions.menu_results import MenuCallback
|
||||
from gui.v2.actions.profile_operations import execute_menu_task
|
||||
from gui.v2.infrastructure.screen_size import get_max_screensize
|
||||
from gui.v2.workers.worker import Worker
|
||||
|
||||
|
||||
_active_workers: set[Worker] = set()
|
||||
|
||||
|
||||
def _release_worker(worker: Worker) -> None:
|
||||
"""Retain workers until their native thread finishes, including long-running browser sessions."""
|
||||
_active_workers.discard(worker)
|
||||
worker.deleteLater()
|
||||
|
||||
|
||||
def _start_task(request: Mapping[str, Any], on_result: MenuCallback) -> Worker:
|
||||
"""Own worker creation and queued GUI delivery so pages only supply task inputs."""
|
||||
operation = partial(execute_menu_task, dict(request), max_resolution=get_max_screensize())
|
||||
worker = Worker(operation)
|
||||
worker.output.connect(on_result, Qt.ConnectionType.QueuedConnection)
|
||||
worker.finished.connect(partial(_release_worker, worker))
|
||||
_active_workers.add(worker)
|
||||
worker.start()
|
||||
return worker
|
||||
|
||||
|
||||
def enable_profile(profile_data: Mapping[str, Any], on_result: MenuCallback, allow_uninstalled_tor: bool = False) -> Worker:
|
||||
"""Start the complete activation workflow with billing and retry data kept in one request."""
|
||||
return _start_task({**profile_data, "task": "enable", "allow_uninstalled_tor": allow_uninstalled_tor}, on_result)
|
||||
|
||||
|
||||
def disable_profile(profile_id: int, connected_profile_ids: Iterable[int], on_result: MenuCallback) -> Worker:
|
||||
"""Let the disable task decide whether dependent sessions require confirmation."""
|
||||
return _start_task({"task": "disable", "id": profile_id, "connected_ids": list(connected_profile_ids)}, on_result)
|
||||
|
||||
|
||||
def delete_profile(profile_id: int, on_result: MenuCallback) -> Worker:
|
||||
"""Capture the deletion target before showing a confirmation."""
|
||||
return _start_task({"task": "delete", "id": profile_id}, on_result)
|
||||
|
||||
|
||||
def create_profile(gui_config_file: str | None, is_synced: bool, on_result: MenuCallback) -> Worker:
|
||||
"""Let the create task select fast registration and perform any required synchronization."""
|
||||
return _start_task({"task": "create", "gui_config_file": gui_config_file, "is_synced": is_synced}, on_result)
|
||||
|
||||
|
||||
def edit_profile(gui_config_file: str | None, is_synced: bool, on_result: MenuCallback) -> Worker:
|
||||
"""Let the edit task finish required synchronization before requesting navigation."""
|
||||
return _start_task({"task": "edit", "gui_config_file": gui_config_file, "is_synced": is_synced}, on_result)
|
||||
|
||||
|
||||
def download_update(on_result: MenuCallback) -> Worker:
|
||||
"""Keep update startup in the action layer rather than the confirmation dialog."""
|
||||
return _start_task({"task": "update"}, on_result)
|
||||
|
||||
|
||||
def continue_menu_task(request: Mapping[str, Any], choice: str, on_result: MenuCallback) -> Worker | None:
|
||||
"""Resume the captured task without losing accepted exceptions or spending a ticket again."""
|
||||
if choice == "abort":
|
||||
on_result({"status": "cancelled", "message": "Operation cancelled"})
|
||||
return None
|
||||
updates = {
|
||||
("tor_download", "continue"): {"allow_uninstalled_tor": True},
|
||||
("billing_tor", "continue"): {"allow_clearweb": True},
|
||||
("force", "continue"): {"ignore_profile_state_conflict": True},
|
||||
("endpoint", "continue"): {"ignore_endpoint_verification": True},
|
||||
("endpoint", "sync"): {"sync_before_enable": True},
|
||||
("disable_all", "continue"): {"confirmed": True},
|
||||
("delete", "continue"): {"confirmed": True},
|
||||
}
|
||||
flags = updates.get((request.get("stage"), choice))
|
||||
if flags is None:
|
||||
on_result({"status": "error", "message": "Unknown confirmation response"})
|
||||
return None
|
||||
return _start_task({**request, **flags}, on_result)
|
||||
298
gui/v2/actions/profile_operations.py
Executable file
298
gui/v2/actions/profile_operations.py
Executable file
|
|
@ -0,0 +1,298 @@
|
|||
import time
|
||||
from datetime import datetime, timezone
|
||||
from functools import partial
|
||||
from typing import Any
|
||||
|
||||
from core.controllers.ApplicationVersionController import ApplicationVersionController
|
||||
from core.controllers.ClientController import ClientController
|
||||
from core.controllers.ConfigurationController import ConfigurationController
|
||||
from core.controllers.LocationController import LocationController
|
||||
from core.controllers.ProfileController import ProfileController
|
||||
from core.controllers.SubscriptionController import SubscriptionController
|
||||
from core.controllers.SyncController import new_sync
|
||||
from core.controllers.tickets.UseTicketController import do_we_use_a_random_ticket, get_unused_tickets, use_ticket
|
||||
from core.Errors import (
|
||||
CommandNotFoundError, EndpointVerificationError, FileIntegrityError,
|
||||
InvalidSubscriptionError, MissingSubscriptionError, ProfileActivationError,
|
||||
ProfileModificationError, ProfileStateConflictError, UnsupportedApplicationVersionError,
|
||||
)
|
||||
from core.models.BaseProfile import BaseProfile
|
||||
from core.models.Result import Result
|
||||
from core.models.session.SessionProfile import SessionProfile
|
||||
from core.models.system.SystemProfile import SystemProfile
|
||||
from core.observers.ProfileObserver import ProfileObserver
|
||||
from gui.v2.actions.database_health import GuiStorageDatabaseError
|
||||
from gui.v2.actions.disable_profiles import disable_profile_via_controller
|
||||
from gui.v2.actions.flags import is_auto_sync_enabled, is_fast_registration_enabled
|
||||
from gui.v2.actions.locations import location_candidates
|
||||
from gui.v2.actions.menu_profiles import profile_incomplete_reason, profile_status_message
|
||||
from gui.v2.actions.menu_results import MenuCallback, MenuResult, failure
|
||||
from gui.v2.infrastructure.setup_observers import (
|
||||
application_version_observer, client_observer, connection_observer,
|
||||
profile_observer, ticket_observer,
|
||||
)
|
||||
|
||||
|
||||
def current_connection() -> str:
|
||||
"""Read connection policy in the action layer instead of asking a window to interpret it."""
|
||||
return ConfigurationController.get_connection() or "system"
|
||||
|
||||
|
||||
def _require_profile(profile_id: int) -> BaseProfile:
|
||||
"""Reject missing profiles before a task can report a misleading success."""
|
||||
profile = ProfileController.get(int(profile_id))
|
||||
if profile is None:
|
||||
raise ValueError(f"No profile found with ID: {profile_id}")
|
||||
return profile
|
||||
|
||||
|
||||
def _confirmation(request: dict[str, Any], stage: str, message: str, button_text: str = "Proceed") -> MenuResult:
|
||||
"""Capture the original request so a later selection change cannot redirect confirmation."""
|
||||
return {"status": "confirmation", "message": message, "button_text": button_text, "busy": True,
|
||||
"request": {**request, "stage": stage}}
|
||||
|
||||
|
||||
def _billing_entry(profile: BaseProfile, request: dict[str, Any], message: str) -> MenuResult:
|
||||
"""Choose billing navigation without allowing a billing helper to take over activation."""
|
||||
if profile.connection.code == "tor" and current_connection() != "tor" and not request.get("allow_clearweb"):
|
||||
return _confirmation(request, "billing_tor", "The Tor profile needs a subscription; switch to Tor for billing or proceed over clearweb.")
|
||||
try:
|
||||
unused = get_unused_tickets(ticket_observer)
|
||||
setting, _ = do_we_use_a_random_ticket(ticket_observer)
|
||||
except Exception:
|
||||
unused, setting = {}, None
|
||||
has_tickets = isinstance(unused, dict) and unused.get("valid") and bool(unused.get("data"))
|
||||
destination = "ticket_or_billing_choice" if has_tickets and setting is None else "id"
|
||||
return {"status": "navigate", "destination": destination, "profile_id": request["id"],
|
||||
"message": message, "request": dict(request)}
|
||||
|
||||
|
||||
def _valid_subscription(profile: BaseProfile) -> bool:
|
||||
"""Avoid spending another ticket when an attached subscription is still valid."""
|
||||
expires = getattr(getattr(profile, "subscription", None), "expires_at", None)
|
||||
if expires is None:
|
||||
return False
|
||||
expires = expires.replace(tzinfo=timezone.utc) if expires.tzinfo is None else expires
|
||||
return expires > datetime.now(timezone.utc)
|
||||
|
||||
|
||||
def _ticket_at_location(profile: BaseProfile, ticket: int | str, location: int) -> tuple[str | None, str, bool]:
|
||||
"""Preserve returned billing codes even when ticket validation reports a recoverable failure."""
|
||||
outcome = use_ticket(which_ticket=ticket, which_location=location,
|
||||
ticket_observer=ticket_observer, connection_observer=connection_observer, profile=profile)
|
||||
if not isinstance(outcome, Result):
|
||||
return None, "Unexpected ticket result", False
|
||||
if outcome.data:
|
||||
return str(outcome.data), "", False
|
||||
message = outcome.message or getattr(outcome.error_type, "value", "") or "Ticket use failed"
|
||||
return None, message, "invalid_location" in message
|
||||
|
||||
|
||||
def _consume_ticket(profile: BaseProfile, request: dict[str, Any], ticket: int | str) -> MenuResult | None:
|
||||
"""Keep the consumed ticket and billing code together so prompt retries cannot spend it twice."""
|
||||
message = "Could not determine profile location for ticket use."
|
||||
for location in location_candidates(profile, request.get("ticket_location")):
|
||||
code, message, retry = _ticket_at_location(profile, ticket, location)
|
||||
if code:
|
||||
request.update(billing_code=code, consumed_ticket=str(ticket))
|
||||
return None
|
||||
if not retry:
|
||||
break
|
||||
return _billing_entry(profile, request, f"Ticket use failed: {message}")
|
||||
|
||||
|
||||
def _prepare_subscription(profile: BaseProfile, request: dict[str, Any]) -> MenuResult | None:
|
||||
"""Prepare billing once before activation while retaining recovery data across prompts."""
|
||||
if request.get("subscription_attached"):
|
||||
return None
|
||||
ticket = request.get("use_ticket")
|
||||
error = None
|
||||
automatic = not _valid_subscription(profile) and profile.connection.code == "wireguard" and not getattr(profile, "assassin", False)
|
||||
if not request.get("billing_code") and ticket is None and automatic:
|
||||
ticket, error = do_we_use_a_random_ticket(ticket_observer)
|
||||
if error:
|
||||
return _billing_entry(profile, request, f"Ticket use failed: {error}")
|
||||
result = None
|
||||
if not request.get("billing_code") and ticket not in (None, "error"):
|
||||
result = _consume_ticket(profile, request, ticket)
|
||||
if result is not None:
|
||||
return result
|
||||
if not request.get("billing_code"):
|
||||
return None
|
||||
try:
|
||||
subscription = SubscriptionController.get(request["billing_code"], connection_observer=connection_observer)
|
||||
if subscription is not None:
|
||||
ProfileController.attach_subscription(profile, subscription)
|
||||
request["subscription_attached"] = True
|
||||
return None
|
||||
except Exception as error:
|
||||
return _subscription_failure(profile, request, str(error))
|
||||
return _subscription_failure(profile, request, "The billing code is invalid.")
|
||||
|
||||
|
||||
def _subscription_failure(profile: BaseProfile, request: dict[str, Any], message: str) -> MenuResult:
|
||||
"""Expose the ticket-derived billing code for recovery instead of silently losing it on failure."""
|
||||
if request.get("consumed_ticket"):
|
||||
return {"status": "ticket_data_loss", "ticket_number": request["consumed_ticket"],
|
||||
"billing_code": request["billing_code"], "request": dict(request)}
|
||||
return _billing_entry(profile, request, message)
|
||||
|
||||
|
||||
def _profile_event(emit: MenuCallback, enabled: bool, event: Any) -> None:
|
||||
"""Deliver live browser-session state without leaving permanent callbacks on the global observer."""
|
||||
profile_observer.notify("enabled" if enabled else "disabled", event.subject, event.meta or {})
|
||||
emit({"status": "state", "is_enabled": enabled, "profile_id": event.subject.id,
|
||||
"message": profile_status_message(event.subject, enabled), "refresh": True})
|
||||
|
||||
|
||||
def _enable_error(error: Exception, profile: BaseProfile, request: dict[str, Any]) -> MenuResult:
|
||||
"""Translate core errors into UI instructions while retaining the complete activation request."""
|
||||
if isinstance(error, EndpointVerificationError):
|
||||
return {"status": "verification", "busy": True, "message": "Operator verification failed. Do you want to continue anyway?",
|
||||
"request": {**request, "stage": "endpoint"}}
|
||||
if isinstance(error, ProfileStateConflictError):
|
||||
return _confirmation(request, "force", "The profile is already enabled. Do you want to force enable it anyway?")
|
||||
if isinstance(error, (InvalidSubscriptionError, MissingSubscriptionError)):
|
||||
return _billing_entry(profile, request, f"Subscription missing or invalid for profile {request['id']}")
|
||||
if isinstance(error, CommandNotFoundError):
|
||||
return {"status": "install", "package_name": str(error.subject), "request": dict(request)}
|
||||
messages = {
|
||||
ProfileActivationError: "The profile could not be enabled",
|
||||
UnsupportedApplicationVersionError: "The application version in question is not supported",
|
||||
FileIntegrityError: "Application version file integrity could not be verified.",
|
||||
ProfileModificationError: "WireGuard configuration could not be attached.",
|
||||
}
|
||||
return {**failure(messages.get(type(error), str(error) or "An unknown error occurred"), request), "refresh": True}
|
||||
|
||||
|
||||
def _enable(request: dict[str, Any], emit: MenuCallback, max_resolution: str | None) -> MenuResult:
|
||||
"""Own readiness, billing, activation and recovery as one connect task."""
|
||||
synced = synchronize(current_connection()) if request.pop("sync_before_enable", False) else None
|
||||
if synced is not None:
|
||||
emit({**synced, "busy": True})
|
||||
if synced is not None and synced["status"] == "error":
|
||||
return failure("Sync failed; the profile was not retried.", request)
|
||||
profile = _require_profile(request["id"])
|
||||
incomplete = profile_incomplete_reason(profile)
|
||||
if incomplete:
|
||||
return failure(incomplete, request)
|
||||
if request.get("stage") == "billing_tor":
|
||||
return _billing_entry(profile, request, "Enter a billing code or use a ticket.")
|
||||
needs_download = profile.connection.code == "tor" and not getattr(getattr(profile, "application_version", None), "installed", False)
|
||||
if needs_download and current_connection() != "tor" and not request.get("allow_uninstalled_tor"):
|
||||
return _confirmation(request, "tor_download", "The Tor browser is not installed; switch to Tor to download it or proceed over clearweb.")
|
||||
request["allow_uninstalled_tor"] = True
|
||||
preparation = _prepare_subscription(profile, request)
|
||||
if preparation is not None:
|
||||
return preparation
|
||||
ignored = {"ignore_endpoint_verification": EndpointVerificationError,
|
||||
"ignore_profile_state_conflict": ProfileStateConflictError}
|
||||
observer = ProfileObserver()
|
||||
observer.subscribe("enabled", partial(_profile_event, emit, True))
|
||||
observer.subscribe("disabled", partial(_profile_event, emit, False))
|
||||
try:
|
||||
ProfileController.enable(profile, ignore=tuple(error for flag, error in ignored.items() if request.get(flag)),
|
||||
profile_observer=observer, application_version_observer=application_version_observer,
|
||||
connection_observer=connection_observer, ticket_observer=ticket_observer,
|
||||
max_resolution=max_resolution)
|
||||
except Exception as error:
|
||||
return _enable_error(error, profile, request)
|
||||
return {"status": "success", "request": dict(request), "refresh": True}
|
||||
|
||||
|
||||
def synchronize(connection: str) -> MenuResult:
|
||||
"""Report sync success explicitly so failed synchronization never triggers a retry or navigation."""
|
||||
try:
|
||||
ConfigurationController.set_connection(connection)
|
||||
outcome = new_sync(client_observer=client_observer, connection_observer=connection_observer)
|
||||
if not outcome.valid:
|
||||
raise ValueError(outcome.message or "Connection error or invalid sync URL")
|
||||
locations = LocationController.get_all()
|
||||
browsers = ApplicationVersionController.get_all()
|
||||
return {"status": "sync", "sync_data": (
|
||||
[f"{item.country_code}_{item.code}" for item in locations],
|
||||
[f"{item.application_code}:{item.version_number}" for item in browsers if item.supported],
|
||||
True, connection == "tor", locations, browsers)}
|
||||
except Exception as error:
|
||||
return {"status": "error", "message": f"Sync failed: {error}", "sync_data": ([], [], False, False, [], [])}
|
||||
|
||||
|
||||
def _disable(request: dict[str, Any]) -> MenuResult:
|
||||
"""Disable dependent browser sessions before their system connection."""
|
||||
profile = _require_profile(request["id"])
|
||||
connected = request.get("connected_ids", [])
|
||||
needs_all = isinstance(profile, SystemProfile) and len(connected) > 1
|
||||
if needs_all and not request.get("confirmed"):
|
||||
return _confirmation(request, "disable_all", "All connected session profiles will be disconnected. Do you want to proceed?", "Disable")
|
||||
profiles = [_require_profile(profile_id) for profile_id in connected] if needs_all else [profile]
|
||||
return _disable_profiles(profiles)
|
||||
|
||||
|
||||
def _disable_profiles(profiles: list[BaseProfile]) -> MenuResult:
|
||||
"""Share ordered teardown with legacy worker callers without duplicating core operations."""
|
||||
sessions = [item for item in profiles if isinstance(item, SessionProfile)]
|
||||
systems = [item for item in profiles if isinstance(item, SystemProfile)]
|
||||
if len(sessions) + len(systems) != len(profiles):
|
||||
return failure("Unsupported profile type", {})
|
||||
for item in sessions:
|
||||
disable_profile_via_controller(item)
|
||||
if sessions and systems:
|
||||
time.sleep(1)
|
||||
for item in systems:
|
||||
disable_profile_via_controller(item)
|
||||
return {"status": "success", "message": "Profiles disabled", "refresh": True}
|
||||
|
||||
|
||||
def _delete(request: dict[str, Any]) -> MenuResult:
|
||||
"""Delete only the confirmed profile and report success separately from display text."""
|
||||
if not request.get("confirmed"):
|
||||
return _confirmation(request, "delete", "Are you sure you want to delete this profile?", "Delete")
|
||||
profile = _require_profile(request["id"])
|
||||
ProfileController.destroy(profile, profile_observer, ticket_observer, connection_observer)
|
||||
return {"status": "success", "message": f"Profile {profile.id} deleted", "profile_id": profile.id,
|
||||
"refresh": True, "clear_selection": True}
|
||||
|
||||
|
||||
def _navigate(request: dict[str, Any], emit: MenuCallback) -> MenuResult:
|
||||
"""Keep fast-registration and edit synchronization policy out of page signal handlers."""
|
||||
fast = is_fast_registration_enabled(request.get("gui_config_file"))
|
||||
auto = is_auto_sync_enabled(request.get("gui_config_file"))
|
||||
create = request["task"] == "create"
|
||||
synced = request.get("is_synced", False)
|
||||
needs_sync = not synced and ((create and fast) or (not create and auto))
|
||||
result = synchronize(current_connection()) if needs_sync else None
|
||||
if result is not None:
|
||||
emit({**result, "busy": True})
|
||||
synced = result["status"] != "error"
|
||||
if result is not None and not synced:
|
||||
return failure("Sync failed. Please try again later.", request)
|
||||
destinations = {(True, True): "fast_registration", (True, False): "fast_registration",
|
||||
(False, True): "protocol", (False, False): "sync_screen"}
|
||||
destination = destinations[(fast, synced)] if create else "editor"
|
||||
return {"status": "navigate", "destination": destination}
|
||||
|
||||
|
||||
def _update() -> MenuResult:
|
||||
"""Delegate the update to core while keeping worker startup outside the prompt."""
|
||||
ClientController.update(client_observer=client_observer)
|
||||
return {"status": "success", "message": "Restart client to apply update."}
|
||||
|
||||
|
||||
def execute_menu_task(request: dict[str, Any], emit: MenuCallback, max_resolution: str | None) -> MenuResult:
|
||||
"""Dispatch background operations independently of Qt and normalize their failure results."""
|
||||
handlers = {"enable": partial(_enable, request, emit, max_resolution), "disable": partial(_disable, request),
|
||||
"disable_all": lambda: _disable_profiles([_require_profile(profile_id) for profile_id in request["connected_ids"]]),
|
||||
"delete": partial(_delete, request), "create": partial(_navigate, request, emit),
|
||||
"edit": partial(_navigate, request, emit), "update": _update}
|
||||
try:
|
||||
return handlers[request["task"]]()
|
||||
except GuiStorageDatabaseError:
|
||||
message = "Local storage database could not be read. Restart and recover storage.db."
|
||||
if request["task"] == "delete":
|
||||
message = f"Could not delete profile {request['id']}: {message}"
|
||||
return failure(message, request)
|
||||
except Exception as error:
|
||||
message = str(error) or "The operation failed."
|
||||
if request["task"] == "delete":
|
||||
message = f"Could not delete profile {request['id']}: {message}"
|
||||
return {**failure(message, request), "refresh": True}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
import json
|
||||
import os
|
||||
from typing import Any
|
||||
|
||||
from core.Constants import Constants
|
||||
from core.controllers.ConfigurationController import ConfigurationController
|
||||
|
|
@ -112,7 +113,8 @@ def extract_endpoint_ip(profile):
|
|||
return None
|
||||
|
||||
|
||||
def truncate_key(text, max_length=50):
|
||||
def truncate_key(text: str, max_length: int = 50) -> str:
|
||||
"""Shorten long keys only for display so copy actions can retain the complete value."""
|
||||
if not text or text == "N/A" or len(text) <= max_length:
|
||||
return text
|
||||
start_len = max_length // 2 - 2
|
||||
|
|
@ -120,11 +122,10 @@ def truncate_key(text, max_length=50):
|
|||
return text[:start_len] + "....." + text[-end_len:]
|
||||
|
||||
|
||||
def build_verification_view(profile):
|
||||
operator = None
|
||||
def build_verification_view(profile: Any | None) -> dict[str, str]:
|
||||
"""Normalize operator verification fields so every screen displays the same key values."""
|
||||
location = getattr(profile, 'location', None) if profile else None
|
||||
if location and not isinstance(location, dict) and getattr(location, 'operator', None):
|
||||
operator = location.operator
|
||||
operator = None if isinstance(location, dict) or location is None else getattr(location, 'operator', None)
|
||||
if not operator:
|
||||
return {
|
||||
"operator_name": "N/A",
|
||||
|
|
@ -132,11 +133,15 @@ def build_verification_view(profile):
|
|||
"hydraveil_public_key": "N/A",
|
||||
"nostr_attestation_event_reference": "N/A",
|
||||
}
|
||||
field_attributes = {
|
||||
"operator_name": "name",
|
||||
"nostr_public_key": "nostr_public_key",
|
||||
"hydraveil_public_key": "public_key",
|
||||
"nostr_attestation_event_reference": "nostr_attestation_event_reference",
|
||||
}
|
||||
return {
|
||||
"operator_name": operator.name or "N/A",
|
||||
"nostr_public_key": operator.nostr_public_key or "N/A",
|
||||
"hydraveil_public_key": operator.public_key or "N/A",
|
||||
"nostr_attestation_event_reference": operator.nostr_attestation_event_reference or "N/A",
|
||||
field: getattr(operator, attribute, None) or "N/A"
|
||||
for field, attribute in field_attributes.items()
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -42,6 +42,10 @@ class ConnectionManager:
|
|||
def get_connected_profiles(self):
|
||||
return list(self._connected_profiles)
|
||||
|
||||
def set_connected_profiles(self, profile_ids):
|
||||
"""Replace the cached connection set atomically so pages never mutate private manager state."""
|
||||
self._connected_profiles = set(profile_ids)
|
||||
|
||||
def set_synced(self, status):
|
||||
self._is_synced = status
|
||||
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ from gui.v2.ui.pages.location_page import LocationPage
|
|||
from gui.v2.ui.pages.screen_page import ScreenPage
|
||||
from gui.v2.ui.popups.confirmation_popup import ConfirmationPopup
|
||||
from gui.v2.workers.page_data_worker import PageDataWorker
|
||||
from gui.v2.actions.menu_profiles import match_core_profiles
|
||||
|
||||
|
||||
class EditorPage(Page):
|
||||
|
|
@ -185,8 +186,10 @@ class EditorPage(Page):
|
|||
new_profiles = self._prefetched_profiles
|
||||
else:
|
||||
new_profiles = ProfileController.get_all()
|
||||
self.profiles_data = menu_page.match_core_profiles(
|
||||
profiles_dict=new_profiles)
|
||||
self.profiles_data = match_core_profiles(
|
||||
new_profiles,
|
||||
getattr(self.update_status, "gui_config_file", None),
|
||||
)
|
||||
self.data_profile = self.profiles_data[selected_profiles_str].copy(
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ from core.services.prepare_tickets.ticket_tracker import delete_ticket_data
|
|||
|
||||
from gui.v2.ui.pages.Page import Page
|
||||
from gui.v2.ui.popups.message_box import style_message_box, mark_confirm_button
|
||||
from gui.v2.actions.menu_tasks import enable_profile as enable_profile_action
|
||||
|
||||
|
||||
HOW_MANY_PROFILES_DEFAULT = 6
|
||||
|
|
@ -49,7 +50,9 @@ class IdPage(Page):
|
|||
}
|
||||
menu_page = self.find_menu_page()
|
||||
if menu_page:
|
||||
menu_page.enabling_profile(profile_data)
|
||||
self.profile_enable_worker = enable_profile_action(
|
||||
profile_data, menu_page.handle_task_result, allow_uninstalled_tor=True,
|
||||
)
|
||||
|
||||
else:
|
||||
self.update_status.update_status(
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -5,6 +5,7 @@ from core.controllers.ProfileController import ProfileController
|
|||
from core.controllers.tickets.UseTicketController import get_unused_tickets
|
||||
|
||||
from gui.v2.actions.locations import location_candidates
|
||||
from gui.v2.actions.menu_tasks import enable_profile as enable_profile_action
|
||||
from gui.v2.infrastructure.setup_observers import ticket_observer
|
||||
from gui.v2.ui.pages.Page import Page
|
||||
|
||||
|
|
@ -103,13 +104,14 @@ class TicketOrBillingChoicePage(Page):
|
|||
profile_data = {
|
||||
'id': int(profile_id),
|
||||
'use_ticket': which_ticket,
|
||||
'ticket_location': candidates[0],
|
||||
'profile': profile
|
||||
'ticket_location': candidates[0]
|
||||
}
|
||||
menu_page = self.custom_window.navigator.get_cached("menu")
|
||||
if menu_page:
|
||||
self.update_status.update_status(f"Using ticket #{which_ticket}...")
|
||||
menu_page.enabling_profile(profile_data)
|
||||
self.profile_enable_worker = enable_profile_action(
|
||||
profile_data, menu_page.handle_task_result, allow_uninstalled_tor=True,
|
||||
)
|
||||
self.custom_window.navigator.navigate("menu")
|
||||
|
||||
def on_use_billing(self):
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ from PyQt6.QtWidgets import QLabel, QPushButton
|
|||
from PyQt6 import QtCore
|
||||
|
||||
from gui.v2.infrastructure.setup_observers import ticket_observer
|
||||
from gui.v2.actions.menu_tasks import enable_profile as enable_profile_action
|
||||
from gui.v2.ui.pages.Page import Page
|
||||
from gui.v2.ui.widgets.terminal_widget import TerminalWidget
|
||||
from gui.v2.workers.ticketing_worker_thread import TicketingWorkerThread
|
||||
|
|
@ -114,4 +115,6 @@ class TicketPrepPage(Page):
|
|||
self.update_status.update_status("Tickets ready. Random ticket use is ON.")
|
||||
return
|
||||
self.update_status.update_status("Applying random ticket to profile...")
|
||||
menu_page.enabling_profile({'id': int(profile_id)})
|
||||
self.profile_enable_worker = enable_profile_action(
|
||||
{'id': int(profile_id)}, menu_page.handle_task_result, allow_uninstalled_tor=True,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ class ConfirmationPopup(QWidget):
|
|||
def __init__(self, parent=None, message="", action_button_text="", cancel_button_text="Cancel"):
|
||||
super().__init__(parent)
|
||||
self.parent_window = parent
|
||||
self._resolved = False
|
||||
self.message = message
|
||||
self.action_button_text = action_button_text
|
||||
self.cancel_button_text = cancel_button_text
|
||||
|
|
@ -79,9 +80,16 @@ class ConfirmationPopup(QWidget):
|
|||
button_layout.addWidget(action_button)
|
||||
|
||||
def perform_action(self):
|
||||
self._resolved = True
|
||||
self.finished.emit(True)
|
||||
self.close()
|
||||
|
||||
def closeEvent(self, event):
|
||||
if not self._resolved:
|
||||
self._resolved = True
|
||||
self.finished.emit(False)
|
||||
event.accept()
|
||||
|
||||
def mousePressEvent(self, event):
|
||||
self.oldPos = event.globalPosition().toPoint()
|
||||
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ class EndpointVerificationPopup(QWidget):
|
|||
def __init__(self, parent=None, message=""):
|
||||
super().__init__(parent)
|
||||
self.parent_window = parent
|
||||
self._resolved = False
|
||||
self.message = message
|
||||
self.initUI()
|
||||
|
||||
|
|
@ -106,10 +107,13 @@ class EndpointVerificationPopup(QWidget):
|
|||
button_layout.addWidget(continue_button)
|
||||
|
||||
def close_with_action(self, action):
|
||||
self._resolved = True
|
||||
self.finished.emit(True, action)
|
||||
self.close()
|
||||
|
||||
def closeEvent(self, event):
|
||||
if not self._resolved:
|
||||
self._resolved = True
|
||||
self.finished.emit(False, "abort")
|
||||
event.accept()
|
||||
|
||||
|
|
|
|||
|
|
@ -1,303 +1,16 @@
|
|||
from datetime import datetime, timezone
|
||||
from typing import Callable
|
||||
|
||||
from PyQt6.QtCore import QObject, pyqtSignal
|
||||
from PyQt6.QtCore import QThread, pyqtSignal
|
||||
|
||||
from core.controllers.ProfileController import ProfileController
|
||||
from core.controllers.SubscriptionController import SubscriptionController
|
||||
from core.controllers.tickets.UseTicketController import (
|
||||
use_ticket,
|
||||
do_we_use_a_random_ticket,
|
||||
)
|
||||
from core.models.session.SessionProfile import SessionProfile
|
||||
from core.models.system.SystemProfile import SystemProfile
|
||||
from core.models.Result import Result, ResultError
|
||||
from core.errors.exceptions import SudoScript, MissingPreReqs, FirewallError
|
||||
from core.Errors import (
|
||||
CommandNotFoundError,
|
||||
EndpointVerificationError,
|
||||
FileIntegrityError,
|
||||
InvalidSubscriptionError,
|
||||
MissingSubscriptionError,
|
||||
ProfileActivationError,
|
||||
ProfileModificationError,
|
||||
ProfileStateConflictError,
|
||||
UnsupportedApplicationVersionError,
|
||||
)
|
||||
|
||||
from gui.v2.actions.locations import location_candidates
|
||||
from gui.v2.actions.database_health import GuiStorageDatabaseError
|
||||
from gui.v2.infrastructure.screen_size import get_max_screensize
|
||||
from gui.v2.infrastructure.setup_observers import (
|
||||
application_version_observer,
|
||||
connection_observer,
|
||||
profile_observer,
|
||||
ticket_observer,
|
||||
)
|
||||
from gui.v2.actions.menu_results import MenuCallback, MenuResult
|
||||
|
||||
|
||||
class Worker(QObject):
|
||||
update_signal = pyqtSignal(str, bool, int, int, str)
|
||||
change_page = pyqtSignal(str, bool)
|
||||
ticket_data_loss = pyqtSignal(str, str)
|
||||
class Worker(QThread):
|
||||
output = pyqtSignal(object)
|
||||
|
||||
def __init__(self, profile_data):
|
||||
self.profile_data = profile_data
|
||||
def __init__(self, operation: Callable[[MenuCallback], MenuResult]):
|
||||
super().__init__()
|
||||
profile_observer.subscribe(
|
||||
'disabled', lambda event: self.handle_profile_status(event.subject, False))
|
||||
profile_observer.subscribe(
|
||||
'enabled', lambda event: self.handle_profile_status(event.subject, True))
|
||||
self.profile_type = None
|
||||
self._ticket_error_emitted = False
|
||||
self._consumed_ticket = None
|
||||
self.operation = operation
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
self.profile = ProfileController.get(int(self.profile_data['id']))
|
||||
except GuiStorageDatabaseError:
|
||||
self.update_signal.emit(
|
||||
"Local storage database could not be read. Restart and recover storage.db.", False, None, None, None)
|
||||
return
|
||||
except Exception:
|
||||
self.update_signal.emit(
|
||||
"Could not load profile data. Sync or restart and try again.", False, None, None, None)
|
||||
return
|
||||
|
||||
incomplete_reason = self._profile_incomplete_reason()
|
||||
if incomplete_reason:
|
||||
self.update_signal.emit(incomplete_reason, False, None, None, None)
|
||||
return
|
||||
|
||||
if 'use_ticket' in self.profile_data:
|
||||
ticket_billing_code = self._consume_ticket(
|
||||
self.profile_data['use_ticket'],
|
||||
self.profile_data.get('ticket_location'))
|
||||
if ticket_billing_code is None:
|
||||
return
|
||||
self.profile_data['billing_code'] = ticket_billing_code
|
||||
elif 'billing_code' not in self.profile_data and self.profile is not None:
|
||||
if not self._profile_has_valid_subscription():
|
||||
ticket_billing_code = self._maybe_auto_use_ticket()
|
||||
if ticket_billing_code is None and self._ticket_error_emitted:
|
||||
return
|
||||
if ticket_billing_code:
|
||||
self.profile_data['billing_code'] = ticket_billing_code
|
||||
|
||||
if 'billing_code' in self.profile_data:
|
||||
subscription = SubscriptionController.get(
|
||||
self.profile_data['billing_code'], connection_observer=connection_observer)
|
||||
if subscription is not None:
|
||||
ProfileController.attach_subscription(
|
||||
self.profile, subscription)
|
||||
else:
|
||||
if self._consumed_ticket is not None:
|
||||
self._ticket_error_emitted = True
|
||||
self.ticket_data_loss.emit(
|
||||
self._consumed_ticket,
|
||||
str(self.profile_data.get('billing_code', '')),
|
||||
)
|
||||
return
|
||||
self.change_page.emit('The billing code is invalid.', True)
|
||||
return
|
||||
if self.profile:
|
||||
try:
|
||||
ignore_exceptions = []
|
||||
if self.profile_data.get('ignore_endpoint_verification', False):
|
||||
ignore_exceptions.append(EndpointVerificationError)
|
||||
if self.profile_data.get('ignore_profile_state_conflict', False):
|
||||
ignore_exceptions.append(ProfileStateConflictError)
|
||||
ignore_tuple = tuple(ignore_exceptions)
|
||||
max_resolution = get_max_screensize()
|
||||
ProfileController.enable(self.profile, ignore=ignore_tuple, profile_observer=profile_observer,
|
||||
application_version_observer=application_version_observer,
|
||||
connection_observer=connection_observer, ticket_observer=ticket_observer, max_resolution=max_resolution)
|
||||
except EndpointVerificationError:
|
||||
self.update_signal.emit(
|
||||
"ENDPOINT_VERIFICATION_ERROR", False, self.profile_data['id'], None, None)
|
||||
except (InvalidSubscriptionError, MissingSubscriptionError) as e:
|
||||
self.change_page.emit(
|
||||
f"Subscription missing or invalid for profile {self.profile_data['id']}", True)
|
||||
except ProfileActivationError:
|
||||
self.update_signal.emit(
|
||||
"The profile could not be enabled", False, None, None, None)
|
||||
except UnsupportedApplicationVersionError:
|
||||
self.update_signal.emit(
|
||||
"The application version in question is not supported", False, None, None, None)
|
||||
except FileIntegrityError:
|
||||
self.update_signal.emit(
|
||||
"Application version file integrity could not be verified.", False, None, None, None)
|
||||
except ProfileModificationError:
|
||||
self.update_signal.emit(
|
||||
"WireGuard configuration could not be attached.", False, None, None, None)
|
||||
except ProfileStateConflictError:
|
||||
self.update_signal.emit(
|
||||
"PROFILE_STATE_CONFLICT_ERROR", False, self.profile_data['id'], None, None)
|
||||
except CommandNotFoundError as e:
|
||||
self.update_signal.emit(str(e.subject), False, -1, None, None)
|
||||
except SudoScript as e:
|
||||
self.update_signal.emit(str(e), False, None, None, None)
|
||||
except MissingPreReqs as e:
|
||||
self.update_signal.emit(str(e), False, None, None, None)
|
||||
except FirewallError as e:
|
||||
self.update_signal.emit(str(e), False, None, None, None)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
self.update_signal.emit(
|
||||
"An unknown error occurred", False, None, None, None)
|
||||
|
||||
else:
|
||||
self.update_signal.emit(
|
||||
f"No profile found with ID: {self.profile_data['id']}", False, None, None, None)
|
||||
|
||||
def _profile_incomplete_reason(self):
|
||||
if self.profile is None:
|
||||
return None
|
||||
|
||||
connection = getattr(self.profile, 'connection', None)
|
||||
if not getattr(connection, 'code', None):
|
||||
return "Profile connection data is incomplete. Sync the database and try again."
|
||||
|
||||
location = getattr(self.profile, 'location', None)
|
||||
if location is None or isinstance(location, dict):
|
||||
return "Profile location data is incomplete. Sync the database and try again."
|
||||
|
||||
if isinstance(self.profile, SessionProfile):
|
||||
application_version = getattr(self.profile, 'application_version', None)
|
||||
if isinstance(application_version, dict) or application_version is None:
|
||||
return "Profile browser data is incomplete. Sync the database and try again."
|
||||
if not getattr(application_version, 'application_code', None) or not getattr(application_version, 'version_number', None):
|
||||
return "Profile browser data is incomplete. Sync the database and try again."
|
||||
|
||||
return None
|
||||
|
||||
def _location_candidates(self, preferred=None):
|
||||
return location_candidates(self.profile, preferred)
|
||||
|
||||
def _profile_has_valid_subscription(self):
|
||||
try:
|
||||
sub = getattr(self.profile, 'subscription', None)
|
||||
if not sub:
|
||||
return False
|
||||
expires_at = getattr(sub, 'expires_at', None)
|
||||
if not expires_at:
|
||||
return False
|
||||
if expires_at.tzinfo is None:
|
||||
expires_at = expires_at.replace(tzinfo=timezone.utc)
|
||||
return expires_at > datetime.now(timezone.utc)
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def _maybe_auto_use_ticket(self):
|
||||
profile_connection_type = self.profile.connection.code
|
||||
|
||||
if profile_connection_type != "wireguard":
|
||||
error_msg = "Tickets are not yet supported for Tor. We are very sorry, but we're not able to develop all features for all products."
|
||||
print(error_msg)
|
||||
return None
|
||||
|
||||
if self.profile.assassin:
|
||||
print("skipping random ticket use for the assassin, as it already has it baked in.")
|
||||
return None
|
||||
try:
|
||||
print("using a random ticket from GUI...")
|
||||
which_ticket, error_msg = do_we_use_a_random_ticket(ticket_observer)
|
||||
except Exception:
|
||||
return None
|
||||
if error_msg:
|
||||
self._ticket_error_emitted = True
|
||||
self.change_page.emit(f'Ticket use failed: {error_msg}', True)
|
||||
return None
|
||||
if which_ticket is None or which_ticket == 'error':
|
||||
return None
|
||||
return self._consume_ticket(which_ticket, None)
|
||||
|
||||
def _consume_ticket(self, which_ticket, which_location):
|
||||
candidates = self._location_candidates(preferred=which_location)
|
||||
if not candidates:
|
||||
self._ticket_error_emitted = True
|
||||
self.change_page.emit('Could not determine profile location for ticket use.', True)
|
||||
return None
|
||||
|
||||
last_msg = None
|
||||
for cand in candidates:
|
||||
try:
|
||||
outcome = use_ticket(
|
||||
which_ticket=which_ticket,
|
||||
which_location=cand,
|
||||
ticket_observer=ticket_observer,
|
||||
connection_observer=connection_observer,
|
||||
profile=self.profile
|
||||
)
|
||||
except Exception as e:
|
||||
last_msg = str(e)
|
||||
continue
|
||||
if not isinstance(outcome, Result):
|
||||
last_msg = f'Unexpected ticket result type: {type(outcome).__name__}'
|
||||
self._ticket_error_emitted = True
|
||||
self.change_page.emit(f'Ticket use failed: {last_msg}', True)
|
||||
return None
|
||||
|
||||
billing_code = outcome.data
|
||||
if outcome.valid and billing_code:
|
||||
self._consumed_ticket = str(which_ticket)
|
||||
return billing_code
|
||||
if billing_code:
|
||||
self._consumed_ticket = str(which_ticket)
|
||||
return billing_code
|
||||
|
||||
msg = outcome.message
|
||||
if not msg and outcome.error_type:
|
||||
msg = outcome.error_type.value
|
||||
msg = msg or 'Ticket use failed.'
|
||||
last_msg = msg
|
||||
if 'invalid_location' not in msg:
|
||||
self._ticket_error_emitted = True
|
||||
self.change_page.emit(f'Ticket use failed: {msg}', True)
|
||||
return None
|
||||
|
||||
self._ticket_error_emitted = True
|
||||
self.change_page.emit(f'Ticket use failed: {last_msg or "no valid location"}', True)
|
||||
return None
|
||||
|
||||
def handle_profile_status(self, profile, is_enabled):
|
||||
profile_id = profile.id
|
||||
profile_connection = str(profile.connection.code)
|
||||
message = self.generate_profile_message(profile, is_enabled)
|
||||
|
||||
if isinstance(profile, SessionProfile):
|
||||
self.profile_type = 1
|
||||
elif isinstance(profile, SystemProfile):
|
||||
self.profile_type = 2
|
||||
else:
|
||||
self.profile_type = None
|
||||
|
||||
self.update_signal.emit(
|
||||
message, is_enabled, profile_id, self.profile_type, profile_connection)
|
||||
|
||||
@staticmethod
|
||||
def generate_profile_message(profile, is_enabled, idle=False):
|
||||
|
||||
profile_id = profile.id
|
||||
if not profile.subscription or not profile.subscription.expires_at:
|
||||
return f"Offline. No subscription found."
|
||||
|
||||
profile_date = profile.subscription.expires_at
|
||||
status = 'enabled' if is_enabled else 'disabled'
|
||||
|
||||
expiration_date = profile_date.replace(tzinfo=timezone.utc)
|
||||
|
||||
time_left = expiration_date - datetime.now(timezone.utc)
|
||||
days_left = time_left.days
|
||||
hours_left, remainder = divmod(time_left.seconds, 3600)
|
||||
|
||||
formatted_expiration = expiration_date.strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
if expiration_date < datetime.now(timezone.utc):
|
||||
return "Offline. Subscription has expired."
|
||||
if idle:
|
||||
return f"Offline. Expires in {days_left} days."
|
||||
|
||||
if is_enabled:
|
||||
return f"Profile {int(profile_id)} {status}. Expires on {formatted_expiration}. Time left: {days_left} days, {hours_left} hours."
|
||||
else:
|
||||
return f"Profile {int(profile_id)} {status}"
|
||||
self.output.emit(self.operation(self.output.emit))
|
||||
|
|
|
|||
|
|
@ -1,14 +1,11 @@
|
|||
import shlex
|
||||
import subprocess
|
||||
# import inspect
|
||||
import time
|
||||
|
||||
from PyQt6.QtCore import QThread, pyqtSignal
|
||||
|
||||
from core.controllers.ApplicationVersionController import ApplicationVersionController
|
||||
from core.controllers.ClientController import ClientController
|
||||
from core.controllers.ConfigurationController import ConfigurationController, ConnectionChoice
|
||||
from core.Constants import Constants
|
||||
from core.controllers.ConfigurationController import ConfigurationController
|
||||
|
||||
from core.controllers.SyncController import new_sync
|
||||
from core.controllers.InvoiceController import InvoiceController
|
||||
|
|
@ -21,14 +18,11 @@ from core.models.session.SessionProfile import SessionProfile
|
|||
from core.models.system.SystemConnection import SystemConnection
|
||||
from core.models.BaseProfile import ProfileType
|
||||
from core.models.system.SystemProfile import SystemProfile
|
||||
from core.errors.exceptions import SudoScript, MissingPreReqs, FirewallError
|
||||
from core.models.Result import Result, ResultError
|
||||
from core.models.Result import ResultError
|
||||
from core.services.helpers.install_dependencies import setup_singbox_binary as install_singbox_binary
|
||||
|
||||
from gui.v2.actions.disable_profiles import (
|
||||
filter_profiles_by_type,
|
||||
disable_profile_via_controller
|
||||
)
|
||||
from gui.v2.actions.profile_operations import execute_menu_task, synchronize
|
||||
from gui.v2.actions.menu_results import MenuResult
|
||||
|
||||
from gui.v2.infrastructure.setup_observers import (
|
||||
application_version_observer,
|
||||
|
|
@ -61,45 +55,26 @@ class WorkerThread(QThread):
|
|||
self.is_running = True
|
||||
self.is_disabling = False
|
||||
|
||||
# def _disable_profile(self, profile):
|
||||
# kwargs = {
|
||||
# 'profile_observer': profile_observer,
|
||||
# 'ticket_observer': ticket_observer,
|
||||
# 'connection_observer': connection_observer,
|
||||
# }
|
||||
# supported = inspect.signature(ProfileController.disable).parameters
|
||||
# ProfileController.disable(
|
||||
# profile,
|
||||
# **{key: value for key, value in kwargs.items() if key in supported}
|
||||
# )
|
||||
|
||||
def run(self):
|
||||
if self.action == 'LIST_PROFILES':
|
||||
self.list_profiles()
|
||||
elif self.action == 'CREATE_SESSION_PROFILE':
|
||||
self.create_profile(self.profile_type)
|
||||
elif self.action == 'CREATE_SYSTEM_PROFILE':
|
||||
self.create_profile(self.profile_type)
|
||||
elif self.action == 'GET_SUBSCRIPTION':
|
||||
self.get_subscription()
|
||||
elif self.action == 'DISABLE_PROFILE':
|
||||
self.disable_profile()
|
||||
elif self.action == 'SYNC' or self.action == 'SYNC_TOR':
|
||||
self.sync()
|
||||
elif self.action == 'DESTROY_PROFILE':
|
||||
self.destroy_profile()
|
||||
elif self.action == 'DISABLE_ALL_PROFILES':
|
||||
self.disable_all_profiles()
|
||||
elif self.action == 'INSTALL_PACKAGE':
|
||||
self.install_package()
|
||||
elif self.action == 'SETUP_SINGBOX_BINARY':
|
||||
self.setup_singbox_binary()
|
||||
elif self.action == 'CHECK_FOR_UPDATE':
|
||||
self.check_for_update()
|
||||
elif self.action == 'DOWNLOAD_UPDATE':
|
||||
self.download_update()
|
||||
elif self.action == 'CHECK_INVOICE_STATUS':
|
||||
self.check_invoice_status()
|
||||
action_handlers = {
|
||||
'LIST_PROFILES': self.list_profiles,
|
||||
'CREATE_SESSION_PROFILE': lambda: self.create_profile(self.profile_type),
|
||||
'CREATE_SYSTEM_PROFILE': lambda: self.create_profile(self.profile_type),
|
||||
'GET_SUBSCRIPTION': self.get_subscription,
|
||||
'DISABLE_PROFILE': self.disable_profile,
|
||||
'SYNC': self.sync,
|
||||
'SYNC_TOR': self.sync,
|
||||
'DESTROY_PROFILE': self.destroy_profile,
|
||||
'DISABLE_ALL_PROFILES': self.disable_all_profiles,
|
||||
'INSTALL_PACKAGE': self.install_package,
|
||||
'SETUP_SINGBOX_BINARY': self.setup_singbox_binary,
|
||||
'CHECK_FOR_UPDATE': self.check_for_update,
|
||||
'DOWNLOAD_UPDATE': self.download_update,
|
||||
'CHECK_INVOICE_STATUS': self.check_invoice_status,
|
||||
}
|
||||
handler = action_handlers.get(self.action)
|
||||
if handler is not None:
|
||||
handler()
|
||||
|
||||
def check_invoice_status(self):
|
||||
try:
|
||||
|
|
@ -117,12 +92,7 @@ class WorkerThread(QThread):
|
|||
self.invoice_finished.emit(False)
|
||||
|
||||
def download_update(self):
|
||||
self.text_output.emit("Starting update process...")
|
||||
ClientController.update(client_observer=client_observer)
|
||||
client_observer.subscribe('update_progressing', lambda event: self.text_output.emit(
|
||||
f"Downloading: {event.meta.get('progress'):.1f}%"))
|
||||
client_observer.subscribe(
|
||||
'updated', lambda event: self.text_output.emit("Update process completed"))
|
||||
self._menu_operation({"task": "update"})
|
||||
|
||||
def check_for_update(self):
|
||||
self.text_output.emit("Checking for updates...")
|
||||
|
|
@ -184,60 +154,10 @@ class WorkerThread(QThread):
|
|||
self.finished.emit(False)
|
||||
|
||||
def disable_all_profiles(self):
|
||||
"""
|
||||
Purpose:
|
||||
Loop through all profiles in the class data,
|
||||
Classify them, and disable them.
|
||||
Why:
|
||||
Session profiles must be disabled first before System profiles.
|
||||
Called by:
|
||||
Same Class run()
|
||||
"""
|
||||
session_profiles, system_profiles = filter_profiles_by_type(self.profile_data)
|
||||
try:
|
||||
# SESSION
|
||||
for profile in session_profiles:
|
||||
disable_profile_via_controller(profile)
|
||||
print("finished with session profiles. now moving onto session profiles")
|
||||
|
||||
if session_profiles and system_profiles:
|
||||
time.sleep(1)
|
||||
|
||||
# SYSTEM
|
||||
for profile in system_profiles:
|
||||
disable_profile_via_controller(profile)
|
||||
self.text_output.emit("All profiles were successfully disabled")
|
||||
except SudoScript as e:
|
||||
self.text_output.emit(str(e))
|
||||
except FirewallError as e:
|
||||
self.text_output.emit(str(e))
|
||||
except MissingPreReqs as e:
|
||||
self.text_output.emit(str(e))
|
||||
except Exception as e:
|
||||
print(f"Error: {str(e)}")
|
||||
self.text_output.emit("An error occurred when disabling profile")
|
||||
finally:
|
||||
self.finished.emit(True)
|
||||
self._menu_operation({"task": "disable_all", "connected_ids": self.profile_data})
|
||||
|
||||
def destroy_profile(self):
|
||||
profile_id = int(self.profile_data['id'])
|
||||
profile = ProfileController.get(profile_id)
|
||||
|
||||
if profile is not None:
|
||||
try:
|
||||
ProfileController.destroy(profile, profile_observer, ticket_observer, connection_observer)
|
||||
except Exception as e:
|
||||
error_name = type(e).__name__
|
||||
error_text = str(e) or 'Unknown deletion error'
|
||||
self.text_output.emit(
|
||||
f'Could not delete profile {profile_id}: {error_name}: {error_text}')
|
||||
self.finished.emit(False)
|
||||
return
|
||||
self.text_output.emit(f'Profile {profile_id} deleted')
|
||||
self.finished.emit(True)
|
||||
else:
|
||||
self.text_output.emit(f'Profile {profile_id} does not exist')
|
||||
self.finished.emit(False)
|
||||
self._menu_operation({"task": "delete", "id": self.profile_data["id"], "confirmed": True})
|
||||
|
||||
def list_profiles(self):
|
||||
try:
|
||||
|
|
@ -312,55 +232,22 @@ class WorkerThread(QThread):
|
|||
f"{profile_type.capitalize()} Profile created with ID: {profile.id}")
|
||||
|
||||
def disable_profile(self):
|
||||
try:
|
||||
profile = ProfileController.get(int(self.profile_data['id']))
|
||||
if profile:
|
||||
disable_profile_via_controller(profile)
|
||||
else:
|
||||
self.text_output.emit(
|
||||
f"No profile found with ID: {self.profile_data['id']}")
|
||||
except SudoScript as e:
|
||||
self.text_output.emit(str(e))
|
||||
except FirewallError as e:
|
||||
self.text_output.emit(str(e))
|
||||
except MissingPreReqs as e:
|
||||
self.text_output.emit(str(e))
|
||||
except Exception as e:
|
||||
self.text_output.emit("An error occurred when disabling profile")
|
||||
finally:
|
||||
self.finished.emit(True)
|
||||
self._menu_operation({"task": "disable", "id": self.profile_data["id"]})
|
||||
|
||||
def _menu_operation(self, request):
|
||||
result = execute_menu_task(request, self._emit_menu_result, None)
|
||||
self._emit_menu_result(result)
|
||||
self.finished.emit(result["status"] == "success")
|
||||
|
||||
def _emit_menu_result(self, result: MenuResult):
|
||||
self.text_output.emit(result.get("message", ""))
|
||||
|
||||
def sync(self):
|
||||
try:
|
||||
if self.action == 'SYNC_TOR':
|
||||
ConfigurationController.set_connection('tor')
|
||||
else:
|
||||
ConfigurationController.set_connection('system')
|
||||
print(f"[SYNC] Syncing from URL: {Constants.SP_API_BASE_URL}/cachedsync")
|
||||
sync_result = new_sync(
|
||||
client_observer=client_observer,
|
||||
connection_observer=connection_observer)
|
||||
if not sync_result.valid:
|
||||
error_message = sync_result.message or "Connection error or invalid sync URL"
|
||||
self.sync_output.emit([], [], False, False, [], [])
|
||||
self.sync_error.emit(f"Sync failed: {error_message}")
|
||||
return
|
||||
|
||||
locations = LocationController.get_all()
|
||||
|
||||
|
||||
browser = ApplicationVersionController.get_all()
|
||||
# print('the browser is: ', browser)
|
||||
all_browser_versions = [
|
||||
f"{browser.application_code}:{browser.version_number}" for browser in browser if browser.supported]
|
||||
all_location_codes = [
|
||||
f"{location.country_code}_{location.code}" for location in locations]
|
||||
self.sync_output.emit(
|
||||
all_location_codes, all_browser_versions, True, False, locations, browser)
|
||||
except Exception as e:
|
||||
print(f'the error is: {e}')
|
||||
self.sync_output.emit([], [], False, False, [], [])
|
||||
self.sync_error.emit(f"Sync failed: {e}")
|
||||
connection = {"SYNC_TOR": "tor", "SYNC": "system"}[self.action]
|
||||
result = synchronize(connection)
|
||||
self.sync_output.emit(*result["sync_data"])
|
||||
if result["status"] == "error":
|
||||
self.sync_error.emit(result["message"])
|
||||
|
||||
def get_connection(self):
|
||||
connection = ConfigurationController.get_connection()
|
||||
|
|
|
|||
Loading…
Reference in a new issue