feat: проверка обновлений GitHub, пакет utils, зависимости по ОС
Проверка последнего релиза при запуске (опционально), диалог и ссылка на страницу релиза; секция в настройках CTk и меню macOS; единый default_tray_config; requirements/*.txt; README. Refs: https://github.com/Flowseal/tg-ws-proxy/issues/357
This commit is contained in:
parent
35ea33ee3f
commit
f0044255fa
21
README.md
21
README.md
|
|
@ -40,10 +40,12 @@ Telegram Desktop → SOCKS5 (127.0.0.1:1080) → TG WS Proxy → WSS → Telegra
|
|||
|
||||
- **Открыть в Telegram** — автоматически настроить прокси через `tg://socks` ссылку
|
||||
- **Перезапустить прокси** — перезапуск без выхода из приложения
|
||||
- **Настройки...** — GUI-редактор конфигурации
|
||||
- **Настройки...** — GUI-редактор конфигурации (в т.ч. версия приложения, опциональная проверка обновлений с GitHub)
|
||||
- **Открыть логи** — открыть файл логов
|
||||
- **Выход** — остановить прокси и закрыть приложение
|
||||
|
||||
При первом запуске после старта может появиться запрос об открытии страницы релиза, если на GitHub вышла новая версия (отключается в настройках).
|
||||
|
||||
### macOS
|
||||
|
||||
Перейдите на [страницу релизов](https://github.com/Flowseal/tg-ws-proxy/releases) и скачайте **`TgWsProxy_macos_universal.dmg`** — универсальная сборка для Apple Silicon и Intel.
|
||||
|
|
@ -82,6 +84,11 @@ chmod +x TgWsProxy_linux_amd64
|
|||
|
||||
## Установка из исходников
|
||||
|
||||
Список пакетов `pip` с версиями **по ОС** (Windows/Linux/macOS, отдельно для Python 3.8 на Windows) — в каталоге [`requirements/`](requirements/README.md). Кратко:
|
||||
|
||||
- **Linux (трей):** системный пакет Tcl/Tk для `tkinter`, например `python3-tk` (Debian/Ubuntu).
|
||||
- Дальше: `pip install -r requirements/<файл>.txt` и `pip install -e .` из корня репозитория.
|
||||
|
||||
### Консольный proxy
|
||||
|
||||
Для запуска только SOCKS5/WebSocket proxy без tray-интерфейса достаточно базовой установки:
|
||||
|
|
@ -94,6 +101,7 @@ tg-ws-proxy
|
|||
### Windows 7/10+
|
||||
|
||||
```bash
|
||||
pip install -r requirements/windows-py39plus.txt
|
||||
pip install -e .
|
||||
tg-ws-proxy-tray-win
|
||||
```
|
||||
|
|
@ -101,6 +109,7 @@ tg-ws-proxy-tray-win
|
|||
### macOS
|
||||
|
||||
```bash
|
||||
pip install -r requirements/macos.txt
|
||||
pip install -e .
|
||||
tg-ws-proxy-tray-macos
|
||||
```
|
||||
|
|
@ -108,6 +117,7 @@ tg-ws-proxy-tray-macos
|
|||
### Linux
|
||||
|
||||
```bash
|
||||
pip install -r requirements/linux.txt
|
||||
pip install -e .
|
||||
tg-ws-proxy-tray-linux
|
||||
```
|
||||
|
|
@ -179,15 +189,22 @@ Tray-приложение хранит данные в:
|
|||
|
||||
```json
|
||||
{
|
||||
"host": "127.0.0.1",
|
||||
"port": 1080,
|
||||
"dc_ip": [
|
||||
"2:149.154.167.220",
|
||||
"4:149.154.167.220"
|
||||
],
|
||||
"verbose": false
|
||||
"verbose": false,
|
||||
"buf_kb": 256,
|
||||
"pool_size": 4,
|
||||
"log_max_mb": 5.0,
|
||||
"check_updates": true
|
||||
}
|
||||
```
|
||||
|
||||
Ключ **`check_updates`** — при `true` при запросе к GitHub сравнивается версия с последним релизом (только уведомление и ссылка на страницу загрузки). На Windows в конфиге может быть **`autostart`** (автозапуск при входе в систему).
|
||||
|
||||
## Автоматическая сборка
|
||||
|
||||
Проект содержит спецификации PyInstaller ([`packaging/windows.spec`](packaging/windows.spec), [`packaging/macos.spec`](packaging/macos.spec), [`packaging/linux.spec`](packaging/linux.spec)) и GitHub Actions workflow ([`.github/workflows/build.yml`](.github/workflows/build.yml)) для автоматической сборки.
|
||||
|
|
|
|||
56
linux.py
56
linux.py
|
|
@ -8,6 +8,7 @@ import os
|
|||
import subprocess
|
||||
import sys
|
||||
import threading
|
||||
import webbrowser
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import Dict, Optional
|
||||
|
|
@ -20,6 +21,7 @@ from PIL import Image, ImageDraw, ImageFont
|
|||
|
||||
import proxy.tg_ws_proxy as tg_ws_proxy
|
||||
from proxy import __version__
|
||||
from utils.default_config import default_tray_config
|
||||
from ui.ctk_tray_ui import (
|
||||
install_tray_config_buttons,
|
||||
install_tray_config_form,
|
||||
|
|
@ -44,15 +46,7 @@ FIRST_RUN_MARKER = APP_DIR / ".first_run_done"
|
|||
IPV6_WARN_MARKER = APP_DIR / ".ipv6_warned"
|
||||
|
||||
|
||||
DEFAULT_CONFIG = {
|
||||
"port": 1080,
|
||||
"host": "127.0.0.1",
|
||||
"dc_ip": ["2:149.154.167.220", "4:149.154.167.220"],
|
||||
"verbose": False,
|
||||
"log_max_mb": 5,
|
||||
"buf_kb": 256,
|
||||
"pool_size": 4,
|
||||
}
|
||||
DEFAULT_CONFIG = default_tray_config()
|
||||
|
||||
|
||||
_proxy_thread: Optional[threading.Thread] = None
|
||||
|
|
@ -350,6 +344,48 @@ def _show_info(text: str, title: str = "TG WS Proxy"):
|
|||
root.destroy()
|
||||
|
||||
|
||||
def _ask_yes_no_dialog(text: str, title: str = "TG WS Proxy") -> bool:
|
||||
import tkinter as _tk
|
||||
from tkinter import messagebox as _mb
|
||||
|
||||
root = _tk.Tk()
|
||||
root.withdraw()
|
||||
try:
|
||||
root.attributes("-topmost", True)
|
||||
except Exception:
|
||||
pass
|
||||
r = _mb.askyesno(title, text, parent=root)
|
||||
root.destroy()
|
||||
return bool(r)
|
||||
|
||||
|
||||
def _maybe_notify_update_async():
|
||||
def _work():
|
||||
time.sleep(1.5)
|
||||
if _exiting:
|
||||
return
|
||||
if not _config.get("check_updates", True):
|
||||
return
|
||||
try:
|
||||
from utils.update_check import RELEASES_PAGE_URL, get_status, run_check
|
||||
run_check(__version__)
|
||||
st = get_status()
|
||||
if not st.get("has_update"):
|
||||
return
|
||||
url = (st.get("html_url") or "").strip() or RELEASES_PAGE_URL
|
||||
ver = st.get("latest") or "?"
|
||||
text = (
|
||||
f"Доступна новая версия: {ver}\n\n"
|
||||
f"Открыть страницу релиза в браузере?"
|
||||
)
|
||||
if _ask_yes_no_dialog(text, "TG WS Proxy — обновление"):
|
||||
webbrowser.open(url)
|
||||
except Exception as exc:
|
||||
log.debug("Update check failed: %s", exc)
|
||||
|
||||
threading.Thread(target=_work, daemon=True, name="update-check").start()
|
||||
|
||||
|
||||
def _on_open_in_telegram(icon=None, item=None):
|
||||
host = _config.get("host", DEFAULT_CONFIG["host"])
|
||||
port = _config.get("port", DEFAULT_CONFIG["port"])
|
||||
|
|
@ -624,6 +660,8 @@ def run_tray():
|
|||
|
||||
start_proxy()
|
||||
|
||||
_maybe_notify_update_async()
|
||||
|
||||
_show_first_run()
|
||||
_check_ipv6_warning()
|
||||
|
||||
|
|
|
|||
72
macos.py
72
macos.py
|
|
@ -31,6 +31,7 @@ except ImportError:
|
|||
|
||||
import proxy.tg_ws_proxy as tg_ws_proxy
|
||||
from proxy import __version__
|
||||
from utils.default_config import default_tray_config
|
||||
|
||||
APP_NAME = "TgWsProxy"
|
||||
APP_DIR = Path.home() / "Library" / "Application Support" / APP_NAME
|
||||
|
|
@ -40,15 +41,7 @@ FIRST_RUN_MARKER = APP_DIR / ".first_run_done"
|
|||
IPV6_WARN_MARKER = APP_DIR / ".ipv6_warned"
|
||||
MENUBAR_ICON_PATH = APP_DIR / "menubar_icon.png"
|
||||
|
||||
DEFAULT_CONFIG = {
|
||||
"port": 1080,
|
||||
"host": "127.0.0.1",
|
||||
"dc_ip": ["2:149.154.167.220", "4:149.154.167.220"],
|
||||
"verbose": False,
|
||||
"log_max_mb": 5,
|
||||
"buf_kb": 256,
|
||||
"pool_size": 4,
|
||||
}
|
||||
DEFAULT_CONFIG = default_tray_config()
|
||||
|
||||
_proxy_thread: Optional[threading.Thread] = None
|
||||
_async_stop: Optional[object] = None
|
||||
|
|
@ -427,6 +420,55 @@ def _on_edit_config(_=None):
|
|||
threading.Thread(target=_edit_config_dialog, daemon=True).start()
|
||||
|
||||
|
||||
def _check_updates_menu_title() -> str:
|
||||
on = bool(_config.get("check_updates", True))
|
||||
return (
|
||||
"✓ Проверять обновления при запуске"
|
||||
if on
|
||||
else "Проверять обновления при запуске (выкл)"
|
||||
)
|
||||
|
||||
|
||||
def _toggle_check_updates(_=None):
|
||||
global _config
|
||||
_config["check_updates"] = not bool(_config.get("check_updates", True))
|
||||
save_config(_config)
|
||||
if _app is not None:
|
||||
_app._check_updates_item.title = _check_updates_menu_title()
|
||||
|
||||
|
||||
def _on_open_release_page(_=None):
|
||||
from utils.update_check import RELEASES_PAGE_URL
|
||||
webbrowser.open(RELEASES_PAGE_URL)
|
||||
|
||||
|
||||
def _maybe_notify_update_async():
|
||||
def _work():
|
||||
time.sleep(1.5)
|
||||
if _exiting:
|
||||
return
|
||||
if not _config.get("check_updates", True):
|
||||
return
|
||||
try:
|
||||
from utils.update_check import RELEASES_PAGE_URL, get_status, run_check
|
||||
run_check(__version__)
|
||||
st = get_status()
|
||||
if not st.get("has_update"):
|
||||
return
|
||||
url = (st.get("html_url") or "").strip() or RELEASES_PAGE_URL
|
||||
ver = st.get("latest") or "?"
|
||||
if _ask_yes_no(
|
||||
f"Доступна новая версия: {ver}\n\n"
|
||||
f"Открыть страницу релиза в браузере?",
|
||||
"TG WS Proxy — обновление",
|
||||
):
|
||||
webbrowser.open(url)
|
||||
except Exception as exc:
|
||||
log.debug("Update check failed: %s", exc)
|
||||
|
||||
threading.Thread(target=_work, daemon=True, name="update-check").start()
|
||||
|
||||
|
||||
# Settings via native macOS dialogs
|
||||
def _edit_config_dialog():
|
||||
cfg = load_config()
|
||||
|
|
@ -617,6 +659,12 @@ class TgWsProxyApp(_TgWsProxyAppBase):
|
|||
self._logs_item = rumps.MenuItem(
|
||||
"Открыть логи",
|
||||
callback=_on_open_logs)
|
||||
self._release_page_item = rumps.MenuItem(
|
||||
"Страница релиза на GitHub…",
|
||||
callback=_on_open_release_page)
|
||||
self._check_updates_item = rumps.MenuItem(
|
||||
_check_updates_menu_title(),
|
||||
callback=_toggle_check_updates)
|
||||
self._version_item = rumps.MenuItem(
|
||||
f"Версия {__version__}",
|
||||
callback=lambda _: None)
|
||||
|
|
@ -633,6 +681,9 @@ class TgWsProxyApp(_TgWsProxyAppBase):
|
|||
self._settings_item,
|
||||
self._logs_item,
|
||||
None,
|
||||
self._release_page_item,
|
||||
self._check_updates_item,
|
||||
None,
|
||||
self._version_item,
|
||||
])
|
||||
|
||||
|
|
@ -672,6 +723,9 @@ def run_menubar():
|
|||
return
|
||||
|
||||
start_proxy()
|
||||
|
||||
_maybe_notify_update_async()
|
||||
|
||||
_show_first_run()
|
||||
_check_ipv6_warning()
|
||||
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ Source = "https://github.com/Flowseal/tg-ws-proxy"
|
|||
Issues = "https://github.com/Flowseal/tg-ws-proxy/issues"
|
||||
|
||||
[tool.hatch.build.targets.wheel]
|
||||
packages = ["proxy", "ui"]
|
||||
packages = ["proxy", "ui", "utils"]
|
||||
|
||||
[tool.hatch.build.force-include]
|
||||
"windows.py" = "windows.py"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,66 @@
|
|||
# Зависимости по ОС (разработка из исходников)
|
||||
|
||||
Версии совпадают с [`pyproject.toml`](../pyproject.toml). Для установки **пакета проекта** после зависимостей выполните из корня репозитория:
|
||||
|
||||
```bash
|
||||
pip install -e .
|
||||
```
|
||||
|
||||
Скрипты точек входа: `tg-ws-proxy`, `tg-ws-proxy-tray-win` / `tg-ws-proxy-tray-linux` / `tg-ws-proxy-tray-macos`.
|
||||
|
||||
---
|
||||
|
||||
## Windows
|
||||
|
||||
| Файл | Условие |
|
||||
|------|---------|
|
||||
| [`windows-py39plus.txt`](windows-py39plus.txt) | Python **3.9+** (рекомендуется) |
|
||||
| [`windows-py38.txt`](windows-py38.txt) | Python **3.8** |
|
||||
|
||||
**Системно:** для сборки/запуска из исходников — установленный [Python](https://www.python.org/downloads/) с опцией *tcl/tk* (обычно включена). Отдельный Tcl/Tk не требуется.
|
||||
|
||||
```powershell
|
||||
pip install -r requirements\windows-py39plus.txt
|
||||
pip install -e .
|
||||
tg-ws-proxy-tray-win
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Linux
|
||||
|
||||
Файл: [`linux.txt`](linux.txt) (Python **3.9+**).
|
||||
|
||||
**Системно:** модуль `tkinter` (нужен CustomTkinter для трея):
|
||||
|
||||
- Debian/Ubuntu: `sudo apt install python3-tk`
|
||||
- Fedora: `sudo dnf install python3-tkinter`
|
||||
- Arch: `sudo pacman -S tk`
|
||||
|
||||
Также нужны заголовки Python при сборке расширений (если pip собирает колёса): `python3-dev` / `gcc` — по сообщениям pip.
|
||||
|
||||
```bash
|
||||
pip install -r requirements/linux.txt
|
||||
pip install -e .
|
||||
tg-ws-proxy-tray-linux
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## macOS
|
||||
|
||||
Файл: [`macos.txt`](macos.txt).
|
||||
|
||||
**Системно:** Python с официального установщика или Homebrew; Tcl/Tk обычно уже в комплекте. Для графики трея используется **rumps** (нативная строка меню), не CustomTkinter.
|
||||
|
||||
```bash
|
||||
pip install -r requirements/macos.txt
|
||||
pip install -e .
|
||||
tg-ws-proxy-tray-macos
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Примечание
|
||||
|
||||
Готовые бинарники для пользователей — на [странице релизов](https://github.com/Flowseal/tg-ws-proxy/releases); отдельная установка Python и `pip` для них не требуется.
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
# Linux (не macOS), Python 3.9+ (как в pyproject.toml)
|
||||
# Системно: пакет Tcl/Tk для tkinter (CustomTkinter), см. README в этой папке.
|
||||
# Установка: pip install -r requirements/linux.txt
|
||||
# Затем из корня репозитория: pip install -e .
|
||||
|
||||
pyperclip==1.9.0
|
||||
psutil==7.0.0
|
||||
cryptography==46.0.5
|
||||
Pillow==12.1.1
|
||||
customtkinter==5.2.2
|
||||
pystray==0.19.5
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
# macOS, Python 3.9+ (без customtkinter/pystray — нативное меню rumps)
|
||||
# Установка: pip install -r requirements/macos.txt
|
||||
# Затем из корня репозитория: pip install -e .
|
||||
|
||||
pyperclip==1.9.0
|
||||
psutil==7.0.0
|
||||
cryptography==46.0.5
|
||||
Pillow==12.1.0
|
||||
rumps==0.4.0
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
# Windows, Python 3.8 (устаревшие пины из pyproject.toml)
|
||||
# Установка: pip install -r requirements/windows-py38.txt
|
||||
|
||||
pyperclip==1.9.0
|
||||
psutil==5.9.8
|
||||
cryptography==41.0.7
|
||||
Pillow==10.4.0
|
||||
customtkinter==5.2.2
|
||||
pystray==0.19.5
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
# Windows, Python 3.9+ (как в pyproject.toml для platform_system == Windows, python_version >= 3.9)
|
||||
# Установка: pip install -r requirements/windows-py39plus.txt
|
||||
# Затем из корня репозитория: pip install -e .
|
||||
|
||||
pyperclip==1.9.0
|
||||
psutil==7.0.0
|
||||
cryptography==46.0.5
|
||||
Pillow==12.1.1
|
||||
customtkinter==5.2.2
|
||||
pystray==0.19.5
|
||||
|
|
@ -5,11 +5,13 @@
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
import webbrowser
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Callable, Dict, List, Optional, Tuple, Union
|
||||
|
||||
import proxy.tg_ws_proxy as tg_ws_proxy
|
||||
from proxy import __version__
|
||||
from utils.update_check import RELEASES_PAGE_URL, get_status
|
||||
|
||||
from ui.ctk_theme import (
|
||||
FIRST_RUN_FRAME_PAD,
|
||||
|
|
@ -52,6 +54,10 @@ _TIP_AUTOSTART = (
|
|||
"Запускать TG WS Proxy при входе в Windows. "
|
||||
"Если вы переместите программу в другую папку, запись автозапуска может сброситься."
|
||||
)
|
||||
_TIP_CHECK_UPDATES = (
|
||||
"При запуске запрашивать с GitHub информацию о последнем релизе. "
|
||||
"При появлении новой версии можно открыть страницу загрузки."
|
||||
)
|
||||
_TIP_SAVE = "Сохранить настройки в файл. После сохранения можно перезапустить прокси."
|
||||
_TIP_CANCEL = "Закрыть окно без сохранения изменений."
|
||||
|
||||
|
|
@ -121,6 +127,7 @@ class TrayConfigFormWidgets:
|
|||
adv_entries: List[Any]
|
||||
adv_keys: Tuple[str, ...]
|
||||
autostart_var: Optional[Any]
|
||||
check_updates_var: Optional[Any]
|
||||
|
||||
|
||||
def install_tray_config_form(
|
||||
|
|
@ -291,6 +298,71 @@ def install_tray_config_form(
|
|||
adv_entries = list(adv_frame.winfo_children())
|
||||
adv_keys = ("buf_kb", "pool_size", "log_max_mb")
|
||||
|
||||
upd_inner = _config_section(ctk, frame, theme, "Обновления")
|
||||
st = get_status()
|
||||
check_updates_var = ctk.BooleanVar(
|
||||
value=bool(
|
||||
cfg.get("check_updates", default_config.get("check_updates", True))
|
||||
)
|
||||
)
|
||||
upd_cb = ctk.CTkCheckBox(
|
||||
upd_inner,
|
||||
text="Проверять обновления при запуске",
|
||||
variable=check_updates_var,
|
||||
font=(theme.ui_font_family, 13),
|
||||
text_color=theme.text_primary,
|
||||
fg_color=theme.tg_blue,
|
||||
hover_color=theme.tg_blue_hover,
|
||||
corner_radius=6,
|
||||
border_width=2,
|
||||
border_color=theme.field_border,
|
||||
)
|
||||
upd_cb.pack(anchor="w", pady=(0, 6))
|
||||
attach_ctk_tooltip(upd_cb, _TIP_CHECK_UPDATES)
|
||||
|
||||
if st.get("error"):
|
||||
upd_status = "Не удалось связаться с GitHub. Проверьте сеть."
|
||||
elif not st.get("checked"):
|
||||
upd_status = "Статус появится после фоновой проверки при запуске."
|
||||
elif st.get("has_update") and st.get("latest"):
|
||||
upd_status = (
|
||||
f"На GitHub доступна версия {st['latest']} "
|
||||
f"(у вас {__version__})."
|
||||
)
|
||||
elif st.get("ahead_of_release") and st.get("latest"):
|
||||
upd_status = (
|
||||
f"У вас {__version__} — новее последнего релиза на GitHub "
|
||||
f"({st['latest']})."
|
||||
)
|
||||
else:
|
||||
upd_status = "Установлена последняя известная версия с GitHub."
|
||||
|
||||
ctk.CTkLabel(
|
||||
upd_inner,
|
||||
text=upd_status,
|
||||
font=(theme.ui_font_family, 11),
|
||||
text_color=theme.text_secondary,
|
||||
anchor="w",
|
||||
justify="left",
|
||||
wraplength=inner_w,
|
||||
).pack(anchor="w", pady=(0, 8))
|
||||
|
||||
rel_url = (st.get("html_url") or "").strip() or RELEASES_PAGE_URL
|
||||
open_rel_btn = ctk.CTkButton(
|
||||
upd_inner,
|
||||
text="Открыть страницу релиза",
|
||||
height=32,
|
||||
font=(theme.ui_font_family, 13),
|
||||
corner_radius=8,
|
||||
fg_color=theme.field_bg,
|
||||
hover_color=theme.field_border,
|
||||
text_color=theme.text_primary,
|
||||
border_width=1,
|
||||
border_color=theme.field_border,
|
||||
command=lambda u=rel_url: webbrowser.open(u),
|
||||
)
|
||||
open_rel_btn.pack(anchor="w")
|
||||
|
||||
autostart_var = None
|
||||
if show_autostart:
|
||||
sys_inner = _config_section(
|
||||
|
|
@ -330,6 +402,7 @@ def install_tray_config_form(
|
|||
adv_entries=adv_entries,
|
||||
adv_keys=adv_keys,
|
||||
autostart_var=autostart_var,
|
||||
check_updates_var=check_updates_var,
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -399,6 +472,8 @@ def validate_config_form(
|
|||
)
|
||||
|
||||
merge_adv_from_form(widgets, new_cfg, default_config)
|
||||
if widgets.check_updates_var is not None:
|
||||
new_cfg["check_updates"] = bool(widgets.check_updates_var.get())
|
||||
return new_cfg
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
"""Вспомогательные утилиты (проверка релизов и т.п.)."""
|
||||
|
||||
from utils.update_check import RELEASES_PAGE_URL, get_status, run_check
|
||||
|
||||
__all__ = ["RELEASES_PAGE_URL", "get_status", "run_check"]
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
"""
|
||||
Общие значения по умолчанию для tray-приложений (Windows / Linux / macOS).
|
||||
Единственное отличие по платформе — ключ autostart только на Windows.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from typing import Any, Dict
|
||||
|
||||
_TRAY_DEFAULTS_COMMON: Dict[str, Any] = {
|
||||
"port": 1080,
|
||||
"host": "127.0.0.1",
|
||||
"dc_ip": ["2:149.154.167.220", "4:149.154.167.220"],
|
||||
"verbose": False,
|
||||
"check_updates": True,
|
||||
"log_max_mb": 5,
|
||||
"buf_kb": 256,
|
||||
"pool_size": 4,
|
||||
}
|
||||
|
||||
|
||||
def default_tray_config() -> Dict[str, Any]:
|
||||
"""Новая копия конфига по умолчанию для текущей ОС."""
|
||||
cfg = dict(_TRAY_DEFAULTS_COMMON)
|
||||
if sys.platform == "win32":
|
||||
cfg["autostart"] = False
|
||||
return cfg
|
||||
|
|
@ -0,0 +1,102 @@
|
|||
"""
|
||||
Минимальная проверка новой версии через GitHub Releases API (без сторонних зависимостей).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from itertools import zip_longest
|
||||
from typing import Any, Dict, Optional
|
||||
from urllib.error import HTTPError, URLError
|
||||
from urllib.request import Request, urlopen
|
||||
|
||||
REPO = "Flowseal/tg-ws-proxy"
|
||||
RELEASES_LATEST_API = f"https://api.github.com/repos/{REPO}/releases/latest"
|
||||
RELEASES_PAGE_URL = f"https://github.com/{REPO}/releases/latest"
|
||||
|
||||
_state: Dict[str, Any] = {
|
||||
"checked": False,
|
||||
"has_update": False,
|
||||
"ahead_of_release": False,
|
||||
"latest": None,
|
||||
"html_url": None,
|
||||
"error": None,
|
||||
}
|
||||
|
||||
|
||||
def _parse_version_tuple(s: str) -> tuple:
|
||||
s = (s or "").strip().lstrip("vV")
|
||||
if not s:
|
||||
return (0,)
|
||||
parts = []
|
||||
for seg in s.split("."):
|
||||
digits = "".join(c for c in seg if c.isdigit())
|
||||
if digits:
|
||||
try:
|
||||
parts.append(int(digits))
|
||||
except ValueError:
|
||||
parts.append(0)
|
||||
else:
|
||||
parts.append(0)
|
||||
return tuple(parts) if parts else (0,)
|
||||
|
||||
|
||||
def _version_gt(a: str, b: str) -> bool:
|
||||
"""True, если версия a новее b (простое сравнение по сегментам)."""
|
||||
ta = _parse_version_tuple(a)
|
||||
tb = _parse_version_tuple(b)
|
||||
for x, y in zip_longest(ta, tb, fillvalue=0):
|
||||
if x > y:
|
||||
return True
|
||||
if x < y:
|
||||
return False
|
||||
return False
|
||||
|
||||
|
||||
def fetch_latest_release(timeout: float = 12.0) -> Optional[dict]:
|
||||
req = Request(
|
||||
RELEASES_LATEST_API,
|
||||
headers={
|
||||
"Accept": "application/vnd.github+json",
|
||||
"User-Agent": "tg-ws-proxy-update-check",
|
||||
},
|
||||
method="GET",
|
||||
)
|
||||
with urlopen(req, timeout=timeout) as resp:
|
||||
raw = resp.read().decode("utf-8", errors="replace")
|
||||
return json.loads(raw)
|
||||
|
||||
|
||||
def run_check(current_version: str) -> None:
|
||||
"""Запрашивает последний релиз и обновляет внутреннее состояние."""
|
||||
global _state
|
||||
_state["checked"] = True
|
||||
_state["error"] = None
|
||||
try:
|
||||
data = fetch_latest_release()
|
||||
tag = (data.get("tag_name") or "").strip()
|
||||
html_url = (data.get("html_url") or "").strip() or RELEASES_PAGE_URL
|
||||
if not tag:
|
||||
_state["has_update"] = False
|
||||
_state["ahead_of_release"] = False
|
||||
_state["latest"] = None
|
||||
_state["html_url"] = html_url
|
||||
return
|
||||
latest_clean = tag.lstrip("vV")
|
||||
cur = (current_version or "").strip().lstrip("vV")
|
||||
_state["latest"] = latest_clean
|
||||
_state["html_url"] = html_url
|
||||
_state["has_update"] = _version_gt(latest_clean, cur)
|
||||
_state["ahead_of_release"] = bool(latest_clean) and _version_gt(
|
||||
cur, latest_clean
|
||||
)
|
||||
except (HTTPError, URLError, OSError, TimeoutError, ValueError, json.JSONDecodeError) as e:
|
||||
_state["error"] = str(e)
|
||||
_state["has_update"] = False
|
||||
_state["ahead_of_release"] = False
|
||||
_state["latest"] = None
|
||||
_state["html_url"] = RELEASES_PAGE_URL
|
||||
|
||||
|
||||
def get_status() -> Dict[str, Any]:
|
||||
"""Снимок состояния после run_check (для подписей в настройках)."""
|
||||
return dict(_state)
|
||||
58
windows.py
58
windows.py
|
|
@ -38,6 +38,7 @@ except ImportError:
|
|||
|
||||
import proxy.tg_ws_proxy as tg_ws_proxy
|
||||
from proxy import __version__
|
||||
from utils.default_config import default_tray_config
|
||||
from ui.ctk_tray_ui import (
|
||||
install_tray_config_buttons,
|
||||
install_tray_config_form,
|
||||
|
|
@ -65,16 +66,7 @@ FIRST_RUN_MARKER = APP_DIR / ".first_run_done"
|
|||
IPV6_WARN_MARKER = APP_DIR / ".ipv6_warned"
|
||||
|
||||
|
||||
DEFAULT_CONFIG = {
|
||||
"port": 1080,
|
||||
"host": "127.0.0.1",
|
||||
"dc_ip": ["2:149.154.167.220", "4:149.154.167.220"],
|
||||
"verbose": False,
|
||||
"autostart": False,
|
||||
"log_max_mb": 5,
|
||||
"buf_kb": 256,
|
||||
"pool_size": 4,
|
||||
}
|
||||
DEFAULT_CONFIG = default_tray_config()
|
||||
|
||||
|
||||
_proxy_thread: Optional[threading.Thread] = None
|
||||
|
|
@ -404,6 +396,50 @@ def _show_info(text: str, title: str = "TG WS Proxy"):
|
|||
_user32.MessageBoxW(None, text, title, 0x40)
|
||||
|
||||
|
||||
def _ask_open_release_page(latest_version: str, url: str) -> bool:
|
||||
"""Win32 Yes/No: открыть страницу релиза."""
|
||||
MB_YESNO = 0x4
|
||||
MB_ICONQUESTION = 0x20
|
||||
IDYES = 6
|
||||
text = (
|
||||
f"Доступна новая версия: {latest_version}\n\n"
|
||||
f"Открыть страницу релиза в браузере?"
|
||||
)
|
||||
r = _user32.MessageBoxW(
|
||||
None,
|
||||
text,
|
||||
"TG WS Proxy — обновление",
|
||||
MB_YESNO | MB_ICONQUESTION,
|
||||
)
|
||||
return r == IDYES
|
||||
|
||||
|
||||
def _maybe_notify_update_async():
|
||||
"""
|
||||
Фоновая проверка GitHub Releases и уведомление (не блокирует трей).
|
||||
"""
|
||||
def _work():
|
||||
time.sleep(1.5)
|
||||
if _exiting:
|
||||
return
|
||||
if not _config.get("check_updates", True):
|
||||
return
|
||||
try:
|
||||
from utils.update_check import RELEASES_PAGE_URL, get_status, run_check
|
||||
run_check(__version__)
|
||||
st = get_status()
|
||||
if not st.get("has_update"):
|
||||
return
|
||||
url = (st.get("html_url") or "").strip() or RELEASES_PAGE_URL
|
||||
ver = st.get("latest") or "?"
|
||||
if _ask_open_release_page(str(ver), url):
|
||||
webbrowser.open(url)
|
||||
except Exception as exc:
|
||||
log.debug("Update check failed: %s", exc)
|
||||
|
||||
threading.Thread(target=_work, daemon=True, name="update-check").start()
|
||||
|
||||
|
||||
def _on_open_in_telegram(icon=None, item=None):
|
||||
host = _config.get("host", DEFAULT_CONFIG["host"])
|
||||
port = _config.get("port", DEFAULT_CONFIG["port"])
|
||||
|
|
@ -705,6 +741,8 @@ def run_tray():
|
|||
|
||||
start_proxy()
|
||||
|
||||
_maybe_notify_update_async()
|
||||
|
||||
_show_first_run()
|
||||
_check_ipv6_warning()
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue