272 lines
13 KiB
Python
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)
|