Files
elmprodvpn/selective-vpn-gui/main_window/singbox/links_actions_mixin.py

272 lines
13 KiB
Python

from __future__ import annotations
import json
from PySide6.QtWidgets import QApplication, QInputDialog, QMenu
from main_window.constants import SINGBOX_EDITOR_PROTOCOL_OPTIONS
class SingBoxLinksActionsMixin:
def _apply_singbox_editor_values(self, values: dict[str, Any]) -> None:
incoming = dict(values or {})
target_protocol = str(incoming.get("protocol") or self._current_editor_protocol() or "vless").strip().lower() or "vless"
payload = self._seed_editor_values_for_protocol(
target_protocol,
profile_name=str(incoming.get("profile_name") or "").strip(),
)
payload.update(incoming)
self._singbox_editor_loading = True
try:
name = str(payload.get("profile_name") or "").strip()
self.ent_singbox_proto_name.setText(name)
self.chk_singbox_proto_enabled.setChecked(bool(payload.get("enabled", True)))
protocol = str(payload.get("protocol") or "").strip().lower()
if protocol:
pidx = self.cmb_singbox_proto_protocol.findData(protocol)
self.cmb_singbox_proto_protocol.setCurrentIndex(pidx if pidx >= 0 else self.cmb_singbox_proto_protocol.currentIndex())
self.ent_singbox_vless_server.setText(str(payload.get("server") or "").strip())
try:
self.spn_singbox_vless_port.setValue(int(payload.get("port") or 443))
except Exception:
self.spn_singbox_vless_port.setValue(443)
self.ent_singbox_vless_uuid.setText(str(payload.get("uuid") or "").strip())
self.ent_singbox_proto_password.setText(str(payload.get("password") or "").strip())
flow_v = str(payload.get("flow") or "").strip()
flow_idx = self.cmb_singbox_vless_flow.findData(flow_v)
if flow_idx >= 0:
self.cmb_singbox_vless_flow.setCurrentIndex(flow_idx)
else:
self.cmb_singbox_vless_flow.setEditText(flow_v)
packet_v = str(payload.get("packet_encoding") or "").strip().lower()
if packet_v in ("none", "off", "false"):
packet_v = ""
packet_idx = self.cmb_singbox_vless_packet_encoding.findData(packet_v)
self.cmb_singbox_vless_packet_encoding.setCurrentIndex(packet_idx if packet_idx >= 0 else 0)
transport_v = str(payload.get("transport") or "tcp").strip().lower()
transport_idx = self.cmb_singbox_vless_transport.findData(transport_v)
self.cmb_singbox_vless_transport.setCurrentIndex(transport_idx if transport_idx >= 0 else 0)
self.ent_singbox_vless_path.setText(str(payload.get("path") or "").strip())
self.ent_singbox_vless_grpc_service.setText(str(payload.get("grpc_service") or "").strip())
sec_v = str(payload.get("security") or "none").strip().lower()
sec_idx = self.cmb_singbox_vless_security.findData(sec_v)
self.cmb_singbox_vless_security.setCurrentIndex(sec_idx if sec_idx >= 0 else 0)
self.ent_singbox_vless_sni.setText(str(payload.get("sni") or "").strip())
fp_v = str(payload.get("utls_fp") or "").strip().lower()
fp_idx = self.cmb_singbox_vless_utls_fp.findData(fp_v)
self.cmb_singbox_vless_utls_fp.setCurrentIndex(fp_idx if fp_idx >= 0 else 0)
self.ent_singbox_vless_reality_pk.setText(str(payload.get("reality_public_key") or "").strip())
self.ent_singbox_vless_reality_sid.setText(str(payload.get("reality_short_id") or "").strip())
self.chk_singbox_vless_insecure.setChecked(bool(payload.get("tls_insecure", False)))
self.chk_singbox_vless_sniff.setChecked(bool(payload.get("sniff", True)))
ss_method = str(payload.get("ss_method") or "").strip().lower()
if ss_method:
idx = self.cmb_singbox_ss_method.findData(ss_method)
if idx >= 0:
self.cmb_singbox_ss_method.setCurrentIndex(idx)
else:
self.cmb_singbox_ss_method.setEditText(ss_method)
else:
self.cmb_singbox_ss_method.setCurrentIndex(0)
self.ent_singbox_ss_plugin.setText(str(payload.get("ss_plugin") or "").strip())
try:
self.spn_singbox_hy2_up_mbps.setValue(int(payload.get("hy2_up_mbps") or 0))
except Exception:
self.spn_singbox_hy2_up_mbps.setValue(0)
try:
self.spn_singbox_hy2_down_mbps.setValue(int(payload.get("hy2_down_mbps") or 0))
except Exception:
self.spn_singbox_hy2_down_mbps.setValue(0)
self.ent_singbox_hy2_obfs.setText(str(payload.get("hy2_obfs") or "").strip())
self.ent_singbox_hy2_obfs_password.setText(str(payload.get("hy2_obfs_password") or "").strip())
tuic_cc = str(payload.get("tuic_congestion") or "").strip()
idx = self.cmb_singbox_tuic_congestion.findData(tuic_cc)
self.cmb_singbox_tuic_congestion.setCurrentIndex(idx if idx >= 0 else 0)
tuic_udp = str(payload.get("tuic_udp_mode") or "").strip()
idx = self.cmb_singbox_tuic_udp_mode.findData(tuic_udp)
self.cmb_singbox_tuic_udp_mode.setCurrentIndex(idx if idx >= 0 else 0)
self.chk_singbox_tuic_zero_rtt.setChecked(bool(payload.get("tuic_zero_rtt", False)))
self.ent_singbox_wg_private_key.setText(str(payload.get("wg_private_key") or "").strip())
self.ent_singbox_wg_peer_public_key.setText(str(payload.get("wg_peer_public_key") or "").strip())
self.ent_singbox_wg_psk.setText(str(payload.get("wg_psk") or "").strip())
wg_local = payload.get("wg_local_address") or []
if isinstance(wg_local, list):
self.ent_singbox_wg_local_address.setText(
",".join([str(x).strip() for x in wg_local if str(x).strip()])
)
else:
self.ent_singbox_wg_local_address.setText(str(wg_local or "").strip())
wg_reserved = payload.get("wg_reserved") or []
if isinstance(wg_reserved, list):
self.ent_singbox_wg_reserved.setText(
",".join([str(x).strip() for x in wg_reserved if str(x).strip()])
)
else:
self.ent_singbox_wg_reserved.setText(str(wg_reserved or "").strip())
try:
self.spn_singbox_wg_mtu.setValue(int(payload.get("wg_mtu") or 0))
except Exception:
self.spn_singbox_wg_mtu.setValue(0)
finally:
self._singbox_editor_loading = False
self.on_singbox_vless_editor_changed()
def _create_singbox_connection(
self,
*,
profile_name: str,
protocol: str = "vless",
raw_config: dict[str, Any] | None = None,
editor_values: dict[str, Any] | None = None,
auto_save: bool = False,
) -> str:
name = str(profile_name or "").strip() or "SingBox connection"
client_id = self._next_free_transport_client_id(name)
proto = self._normalized_seed_protocol(protocol)
config = self._default_new_singbox_client_config(client_id, protocol=proto)
created = self.ctrl.transport_client_create_action(
client_id=client_id,
kind="singbox",
name=name,
enabled=True,
config=config,
)
line = (created.pretty_text or "").strip() or f"create {client_id}"
self._append_transport_log(f"[engine] {line}")
self.ctrl.log_gui(f"[transport-engine] {line}")
if not created.ok:
raise RuntimeError(line)
self.refresh_transport_engines(silent=True)
if not self._select_transport_engine_by_id(client_id):
raise RuntimeError(f"created client '{client_id}' was not found after refresh")
self._sync_selected_singbox_profile_link(silent=False)
client, _eid, pid = self._selected_singbox_profile_context()
seed_raw = raw_config if isinstance(raw_config, dict) else self._seed_raw_config_for_protocol(proto)
saved_seed = self.ctrl.singbox_profile_save_raw_for_client(
client,
profile_id=pid,
name=name,
enabled=True,
protocol=proto,
raw_config=seed_raw,
)
seed_line = (saved_seed.pretty_text or "").strip() or f"save profile {pid}"
self._append_transport_log(f"[profile] {seed_line}")
self.ctrl.log_gui(f"[singbox-profile] {seed_line}")
self._load_singbox_editor_for_selected(silent=True)
if editor_values:
payload = dict(editor_values)
seeded = self._seed_editor_values_for_protocol(proto, profile_name=name)
seeded.update(payload)
payload = seeded
if not str(payload.get("profile_name") or "").strip():
payload["profile_name"] = name
self._apply_singbox_editor_values(payload)
if auto_save:
saved = self._save_singbox_editor_draft(client, profile_id=pid)
save_line = (saved.pretty_text or "").strip() or f"save profile {pid}"
self._append_transport_log(f"[profile] {save_line}")
self.ctrl.log_gui(f"[singbox-profile] {save_line}")
return client_id
def on_singbox_create_connection_click(self) -> None:
menu = QMenu(self)
act_clip = menu.addAction("Create from clipboard")
act_link = menu.addAction("Create from link...")
act_manual = menu.addAction("Create manual")
pos = self.btn_singbox_profile_create.mapToGlobal(
self.btn_singbox_profile_create.rect().bottomLeft()
)
chosen = menu.exec(pos)
if chosen is None:
return
if chosen == act_clip:
self._safe(self.on_singbox_create_connection_from_clipboard, title="Create connection error")
return
if chosen == act_link:
self._safe(self.on_singbox_create_connection_from_link, title="Create connection error")
return
if chosen == act_manual:
self._safe(self.on_singbox_create_connection_manual, title="Create connection error")
def on_singbox_create_connection_from_clipboard(self) -> None:
raw = str(QApplication.clipboard().text() or "").strip()
if not raw:
raise RuntimeError("Clipboard is empty")
payload = self._parse_connection_link_payload(raw)
profile_name = str(payload.get("profile_name") or "").strip() or "SingBox Clipboard"
cid = self._create_singbox_connection(
profile_name=profile_name,
protocol=str(payload.get("protocol") or "vless"),
raw_config=payload.get("raw_config") if isinstance(payload.get("raw_config"), dict) else None,
editor_values=payload.get("editor_values") if isinstance(payload.get("editor_values"), dict) else None,
auto_save=True,
)
self.on_singbox_profile_edit_dialog(cid)
def on_singbox_create_connection_from_link(self) -> None:
raw, ok = QInputDialog.getText(
self,
"Create connection from link",
"Paste connection link (vless:// trojan:// ss:// hysteria2:// hy2:// tuic:// wireguard://):",
)
if not ok:
return
payload = self._parse_connection_link_payload(raw)
profile_name = str(payload.get("profile_name") or "").strip() or "SingBox Link"
cid = self._create_singbox_connection(
profile_name=profile_name,
protocol=str(payload.get("protocol") or "vless"),
raw_config=payload.get("raw_config") if isinstance(payload.get("raw_config"), dict) else None,
editor_values=payload.get("editor_values") if isinstance(payload.get("editor_values"), dict) else None,
auto_save=True,
)
self.on_singbox_profile_edit_dialog(cid)
def on_singbox_create_connection_manual(self) -> None:
name, ok = QInputDialog.getText(
self,
"Create manual connection",
"Connection name:",
)
if not ok:
return
profile_name = str(name or "").strip() or "SingBox Manual"
proto_title, ok = QInputDialog.getItem(
self,
"Create manual connection",
"Protocol:",
[label for label, _pid in SINGBOX_EDITOR_PROTOCOL_OPTIONS],
0,
False,
)
if not ok:
return
proto_map = {label.lower(): pid for label, pid in SINGBOX_EDITOR_PROTOCOL_OPTIONS}
proto = self._normalized_seed_protocol(proto_map.get(str(proto_title or "").strip().lower(), "vless"))
cid = self._create_singbox_connection(
profile_name=profile_name,
protocol=proto,
editor_values=self._seed_editor_values_for_protocol(proto, profile_name=profile_name),
auto_save=False,
)
self.on_singbox_profile_edit_dialog(cid)