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` ссылку
|
- **Открыть в Telegram** — автоматически настроить прокси через `tg://socks` ссылку
|
||||||
- **Перезапустить прокси** — перезапуск без выхода из приложения
|
- **Перезапустить прокси** — перезапуск без выхода из приложения
|
||||||
- **Настройки...** — GUI-редактор конфигурации
|
- **Настройки...** — GUI-редактор конфигурации (в т.ч. версия приложения, опциональная проверка обновлений с GitHub)
|
||||||
- **Открыть логи** — открыть файл логов
|
- **Открыть логи** — открыть файл логов
|
||||||
- **Выход** — остановить прокси и закрыть приложение
|
- **Выход** — остановить прокси и закрыть приложение
|
||||||
|
|
||||||
|
При первом запуске после старта может появиться запрос об открытии страницы релиза, если на GitHub вышла новая версия (отключается в настройках).
|
||||||
|
|
||||||
### macOS
|
### macOS
|
||||||
|
|
||||||
Перейдите на [страницу релизов](https://github.com/Flowseal/tg-ws-proxy/releases) и скачайте **`TgWsProxy_macos_universal.dmg`** — универсальная сборка для Apple Silicon и Intel.
|
Перейдите на [страницу релизов](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
|
### Консольный proxy
|
||||||
|
|
||||||
Для запуска только SOCKS5/WebSocket proxy без tray-интерфейса достаточно базовой установки:
|
Для запуска только SOCKS5/WebSocket proxy без tray-интерфейса достаточно базовой установки:
|
||||||
|
|
@ -94,6 +101,7 @@ tg-ws-proxy
|
||||||
### Windows 7/10+
|
### Windows 7/10+
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
pip install -r requirements/windows-py39plus.txt
|
||||||
pip install -e .
|
pip install -e .
|
||||||
tg-ws-proxy-tray-win
|
tg-ws-proxy-tray-win
|
||||||
```
|
```
|
||||||
|
|
@ -101,6 +109,7 @@ tg-ws-proxy-tray-win
|
||||||
### macOS
|
### macOS
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
pip install -r requirements/macos.txt
|
||||||
pip install -e .
|
pip install -e .
|
||||||
tg-ws-proxy-tray-macos
|
tg-ws-proxy-tray-macos
|
||||||
```
|
```
|
||||||
|
|
@ -108,6 +117,7 @@ tg-ws-proxy-tray-macos
|
||||||
### Linux
|
### Linux
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
pip install -r requirements/linux.txt
|
||||||
pip install -e .
|
pip install -e .
|
||||||
tg-ws-proxy-tray-linux
|
tg-ws-proxy-tray-linux
|
||||||
```
|
```
|
||||||
|
|
@ -179,15 +189,22 @@ Tray-приложение хранит данные в:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
|
"host": "127.0.0.1",
|
||||||
"port": 1080,
|
"port": 1080,
|
||||||
"dc_ip": [
|
"dc_ip": [
|
||||||
"2:149.154.167.220",
|
"2:149.154.167.220",
|
||||||
"4: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)) для автоматической сборки.
|
Проект содержит спецификации 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 subprocess
|
||||||
import sys
|
import sys
|
||||||
import threading
|
import threading
|
||||||
|
import webbrowser
|
||||||
import time
|
import time
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Dict, Optional
|
from typing import Dict, Optional
|
||||||
|
|
@ -20,6 +21,7 @@ from PIL import Image, ImageDraw, ImageFont
|
||||||
|
|
||||||
import proxy.tg_ws_proxy as tg_ws_proxy
|
import proxy.tg_ws_proxy as tg_ws_proxy
|
||||||
from proxy import __version__
|
from proxy import __version__
|
||||||
|
from utils.default_config import default_tray_config
|
||||||
from ui.ctk_tray_ui import (
|
from ui.ctk_tray_ui import (
|
||||||
install_tray_config_buttons,
|
install_tray_config_buttons,
|
||||||
install_tray_config_form,
|
install_tray_config_form,
|
||||||
|
|
@ -44,15 +46,7 @@ FIRST_RUN_MARKER = APP_DIR / ".first_run_done"
|
||||||
IPV6_WARN_MARKER = APP_DIR / ".ipv6_warned"
|
IPV6_WARN_MARKER = APP_DIR / ".ipv6_warned"
|
||||||
|
|
||||||
|
|
||||||
DEFAULT_CONFIG = {
|
DEFAULT_CONFIG = default_tray_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,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
_proxy_thread: Optional[threading.Thread] = None
|
_proxy_thread: Optional[threading.Thread] = None
|
||||||
|
|
@ -350,6 +344,48 @@ def _show_info(text: str, title: str = "TG WS Proxy"):
|
||||||
root.destroy()
|
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):
|
def _on_open_in_telegram(icon=None, item=None):
|
||||||
host = _config.get("host", DEFAULT_CONFIG["host"])
|
host = _config.get("host", DEFAULT_CONFIG["host"])
|
||||||
port = _config.get("port", DEFAULT_CONFIG["port"])
|
port = _config.get("port", DEFAULT_CONFIG["port"])
|
||||||
|
|
@ -624,6 +660,8 @@ def run_tray():
|
||||||
|
|
||||||
start_proxy()
|
start_proxy()
|
||||||
|
|
||||||
|
_maybe_notify_update_async()
|
||||||
|
|
||||||
_show_first_run()
|
_show_first_run()
|
||||||
_check_ipv6_warning()
|
_check_ipv6_warning()
|
||||||
|
|
||||||
|
|
|
||||||
72
macos.py
72
macos.py
|
|
@ -31,6 +31,7 @@ except ImportError:
|
||||||
|
|
||||||
import proxy.tg_ws_proxy as tg_ws_proxy
|
import proxy.tg_ws_proxy as tg_ws_proxy
|
||||||
from proxy import __version__
|
from proxy import __version__
|
||||||
|
from utils.default_config import default_tray_config
|
||||||
|
|
||||||
APP_NAME = "TgWsProxy"
|
APP_NAME = "TgWsProxy"
|
||||||
APP_DIR = Path.home() / "Library" / "Application Support" / APP_NAME
|
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"
|
IPV6_WARN_MARKER = APP_DIR / ".ipv6_warned"
|
||||||
MENUBAR_ICON_PATH = APP_DIR / "menubar_icon.png"
|
MENUBAR_ICON_PATH = APP_DIR / "menubar_icon.png"
|
||||||
|
|
||||||
DEFAULT_CONFIG = {
|
DEFAULT_CONFIG = default_tray_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,
|
|
||||||
}
|
|
||||||
|
|
||||||
_proxy_thread: Optional[threading.Thread] = None
|
_proxy_thread: Optional[threading.Thread] = None
|
||||||
_async_stop: Optional[object] = None
|
_async_stop: Optional[object] = None
|
||||||
|
|
@ -427,6 +420,55 @@ def _on_edit_config(_=None):
|
||||||
threading.Thread(target=_edit_config_dialog, daemon=True).start()
|
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
|
# Settings via native macOS dialogs
|
||||||
def _edit_config_dialog():
|
def _edit_config_dialog():
|
||||||
cfg = load_config()
|
cfg = load_config()
|
||||||
|
|
@ -617,6 +659,12 @@ class TgWsProxyApp(_TgWsProxyAppBase):
|
||||||
self._logs_item = rumps.MenuItem(
|
self._logs_item = rumps.MenuItem(
|
||||||
"Открыть логи",
|
"Открыть логи",
|
||||||
callback=_on_open_logs)
|
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(
|
self._version_item = rumps.MenuItem(
|
||||||
f"Версия {__version__}",
|
f"Версия {__version__}",
|
||||||
callback=lambda _: None)
|
callback=lambda _: None)
|
||||||
|
|
@ -633,6 +681,9 @@ class TgWsProxyApp(_TgWsProxyAppBase):
|
||||||
self._settings_item,
|
self._settings_item,
|
||||||
self._logs_item,
|
self._logs_item,
|
||||||
None,
|
None,
|
||||||
|
self._release_page_item,
|
||||||
|
self._check_updates_item,
|
||||||
|
None,
|
||||||
self._version_item,
|
self._version_item,
|
||||||
])
|
])
|
||||||
|
|
||||||
|
|
@ -672,6 +723,9 @@ def run_menubar():
|
||||||
return
|
return
|
||||||
|
|
||||||
start_proxy()
|
start_proxy()
|
||||||
|
|
||||||
|
_maybe_notify_update_async()
|
||||||
|
|
||||||
_show_first_run()
|
_show_first_run()
|
||||||
_check_ipv6_warning()
|
_check_ipv6_warning()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -62,7 +62,7 @@ Source = "https://github.com/Flowseal/tg-ws-proxy"
|
||||||
Issues = "https://github.com/Flowseal/tg-ws-proxy/issues"
|
Issues = "https://github.com/Flowseal/tg-ws-proxy/issues"
|
||||||
|
|
||||||
[tool.hatch.build.targets.wheel]
|
[tool.hatch.build.targets.wheel]
|
||||||
packages = ["proxy", "ui"]
|
packages = ["proxy", "ui", "utils"]
|
||||||
|
|
||||||
[tool.hatch.build.force-include]
|
[tool.hatch.build.force-include]
|
||||||
"windows.py" = "windows.py"
|
"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
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import webbrowser
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Any, Callable, Dict, List, Optional, Tuple, Union
|
from typing import Any, Callable, Dict, List, Optional, Tuple, Union
|
||||||
|
|
||||||
import proxy.tg_ws_proxy as tg_ws_proxy
|
import proxy.tg_ws_proxy as tg_ws_proxy
|
||||||
from proxy import __version__
|
from proxy import __version__
|
||||||
|
from utils.update_check import RELEASES_PAGE_URL, get_status
|
||||||
|
|
||||||
from ui.ctk_theme import (
|
from ui.ctk_theme import (
|
||||||
FIRST_RUN_FRAME_PAD,
|
FIRST_RUN_FRAME_PAD,
|
||||||
|
|
@ -52,6 +54,10 @@ _TIP_AUTOSTART = (
|
||||||
"Запускать TG WS Proxy при входе в Windows. "
|
"Запускать TG WS Proxy при входе в Windows. "
|
||||||
"Если вы переместите программу в другую папку, запись автозапуска может сброситься."
|
"Если вы переместите программу в другую папку, запись автозапуска может сброситься."
|
||||||
)
|
)
|
||||||
|
_TIP_CHECK_UPDATES = (
|
||||||
|
"При запуске запрашивать с GitHub информацию о последнем релизе. "
|
||||||
|
"При появлении новой версии можно открыть страницу загрузки."
|
||||||
|
)
|
||||||
_TIP_SAVE = "Сохранить настройки в файл. После сохранения можно перезапустить прокси."
|
_TIP_SAVE = "Сохранить настройки в файл. После сохранения можно перезапустить прокси."
|
||||||
_TIP_CANCEL = "Закрыть окно без сохранения изменений."
|
_TIP_CANCEL = "Закрыть окно без сохранения изменений."
|
||||||
|
|
||||||
|
|
@ -121,6 +127,7 @@ class TrayConfigFormWidgets:
|
||||||
adv_entries: List[Any]
|
adv_entries: List[Any]
|
||||||
adv_keys: Tuple[str, ...]
|
adv_keys: Tuple[str, ...]
|
||||||
autostart_var: Optional[Any]
|
autostart_var: Optional[Any]
|
||||||
|
check_updates_var: Optional[Any]
|
||||||
|
|
||||||
|
|
||||||
def install_tray_config_form(
|
def install_tray_config_form(
|
||||||
|
|
@ -291,6 +298,71 @@ def install_tray_config_form(
|
||||||
adv_entries = list(adv_frame.winfo_children())
|
adv_entries = list(adv_frame.winfo_children())
|
||||||
adv_keys = ("buf_kb", "pool_size", "log_max_mb")
|
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
|
autostart_var = None
|
||||||
if show_autostart:
|
if show_autostart:
|
||||||
sys_inner = _config_section(
|
sys_inner = _config_section(
|
||||||
|
|
@ -330,6 +402,7 @@ def install_tray_config_form(
|
||||||
adv_entries=adv_entries,
|
adv_entries=adv_entries,
|
||||||
adv_keys=adv_keys,
|
adv_keys=adv_keys,
|
||||||
autostart_var=autostart_var,
|
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)
|
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
|
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
|
import proxy.tg_ws_proxy as tg_ws_proxy
|
||||||
from proxy import __version__
|
from proxy import __version__
|
||||||
|
from utils.default_config import default_tray_config
|
||||||
from ui.ctk_tray_ui import (
|
from ui.ctk_tray_ui import (
|
||||||
install_tray_config_buttons,
|
install_tray_config_buttons,
|
||||||
install_tray_config_form,
|
install_tray_config_form,
|
||||||
|
|
@ -65,16 +66,7 @@ FIRST_RUN_MARKER = APP_DIR / ".first_run_done"
|
||||||
IPV6_WARN_MARKER = APP_DIR / ".ipv6_warned"
|
IPV6_WARN_MARKER = APP_DIR / ".ipv6_warned"
|
||||||
|
|
||||||
|
|
||||||
DEFAULT_CONFIG = {
|
DEFAULT_CONFIG = default_tray_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,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
_proxy_thread: Optional[threading.Thread] = None
|
_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)
|
_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):
|
def _on_open_in_telegram(icon=None, item=None):
|
||||||
host = _config.get("host", DEFAULT_CONFIG["host"])
|
host = _config.get("host", DEFAULT_CONFIG["host"])
|
||||||
port = _config.get("port", DEFAULT_CONFIG["port"])
|
port = _config.get("port", DEFAULT_CONFIG["port"])
|
||||||
|
|
@ -705,6 +741,8 @@ def run_tray():
|
||||||
|
|
||||||
start_proxy()
|
start_proxy()
|
||||||
|
|
||||||
|
_maybe_notify_update_async()
|
||||||
|
|
||||||
_show_first_run()
|
_show_first_run()
|
||||||
_check_ipv6_warning()
|
_check_ipv6_warning()
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue