From 8219b9f14479e8b05f7336b9588e1011bfb69fb8 Mon Sep 17 00:00:00 2001 From: Flowseal Date: Tue, 17 Mar 2026 22:15:04 +0300 Subject: [PATCH 01/22] pyinstaller changed to previous version for false detect prevention --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9887746..cfab904 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -28,7 +28,7 @@ jobs: run: pip install -r requirements.txt - name: Install pyinstaller - run: pip install pyinstaller + run: pip install "pyinstaller==6.13.0" - name: Build EXE with PyInstaller run: pyinstaller packaging/windows.spec --noconfirm From 46011c0ff53bab10a53b12b1a9a27cf5b6e8525a Mon Sep 17 00:00:00 2001 From: Flowseal Date: Tue, 17 Mar 2026 22:18:21 +0300 Subject: [PATCH 02/22] Github optional release on build --- .github/workflows/build.yml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index cfab904..b31cb6a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -3,9 +3,14 @@ name: Build & Release on: workflow_dispatch: inputs: + make_release: + description: 'Create Github Release?' + type: boolean + required: true + default: false version: description: "Release version tag (e.g. v1.0.0)" - required: true + required: false default: "v1.0.0" permissions: @@ -73,6 +78,7 @@ jobs: release: needs: [build, build-win7] runs-on: ubuntu-latest + if: ${{ github.event.inputs.make_release == 'true' }} steps: - name: Download main build uses: actions/download-artifact@v4 From 473078593a315027e5c17595da2b70841adeb50e Mon Sep 17 00:00:00 2001 From: hir-lol Date: Wed, 18 Mar 2026 01:40:09 +0300 Subject: [PATCH 03/22] Merge pull request #244 from hir-lol/main --- windows.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/windows.py b/windows.py index 64e581b..8e8ed5d 100644 --- a/windows.py +++ b/windows.py @@ -314,6 +314,8 @@ def _edit_config_dialog(): root.title("TG WS Proxy — Настройки") root.resizable(False, False) root.attributes("-topmost", True) + icon_path = str(Path(__file__).parent / "icon.ico") + root.iconbitmap(icon_path) TG_BLUE = "#3390ec" TG_BLUE_HOVER = "#2b7cd4" @@ -502,6 +504,8 @@ def _show_first_run(): root.title("TG WS Proxy") root.resizable(False, False) root.attributes("-topmost", True) + icon_path = str(Path(__file__).parent / "icon.ico") + root.iconbitmap(icon_path) w, h = 520, 440 sw = root.winfo_screenwidth() From 533420b516ae88586699295585cdd6f8d8cdaf62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=98=D0=BB=D1=8C=D1=8F?= Date: Wed, 18 Mar 2026 17:33:38 +0300 Subject: [PATCH 04/22] MacOS support (#225) --- .github/workflows/build.yml | 108 +++++- .gitignore | 1 + README.md | 44 ++- macos.py | 626 ++++++++++++++++++++++++++++++++++ packaging/create_icon.sh | 30 ++ packaging/macos.spec | 83 +++++ packaging/merge_universal2.sh | 64 ++++ requirements-macos.txt | 5 + requirements.txt | 2 +- 9 files changed, 959 insertions(+), 4 deletions(-) create mode 100644 macos.py create mode 100644 packaging/create_icon.sh create mode 100644 packaging/macos.spec create mode 100644 packaging/merge_universal2.sh create mode 100644 requirements-macos.txt diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b31cb6a..d804c69 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -75,8 +75,105 @@ jobs: name: TgWsProxy-win7 path: dist/TgWsProxy-win7.exe + build-macos: + runs-on: macos-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + cache: "pip" + + - name: Install dependencies + run: pip install -r requirements-macos.txt + + - name: Install pyinstaller + run: pip install pyinstaller + + - name: Create macOS icon from ICO + run: | + chmod +x packaging/create_icon.sh + packaging/create_icon.sh + + - name: Build app with PyInstaller + run: pyinstaller packaging/macos.spec --noconfirm + + - name: Upload app bundle + uses: actions/upload-artifact@v4 + with: + name: TgWsProxy-app-arm64 + path: "dist/TG WS Proxy.app" + + build-macos-intel: + runs-on: macos-15-intel + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + cache: "pip" + + - name: Install dependencies + run: pip install -r requirements-macos.txt + + - name: Install pyinstaller + run: pip install pyinstaller + + - name: Create macOS icon from ICO + run: | + chmod +x packaging/create_icon.sh + packaging/create_icon.sh + + - name: Build app with PyInstaller + run: pyinstaller packaging/macos.spec --noconfirm + + - name: Upload app bundle + uses: actions/upload-artifact@v4 + with: + name: TgWsProxy-app-x86_64 + path: "dist/TG WS Proxy.app" + + build-macos-universal: + needs: [build-macos, build-macos-intel] + runs-on: macos-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Download arm64 app + uses: actions/download-artifact@v4 + with: + name: TgWsProxy-app-arm64 + path: "dist/arm64/TG WS Proxy.app" + + - name: Download x86_64 app + uses: actions/download-artifact@v4 + with: + name: TgWsProxy-app-x86_64 + path: "dist/x86_64/TG WS Proxy.app" + + - name: Merge into universal2 app and create DMG + run: | + chmod +x packaging/merge_universal2.sh + packaging/merge_universal2.sh \ + "dist/arm64/TG WS Proxy.app" \ + "dist/x86_64/TG WS Proxy.app" \ + "dist/TG WS Proxy.app" + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: TgWsProxy-macOS + path: dist/TgWsProxy.dmg + release: - needs: [build, build-win7] + needs: [build, build-win7, build-macos-universal] runs-on: ubuntu-latest if: ${{ github.event.inputs.make_release == 'true' }} steps: @@ -92,6 +189,12 @@ jobs: name: TgWsProxy-win7 path: dist + - name: Download macOS build + uses: actions/download-artifact@v4 + with: + name: TgWsProxy-macOS + path: dist + - name: Create GitHub Release uses: softprops/action-gh-release@v2 with: @@ -102,7 +205,8 @@ jobs: files: | dist/TgWsProxy.exe dist/TgWsProxy-win7.exe + dist/TgWsProxy.dmg draft: false prerelease: false env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore index 42a354f..8aee6ca 100644 --- a/.gitignore +++ b/.gitignore @@ -27,3 +27,4 @@ scan_ips.py scan.txt AyuGramDesktop-dev/ tweb-master/ +/icon.icns diff --git a/README.md b/README.md index 1965130..8cefc59 100644 --- a/README.md +++ b/README.md @@ -44,17 +44,32 @@ Telegram Desktop → SOCKS5 (127.0.0.1:1080) → TG WS Proxy → WSS (kws*.web.t ## Установка из исходников +> Для Windows: + ```bash pip install -r requirements.txt ``` +> Для MacOS: +```bash +pip install -r requirements-macos.txt +``` + ### Windows (Tray-приложение) ```bash python windows.py ``` -### Консольный режим +### MacOS (Tray-приложение) + +Перейдите на [страницу релизов](https://github.com/Flowseal/tg-ws-proxy/releases) и скачайте **`TgWsProxy.dmg`** — универсальная сборка для Apple Silicon и Intel. + +1. Открыть образ +2. Перенести **TG WS Proxy.app** в папку **Applications** +3. При первом запуске macOS может попросить подтвердить открытие: **Системные настройки → Конфиденциальность и безопасность → Всё равно открыть** + +### Консольный режим (Windows) ```bash python proxy/tg_ws_proxy.py [--port PORT] [--dc-ip DC:IP ...] [-v] @@ -81,6 +96,11 @@ python proxy/tg_ws_proxy.py --port 9050 --dc-ip 1:149.154.175.205 --dc-ip 2:149. python proxy/tg_ws_proxy.py -v ``` +### Консольный режим (MacOS) +```bash +python macos.py +``` + ## Настройка Telegram Desktop ### Автоматически @@ -115,11 +135,33 @@ Tray-приложение хранит данные в `%APPDATA%/TgWsProxy`: Проект содержит спецификацию PyInstaller ([`windows.spec`](packaging/windows.spec)) и GitHub Actions workflow ([`.github/workflows/build.yml`](.github/workflows/build.yml)) для автоматической сборки. +> Для Windows: + ```bash pip install pyinstaller pyinstaller packaging/windows.spec ``` +> Для MacOS: +```bash +pip install pyinstaller +packaging/create_icon.sh +pyinstaller packaging/macos.spec +``` + +> Создать универсальный DMG (local-тест с двумя копями одной архитектуры): +```bash +pip install pyinstaller +packaging/create_icon.sh +pyinstaller packaging/macos.spec --noconfirm +cp -R "dist/TG WS Proxy.app" "dist/TG WS Proxy-intel.app" +packaging/merge_universal2.sh \ + "dist/TG WS Proxy.app" \ + "dist/TG WS Proxy-intel.app" \ + "dist/TG WS Proxy-universal.app" +``` +- В результате в папке `dist/` появятся `.app` под текущую архитектуру и `TgWsProxy.dmg`. + ## Лицензия [MIT License](LICENSE) diff --git a/macos.py b/macos.py new file mode 100644 index 0000000..fc0b44a --- /dev/null +++ b/macos.py @@ -0,0 +1,626 @@ +from __future__ import annotations + +import json +import logging +import os +import psutil +import subprocess +import sys +import threading +import time +import webbrowser +import asyncio as _asyncio +from pathlib import Path +from typing import Dict, Optional + +try: + import rumps +except ImportError: + rumps = None + +try: + from PIL import Image, ImageDraw, ImageFont +except ImportError: + Image = ImageDraw = ImageFont = None + +try: + import pyperclip +except ImportError: + pyperclip = None + +import proxy.tg_ws_proxy as tg_ws_proxy + +APP_NAME = "TgWsProxy" +APP_DIR = Path.home() / "Library" / "Application Support" / APP_NAME +CONFIG_FILE = APP_DIR / "config.json" +LOG_FILE = APP_DIR / "proxy.log" +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, +} + +_proxy_thread: Optional[threading.Thread] = None +_async_stop: Optional[object] = None +_app: Optional[object] = None +_config: dict = {} +_exiting: bool = False +_lock_file_path: Optional[Path] = None + +log = logging.getLogger("tg-ws-tray") + + +# Single-instance lock + +def _same_process(lock_meta: dict, proc: psutil.Process) -> bool: + try: + lock_ct = float(lock_meta.get("create_time", 0.0)) + proc_ct = float(proc.create_time()) + if lock_ct > 0 and abs(lock_ct - proc_ct) > 1.0: + return False + except Exception: + return False + + frozen = bool(getattr(sys, "frozen", False)) + if frozen: + return APP_NAME.lower() in proc.name().lower() + return False + + +def _release_lock(): + global _lock_file_path + if not _lock_file_path: + return + try: + _lock_file_path.unlink(missing_ok=True) + except Exception: + pass + _lock_file_path = None + + +def _acquire_lock() -> bool: + global _lock_file_path + _ensure_dirs() + lock_files = list(APP_DIR.glob("*.lock")) + + for f in lock_files: + pid = None + meta: dict = {} + + try: + pid = int(f.stem) + except Exception: + f.unlink(missing_ok=True) + continue + + try: + raw = f.read_text(encoding="utf-8").strip() + if raw: + meta = json.loads(raw) + except Exception: + meta = {} + + try: + proc = psutil.Process(pid) + if _same_process(meta, proc): + return False + except Exception: + pass + + f.unlink(missing_ok=True) + + lock_file = APP_DIR / f"{os.getpid()}.lock" + try: + proc = psutil.Process(os.getpid()) + payload = {"create_time": proc.create_time()} + lock_file.write_text(json.dumps(payload, ensure_ascii=False), + encoding="utf-8") + except Exception: + lock_file.touch() + + _lock_file_path = lock_file + return True + + +# Filesystem helpers + +def _ensure_dirs(): + APP_DIR.mkdir(parents=True, exist_ok=True) + + +def load_config() -> dict: + _ensure_dirs() + if CONFIG_FILE.exists(): + try: + with open(CONFIG_FILE, "r", encoding="utf-8") as f: + data = json.load(f) + for k, v in DEFAULT_CONFIG.items(): + data.setdefault(k, v) + return data + except Exception as exc: + log.warning("Failed to load config: %s", exc) + return dict(DEFAULT_CONFIG) + + +def save_config(cfg: dict): + _ensure_dirs() + with open(CONFIG_FILE, "w", encoding="utf-8") as f: + json.dump(cfg, f, indent=2, ensure_ascii=False) + + +def setup_logging(verbose: bool = False): + _ensure_dirs() + root = logging.getLogger() + root.setLevel(logging.DEBUG if verbose else logging.INFO) + + fh = logging.FileHandler(str(LOG_FILE), encoding="utf-8") + fh.setLevel(logging.DEBUG) + fh.setFormatter(logging.Formatter( + "%(asctime)s %(levelname)-5s %(name)s %(message)s", + datefmt="%Y-%m-%d %H:%M:%S")) + root.addHandler(fh) + + if not getattr(sys, "frozen", False): + ch = logging.StreamHandler(sys.stdout) + ch.setLevel(logging.DEBUG if verbose else logging.INFO) + ch.setFormatter(logging.Formatter( + "%(asctime)s %(levelname)-5s %(message)s", + datefmt="%H:%M:%S")) + root.addHandler(ch) + + +# Menubar icon + +def _make_menubar_icon(size: int = 44): + if Image is None: + return None + img = Image.new("RGBA", (size, size), (0, 0, 0, 0)) + draw = ImageDraw.Draw(img) + + margin = size // 11 + draw.ellipse([margin, margin, size - margin, size - margin], + fill=(0, 0, 0, 255)) + + try: + font = ImageFont.truetype( + "/System/Library/Fonts/Helvetica.ttc", + size=int(size * 0.55)) + except Exception: + font = ImageFont.load_default() + + bbox = draw.textbbox((0, 0), "T", font=font) + tw, th = bbox[2] - bbox[0], bbox[3] - bbox[1] + tx = (size - tw) // 2 - bbox[0] + ty = (size - th) // 2 - bbox[1] + draw.text((tx, ty), "T", fill=(255, 255, 255, 255), font=font) + return img + +# Generate menubar icon PNG if it does not exist. +def _ensure_menubar_icon(): + if MENUBAR_ICON_PATH.exists(): + return + _ensure_dirs() + img = _make_menubar_icon(44) + if img: + img.save(str(MENUBAR_ICON_PATH), "PNG") + + +# Native macOS dialogs + +def _osascript(script: str) -> str: + r = subprocess.run( + ['osascript', '-e', script], + capture_output=True, text=True) + return r.stdout.strip() + + +def _show_error(text: str, title: str = "TG WS Proxy"): + text_esc = text.replace('\\', '\\\\').replace('"', '\\"') + title_esc = title.replace('\\', '\\\\').replace('"', '\\"') + _osascript( + f'display dialog "{text_esc}" with title "{title_esc}" ' + f'buttons {{"OK"}} default button "OK" with icon stop') + + +def _show_info(text: str, title: str = "TG WS Proxy"): + text_esc = text.replace('\\', '\\\\').replace('"', '\\"') + title_esc = title.replace('\\', '\\\\').replace('"', '\\"') + _osascript( + f'display dialog "{text_esc}" with title "{title_esc}" ' + f'buttons {{"OK"}} default button "OK" with icon note') + + +def _ask_yes_no(text: str, title: str = "TG WS Proxy") -> bool: + text_esc = text.replace('\\', '\\\\').replace('"', '\\"') + title_esc = title.replace('\\', '\\\\').replace('"', '\\"') + result = _osascript( + f'display dialog "{text_esc}" with title "{title_esc}" ' + f'buttons {{"Нет", "Да"}} default button "Да" with icon note') + return "Да" in result + + +# Proxy lifecycle + +def _run_proxy_thread(port: int, dc_opt: Dict[int, str], verbose: bool, + host: str = '127.0.0.1'): + global _async_stop + loop = _asyncio.new_event_loop() + _asyncio.set_event_loop(loop) + stop_ev = _asyncio.Event() + _async_stop = (loop, stop_ev) + + try: + loop.run_until_complete( + tg_ws_proxy._run(port, dc_opt, stop_event=stop_ev, host=host)) + except Exception as exc: + log.error("Proxy thread crashed: %s", exc) + if "Address already in use" in str(exc): + _show_error( + "Не удалось запустить прокси:\n" + "Порт уже используется другим приложением.\n\n" + "Закройте приложение, использующее этот порт, " + "или измените порт в настройках прокси и перезапустите.") + finally: + loop.close() + _async_stop = None + + +def start_proxy(): + global _proxy_thread, _config + if _proxy_thread and _proxy_thread.is_alive(): + log.info("Proxy already running") + return + + cfg = _config + port = cfg.get("port", DEFAULT_CONFIG["port"]) + host = cfg.get("host", DEFAULT_CONFIG["host"]) + dc_ip_list = cfg.get("dc_ip", DEFAULT_CONFIG["dc_ip"]) + verbose = cfg.get("verbose", False) + + try: + dc_opt = tg_ws_proxy.parse_dc_ip_list(dc_ip_list) + except ValueError as e: + log.error("Bad config dc_ip: %s", e) + _show_error(f"Ошибка конфигурации:\n{e}") + return + + log.info("Starting proxy on %s:%d ...", host, port) + _proxy_thread = threading.Thread( + target=_run_proxy_thread, + args=(port, dc_opt, verbose, host), + daemon=True, name="proxy") + _proxy_thread.start() + + +def stop_proxy(): + global _proxy_thread, _async_stop + if _async_stop: + loop, stop_ev = _async_stop + loop.call_soon_threadsafe(stop_ev.set) + if _proxy_thread: + _proxy_thread.join(timeout=2) + _proxy_thread = None + log.info("Proxy stopped") + + +def restart_proxy(): + log.info("Restarting proxy...") + stop_proxy() + time.sleep(0.3) + start_proxy() + + +# Menu callbacks + +def _on_open_in_telegram(_=None): + port = _config.get("port", DEFAULT_CONFIG["port"]) + url = f"tg://socks?server=127.0.0.1&port={port}" + log.info("Opening %s", url) + try: + result = subprocess.call(['open', url]) + if result != 0: + raise RuntimeError("open command failed") + except Exception: + log.info("open command failed, trying webbrowser") + try: + if not webbrowser.open(url): + raise RuntimeError("webbrowser.open returned False") + except Exception: + log.info("Browser open failed, copying to clipboard") + try: + if pyperclip: + pyperclip.copy(url) + else: + subprocess.run(['pbcopy'], input=url.encode(), + check=True) + _show_info( + "Не удалось открыть Telegram автоматически.\n\n" + f"Ссылка скопирована в буфер обмена:\n{url}") + except Exception as exc: + log.error("Clipboard copy failed: %s", exc) + _show_error(f"Не удалось скопировать ссылку:\n{exc}") + + +def _on_restart(_=None): + def _do_restart(): + global _config + _config = load_config() + if _app: + _app.update_menu_title() + restart_proxy() + + threading.Thread(target=_do_restart, daemon=True).start() + + +def _on_open_logs(_=None): + log.info("Opening log file: %s", LOG_FILE) + if LOG_FILE.exists(): + subprocess.call(['open', str(LOG_FILE)]) + else: + _show_info("Файл логов ещё не создан.") + +# Show a native text input dialog. Returns None if cancelled. +def _osascript_input(prompt: str, default: str, + title: str = "TG WS Proxy") -> Optional[str]: + prompt_esc = prompt.replace('\\', '\\\\').replace('"', '\\"') + default_esc = default.replace('\\', '\\\\').replace('"', '\\"') + title_esc = title.replace('\\', '\\\\').replace('"', '\\"') + r = subprocess.run( + ['osascript', '-e', + f'display dialog "{prompt_esc}" ' + f'default answer "{default_esc}" ' + f'with title "{title_esc}" ' + f'buttons {{"Отмена", "OK"}} default button "OK"'], + capture_output=True, text=True) + if r.returncode != 0: + return None + for part in r.stdout.strip().split(', '): + if part.startswith('text returned:'): + return part[len('text returned:'):] + return None + + +def _on_edit_config(_=None): + threading.Thread(target=_edit_config_dialog, daemon=True).start() + + +# Settings via native macOS dialogs +def _edit_config_dialog(): + cfg = load_config() + + # Host + host = _osascript_input( + "IP-адрес прокси:", + cfg.get("host", DEFAULT_CONFIG["host"])) + if host is None: + return + host = host.strip() + + import socket as _sock + try: + _sock.inet_aton(host) + except OSError: + _show_error("Некорректный IP-адрес.") + return + + # Port + port_str = _osascript_input( + "Порт прокси:", + str(cfg.get("port", DEFAULT_CONFIG["port"]))) + if port_str is None: + return + try: + port = int(port_str.strip()) + if not (1 <= port <= 65535): + raise ValueError + except ValueError: + _show_error("Порт должен быть числом 1-65535") + return + + # DC-IP mappings + dc_default = ", ".join(cfg.get("dc_ip", DEFAULT_CONFIG["dc_ip"])) + dc_str = _osascript_input( + "DC → IP маппинги (через запятую, формат DC:IP):\n" + "Например: 2:149.154.167.220, 4:149.154.167.220", + dc_default) + if dc_str is None: + return + dc_lines = [s.strip() for s in dc_str.replace(',', '\n').splitlines() + if s.strip()] + try: + tg_ws_proxy.parse_dc_ip_list(dc_lines) + except ValueError as e: + _show_error(str(e)) + return + + # Verbose + verbose = _ask_yes_no("Включить подробное логирование (verbose)?") + + new_cfg = { + "host": host, + "port": port, + "dc_ip": dc_lines, + "verbose": verbose, + } + save_config(new_cfg) + log.info("Config saved: %s", new_cfg) + + global _config + _config = new_cfg + if _app: + _app.update_menu_title() + + if _ask_yes_no("Настройки сохранены.\n\nПерезапустить прокси сейчас?"): + restart_proxy() + + +# First-run & IPv6 dialogs + +def _show_first_run(): + _ensure_dirs() + if FIRST_RUN_MARKER.exists(): + return + + host = _config.get("host", DEFAULT_CONFIG["host"]) + port = _config.get("port", DEFAULT_CONFIG["port"]) + tg_url = f"tg://socks?server={host}&port={port}" + + text = ( + f"Прокси запущен и работает в строке меню.\n\n" + f"Как подключить Telegram Desktop:\n\n" + f"Автоматически:\n" + f" Нажмите «Открыть в Telegram» в меню\n" + f" Или ссылка: {tg_url}\n\n" + f"Вручную:\n" + f" Настройки → Продвинутые → Тип подключения → Прокси\n" + f" SOCKS5 → {host} : {port} (без логина/пароля)\n\n" + f"Открыть прокси в Telegram сейчас?" + ) + + FIRST_RUN_MARKER.touch() + + if _ask_yes_no(text, "TG WS Proxy"): + _on_open_in_telegram() + + +def _has_ipv6_enabled() -> bool: + import socket as _sock + try: + addrs = _sock.getaddrinfo(_sock.gethostname(), None, _sock.AF_INET6) + for addr in addrs: + ip = addr[4][0] + if ip and not ip.startswith('::1') and not ip.startswith('fe80::1'): + return True + except Exception: + pass + try: + s = _sock.socket(_sock.AF_INET6, _sock.SOCK_STREAM) + s.bind(('::1', 0)) + s.close() + return True + except Exception: + return False + + +def _check_ipv6_warning(): + _ensure_dirs() + if IPV6_WARN_MARKER.exists(): + return + if not _has_ipv6_enabled(): + return + + IPV6_WARN_MARKER.touch() + + _show_info( + "На вашем компьютере включена поддержка подключения по IPv6.\n\n" + "Telegram может пытаться подключаться через IPv6, " + "что не поддерживается и может привести к ошибкам.\n\n" + "Если прокси не работает, попробуйте отключить " + "попытку соединения по IPv6 в настройках прокси Telegram.\n\n" + "Это предупреждение будет показано только один раз.") + + +# rumps menubar app + +_TgWsProxyAppBase = rumps.App if rumps else object + + +class TgWsProxyApp(_TgWsProxyAppBase): + def __init__(self): + _ensure_menubar_icon() + icon_path = (str(MENUBAR_ICON_PATH) + if MENUBAR_ICON_PATH.exists() else None) + + host = _config.get("host", DEFAULT_CONFIG["host"]) + port = _config.get("port", DEFAULT_CONFIG["port"]) + + self._open_tg_item = rumps.MenuItem( + f"Открыть в Telegram ({host}:{port})", + callback=_on_open_in_telegram) + self._restart_item = rumps.MenuItem( + "Перезапустить прокси", + callback=_on_restart) + self._settings_item = rumps.MenuItem( + "Настройки...", + callback=_on_edit_config) + self._logs_item = rumps.MenuItem( + "Открыть логи", + callback=_on_open_logs) + + super().__init__( + "TG WS Proxy", + icon=icon_path, + template=True, + quit_button="Выход", + menu=[ + self._open_tg_item, + None, + self._restart_item, + self._settings_item, + self._logs_item, + ]) + + def update_menu_title(self): + host = _config.get("host", DEFAULT_CONFIG["host"]) + port = _config.get("port", DEFAULT_CONFIG["port"]) + self._open_tg_item.title = ( + f"Открыть в Telegram ({host}:{port})") + + +def run_menubar(): + global _app, _config + + _config = load_config() + save_config(_config) + + if LOG_FILE.exists(): + try: + LOG_FILE.unlink() + except Exception: + pass + + setup_logging(_config.get("verbose", False)) + log.info("TG WS Proxy menubar app starting") + log.info("Config: %s", _config) + log.info("Log file: %s", LOG_FILE) + + if rumps is None or Image is None: + log.error("rumps or Pillow not installed; running in console mode") + start_proxy() + try: + while True: + time.sleep(1) + except KeyboardInterrupt: + stop_proxy() + return + + start_proxy() + _show_first_run() + _check_ipv6_warning() + + _app = TgWsProxyApp() + log.info("Menubar app running") + _app.run() + + stop_proxy() + log.info("Menubar app exited") + + +def main(): + if not _acquire_lock(): + _show_info("Приложение уже запущено.") + return + + try: + run_menubar() + finally: + _release_lock() + + +if __name__ == "__main__": + main() diff --git a/packaging/create_icon.sh b/packaging/create_icon.sh new file mode 100644 index 0000000..e222e7a --- /dev/null +++ b/packaging/create_icon.sh @@ -0,0 +1,30 @@ +#!/bin/bash +# Create icon.icns from icon.ico for macOS app +set -e + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +PROJECT_DIR="$(dirname "$SCRIPT_DIR")" + +python3 -c " +from PIL import Image +img = Image.open('$PROJECT_DIR/icon.ico') +img = img.resize((1024, 1024), Image.LANCZOS) +img.save('$PROJECT_DIR/icon_1024.png', 'PNG') +" + +mkdir -p "$PROJECT_DIR/icon.iconset" +sips -z 16 16 "$PROJECT_DIR/icon_1024.png" --out "$PROJECT_DIR/icon.iconset/icon_16x16.png" +sips -z 32 32 "$PROJECT_DIR/icon_1024.png" --out "$PROJECT_DIR/icon.iconset/icon_16x16@2x.png" +sips -z 32 32 "$PROJECT_DIR/icon_1024.png" --out "$PROJECT_DIR/icon.iconset/icon_32x32.png" +sips -z 64 64 "$PROJECT_DIR/icon_1024.png" --out "$PROJECT_DIR/icon.iconset/icon_32x32@2x.png" +sips -z 128 128 "$PROJECT_DIR/icon_1024.png" --out "$PROJECT_DIR/icon.iconset/icon_128x128.png" +sips -z 256 256 "$PROJECT_DIR/icon_1024.png" --out "$PROJECT_DIR/icon.iconset/icon_128x128@2x.png" +sips -z 256 256 "$PROJECT_DIR/icon_1024.png" --out "$PROJECT_DIR/icon.iconset/icon_256x256.png" +sips -z 512 512 "$PROJECT_DIR/icon_1024.png" --out "$PROJECT_DIR/icon.iconset/icon_256x256@2x.png" +sips -z 512 512 "$PROJECT_DIR/icon_1024.png" --out "$PROJECT_DIR/icon.iconset/icon_512x512.png" +sips -z 1024 1024 "$PROJECT_DIR/icon_1024.png" --out "$PROJECT_DIR/icon.iconset/icon_512x512@2x.png" +iconutil -c icns "$PROJECT_DIR/icon.iconset" -o "$PROJECT_DIR/icon.icns" + +rm -rf "$PROJECT_DIR/icon.iconset" "$PROJECT_DIR/icon_1024.png" + +echo "icon.icns created: $PROJECT_DIR/icon.icns" diff --git a/packaging/macos.spec b/packaging/macos.spec new file mode 100644 index 0000000..065461e --- /dev/null +++ b/packaging/macos.spec @@ -0,0 +1,83 @@ +# -*- mode: python ; coding: utf-8 -*- + +import sys +import os + +block_cipher = None + +a = Analysis( + [os.path.join(os.path.dirname(SPEC), os.pardir, 'macos.py')], + pathex=[], + binaries=[], + datas=[], + hiddenimports=[ + 'rumps', + 'objc', + 'Foundation', + 'AppKit', + 'PyObjCTools', + 'PyObjCTools.AppHelper', + 'cryptography.hazmat.primitives.ciphers', + 'cryptography.hazmat.primitives.ciphers.algorithms', + 'cryptography.hazmat.primitives.ciphers.modes', + 'cryptography.hazmat.backends.openssl', + ], + hookspath=[], + hooksconfig={}, + runtime_hooks=[], + excludes=[], + noarchive=False, + cipher=block_cipher, +) + +icon_path = os.path.join(os.path.dirname(SPEC), os.pardir, 'icon.icns') +if not os.path.exists(icon_path): + icon_path = None + +pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher) + +exe = EXE( + pyz, + a.scripts, + [], + exclude_binaries=True, + name='TgWsProxy', + debug=False, + bootloader_ignore_signals=False, + strip=False, + upx=False, + console=False, + argv_emulation=False, + target_arch=None, + codesign_identity=None, + entitlements_file=None, +) + +coll = COLLECT( + exe, + a.binaries, + a.zipfiles, + a.datas, + strip=False, + upx=False, + upx_exclude=[], + name='TgWsProxy', +) + +app = BUNDLE( + coll, + name='TG WS Proxy.app', + icon=icon_path, + bundle_identifier='com.tgwsproxy.app', + info_plist={ + 'CFBundleName': 'TG WS Proxy', + 'CFBundleDisplayName': 'TG WS Proxy', + 'CFBundleShortVersionString': '1.0.0', + 'CFBundleVersion': '1.0.0', + 'LSMinimumSystemVersion': '10.15', + 'LSUIElement': True, + 'NSHighResolutionCapable': True, + 'NSAppleEventsUsageDescription': + 'TG WS Proxy needs to display dialogs.', + }, +) diff --git a/packaging/merge_universal2.sh b/packaging/merge_universal2.sh new file mode 100644 index 0000000..a0092eb --- /dev/null +++ b/packaging/merge_universal2.sh @@ -0,0 +1,64 @@ +#!/bin/bash +# Merge arm64 and x86_64 .app bundles into a universal2 .app and create DMG +set -e + +ARM_APP="$1" +INTEL_APP="$2" +OUT_APP="$3" + +if [ -z "$ARM_APP" ] || [ -z "$INTEL_APP" ] || [ -z "$OUT_APP" ]; then + echo "Usage: $0 " + exit 1 +fi + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +PROJECT_DIR="$(dirname "$SCRIPT_DIR")" +DIST_DIR="$PROJECT_DIR/dist" +APP_NAME="$(basename "$OUT_APP" .app)" +DMG_NAME="TgWsProxy" + +# --- Merge --- + +echo "Merging '$ARM_APP' + '$INTEL_APP' -> '$OUT_APP'" + +rm -rf "$OUT_APP" +cp -R "$ARM_APP" "$OUT_APP" + +find "$OUT_APP" -type f | while read -r file; do + rel="${file#"$OUT_APP"/}" + intel_file="$INTEL_APP/$rel" + + [ -f "$intel_file" ] || continue + + if file "$file" | grep -qE "Mach-O (64-bit )?executable|Mach-O (64-bit )?dynamically linked|Mach-O (64-bit )?bundle"; then + arm_arch=$(lipo -archs "$file" 2>/dev/null || echo "") + intel_arch=$(lipo -archs "$intel_file" 2>/dev/null || echo "") + if [ "$arm_arch" = "$intel_arch" ]; then + # same arch (e.g. local test with two arm64 copies) — skip + continue + fi + lipo -create "$file" "$intel_file" -output "$file" + fi +done + +echo "Merge done: $OUT_APP" + +# --- Create DMG --- + +DMG_TEMP="$DIST_DIR/dmg_temp" +rm -rf "$DMG_TEMP" +mkdir -p "$DMG_TEMP" + +cp -R "$OUT_APP" "$DMG_TEMP/" +ln -s /Applications "$DMG_TEMP/Applications" + +hdiutil create \ + -volname "$APP_NAME" \ + -srcfolder "$DMG_TEMP" \ + -ov \ + -format UDZO \ + "$DIST_DIR/$DMG_NAME.dmg" + +rm -rf "$DMG_TEMP" + +echo "DMG created: $DIST_DIR/$DMG_NAME.dmg" diff --git a/requirements-macos.txt b/requirements-macos.txt new file mode 100644 index 0000000..aa60e3a --- /dev/null +++ b/requirements-macos.txt @@ -0,0 +1,5 @@ +cryptography==46.0.5 +Pillow==12.1.0 +psutil==7.0.0 +rumps==0.4.0 +pyperclip==1.9.0 diff --git a/requirements.txt b/requirements.txt index c7a85f3..ab67f74 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ cryptography==46.0.5 customtkinter==5.2.2 -Pillow==12.1.1 +Pillow==12.1.0 psutil==7.0.0 pystray==0.19.5 pyperclip==1.9.0 From 55affaf78fa1be4ca14e4395bf3b1184592cc66a Mon Sep 17 00:00:00 2001 From: Flowseal Date: Wed, 18 Mar 2026 17:49:24 +0300 Subject: [PATCH 05/22] macos dialog fix; macos merge logs --- macos.py | 9 ++---- packaging/merge_universal2.sh | 58 +++++++++++++++++++++++++++++++++-- 2 files changed, 58 insertions(+), 9 deletions(-) diff --git a/macos.py b/macos.py index fc0b44a..0ed175b 100644 --- a/macos.py +++ b/macos.py @@ -372,17 +372,14 @@ def _osascript_input(prompt: str, default: str, title_esc = title.replace('\\', '\\\\').replace('"', '\\"') r = subprocess.run( ['osascript', '-e', - f'display dialog "{prompt_esc}" ' + f'text returned of (display dialog "{prompt_esc}" ' f'default answer "{default_esc}" ' f'with title "{title_esc}" ' - f'buttons {{"Отмена", "OK"}} default button "OK"'], + f'buttons {{"Отмена", "OK"}} default button "OK")'], capture_output=True, text=True) if r.returncode != 0: return None - for part in r.stdout.strip().split(', '): - if part.startswith('text returned:'): - return part[len('text returned:'):] - return None + return r.stdout.rstrip("\r\n") def _on_edit_config(_=None): diff --git a/packaging/merge_universal2.sh b/packaging/merge_universal2.sh index a0092eb..d5fc4bb 100644 --- a/packaging/merge_universal2.sh +++ b/packaging/merge_universal2.sh @@ -1,6 +1,8 @@ #!/bin/bash # Merge arm64 and x86_64 .app bundles into a universal2 .app and create DMG -set -e +set -euo pipefail + +trap 'echo "merge_universal2.sh failed at line $LINENO" >&2' ERR ARM_APP="$1" INTEL_APP="$2" @@ -17,6 +19,42 @@ DIST_DIR="$PROJECT_DIR/dist" APP_NAME="$(basename "$OUT_APP" .app)" DMG_NAME="TgWsProxy" +echo "Starting universal2 merge" +echo "ARM_APP=$ARM_APP" +echo "INTEL_APP=$INTEL_APP" +echo "OUT_APP=$OUT_APP" +echo "DIST_DIR=$DIST_DIR" + +if [ ! -d "$ARM_APP" ]; then + echo "ARM app bundle not found: $ARM_APP" >&2 + exit 1 +fi + +if [ ! -d "$INTEL_APP" ]; then + echo "Intel app bundle not found: $INTEL_APP" >&2 + exit 1 +fi + +has_arch() { + local arches="$1" + local target="$2" + for arch in $arches; do + if [ "$arch" = "$target" ]; then + return 0 + fi + done + return 1 +} + +is_subset() { + local subset="$1" + local superset="$2" + for arch in $subset; do + has_arch "$superset" "$arch" || return 1 + done + return 0 +} + # --- Merge --- echo "Merging '$ARM_APP' + '$INTEL_APP' -> '$OUT_APP'" @@ -33,11 +71,25 @@ find "$OUT_APP" -type f | while read -r file; do if file "$file" | grep -qE "Mach-O (64-bit )?executable|Mach-O (64-bit )?dynamically linked|Mach-O (64-bit )?bundle"; then arm_arch=$(lipo -archs "$file" 2>/dev/null || echo "") intel_arch=$(lipo -archs "$intel_file" 2>/dev/null || echo "") - if [ "$arm_arch" = "$intel_arch" ]; then - # same arch (e.g. local test with two arm64 copies) — skip + echo "Processing Mach-O: $rel" + echo " arm_arch=$arm_arch" + echo " intel_arch=$intel_arch" + if [ -z "$arm_arch" ] || [ -z "$intel_arch" ]; then + echo " action=skip (unable to determine architecture)" continue fi + if is_subset "$intel_arch" "$arm_arch"; then + echo " action=skip (arm binary already contains intel slices)" + continue + fi + if is_subset "$arm_arch" "$intel_arch"; then + echo " action=copy-intel (intel binary is a superset)" + cp "$intel_file" "$file" + continue + fi + echo " action=lipo-create" lipo -create "$file" "$intel_file" -output "$file" + echo " merged_arch=$(lipo -archs "$file" 2>/dev/null || echo "")" fi done From 053ec3e00fc6d05283614a0aef0da9f113674f73 Mon Sep 17 00:00:00 2001 From: Flowseal Date: Wed, 18 Mar 2026 18:11:07 +0300 Subject: [PATCH 06/22] Universal2 macos test --- .github/workflows/build.yml | 161 ++++++++++++++++++++---------------- packaging/macos.spec | 2 +- 2 files changed, 92 insertions(+), 71 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d804c69..3bbcc65 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -81,90 +81,111 @@ jobs: - name: Checkout uses: actions/checkout@v4 - - name: Setup Python - uses: actions/setup-python@v5 - with: - python-version: "3.12" - cache: "pip" + - name: Install universal2 Python + run: | + set -euo pipefail + curl -LO https://www.python.org/ftp/python/3.12.10/python-3.12.10-macos11.pkg + sudo installer -pkg python-3.12.10-macos11.pkg -target / + echo "/Library/Frameworks/Python.framework/Versions/3.12/bin" >> "$GITHUB_PATH" - name: Install dependencies - run: pip install -r requirements-macos.txt + run: | + set -euo pipefail + python3.12 -m pip install --upgrade pip setuptools wheel + python3.12 -m pip install delocate==0.13.0 - - name: Install pyinstaller - run: pip install pyinstaller + mkdir -p wheelhouse/arm64 wheelhouse/x86_64 wheelhouse/universal2 + + python3.12 -m pip download \ + --only-binary=:all: \ + --platform macosx_11_0_arm64 \ + --python-version 3.12 \ + --implementation cp \ + -d wheelhouse/arm64 \ + Pillow==12.1.0 \ + psutil==7.0.0 + + python3.12 -m pip download \ + --only-binary=:all: \ + --platform macosx_10_13_x86_64 \ + --python-version 3.12 \ + --implementation cp \ + -d wheelhouse/x86_64 \ + Pillow==12.1.0 + + python3.12 -m pip download \ + --only-binary=:all: \ + --platform macosx_10_9_x86_64 \ + --python-version 3.12 \ + --implementation cp \ + -d wheelhouse/x86_64 \ + psutil==7.0.0 + + delocate-merge \ + wheelhouse/arm64/pillow-12.1.0-*.whl \ + wheelhouse/x86_64/pillow-12.1.0-*.whl \ + -w wheelhouse/universal2 + + delocate-merge \ + wheelhouse/arm64/psutil-7.0.0-*.whl \ + wheelhouse/x86_64/psutil-7.0.0-*.whl \ + -w wheelhouse/universal2 + + python3.12 -m pip install --no-deps wheelhouse/universal2/*.whl + python3.12 -m pip install -r requirements-macos.txt + python3.12 -m pip install pyinstaller==6.13.0 - name: Create macOS icon from ICO run: | + set -euo pipefail chmod +x packaging/create_icon.sh packaging/create_icon.sh - name: Build app with PyInstaller - run: pyinstaller packaging/macos.spec --noconfirm + run: python3.12 -m PyInstaller packaging/macos.spec --noconfirm - - name: Upload app bundle - uses: actions/upload-artifact@v4 - with: - name: TgWsProxy-app-arm64 - path: "dist/TG WS Proxy.app" - - build-macos-intel: - runs-on: macos-15-intel - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Setup Python - uses: actions/setup-python@v5 - with: - python-version: "3.12" - cache: "pip" - - - name: Install dependencies - run: pip install -r requirements-macos.txt - - - name: Install pyinstaller - run: pip install pyinstaller - - - name: Create macOS icon from ICO + - name: Validate universal2 app bundle run: | - chmod +x packaging/create_icon.sh - packaging/create_icon.sh + set -euo pipefail + found=0 + while IFS= read -r -d '' file; do + if file "$file" | grep -q "Mach-O"; then + found=1 + archs="$(lipo -archs "$file" 2>/dev/null || true)" + case "$archs" in + *arm64*x86_64*|*x86_64*arm64*) ;; + *) + echo "Missing universal2 slices in $file: ${archs:-unknown}" >&2 + exit 1 + ;; + esac + fi + done < <(find "dist/TG WS Proxy.app" -type f -print0) - - name: Build app with PyInstaller - run: pyinstaller packaging/macos.spec --noconfirm + if [ "$found" -eq 0 ]; then + echo "No Mach-O files found in app bundle" >&2 + exit 1 + fi - - name: Upload app bundle - uses: actions/upload-artifact@v4 - with: - name: TgWsProxy-app-x86_64 - path: "dist/TG WS Proxy.app" - - build-macos-universal: - needs: [build-macos, build-macos-intel] - runs-on: macos-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Download arm64 app - uses: actions/download-artifact@v4 - with: - name: TgWsProxy-app-arm64 - path: "dist/arm64/TG WS Proxy.app" - - - name: Download x86_64 app - uses: actions/download-artifact@v4 - with: - name: TgWsProxy-app-x86_64 - path: "dist/x86_64/TG WS Proxy.app" - - - name: Merge into universal2 app and create DMG + - name: Create DMG run: | - chmod +x packaging/merge_universal2.sh - packaging/merge_universal2.sh \ - "dist/arm64/TG WS Proxy.app" \ - "dist/x86_64/TG WS Proxy.app" \ - "dist/TG WS Proxy.app" + set -euo pipefail + APP_NAME="TG WS Proxy" + DMG_TEMP="dist/dmg_temp" + + rm -rf "$DMG_TEMP" + mkdir -p "$DMG_TEMP" + cp -R "dist/${APP_NAME}.app" "$DMG_TEMP/" + ln -s /Applications "$DMG_TEMP/Applications" + + hdiutil create \ + -volname "$APP_NAME" \ + -srcfolder "$DMG_TEMP" \ + -ov \ + -format UDZO \ + "dist/TgWsProxy.dmg" + + rm -rf "$DMG_TEMP" - name: Upload artifact uses: actions/upload-artifact@v4 @@ -173,7 +194,7 @@ jobs: path: dist/TgWsProxy.dmg release: - needs: [build, build-win7, build-macos-universal] + needs: [build, build-win7, build-macos] runs-on: ubuntu-latest if: ${{ github.event.inputs.make_release == 'true' }} steps: diff --git a/packaging/macos.spec b/packaging/macos.spec index 065461e..5f38945 100644 --- a/packaging/macos.spec +++ b/packaging/macos.spec @@ -48,7 +48,7 @@ exe = EXE( upx=False, console=False, argv_emulation=False, - target_arch=None, + target_arch='universal2', codesign_identity=None, entitlements_file=None, ) From d2190cfec616513e55df4f03a45819a5e21c9b16 Mon Sep 17 00:00:00 2001 From: Flowseal Date: Wed, 18 Mar 2026 18:15:06 +0300 Subject: [PATCH 07/22] cffi universal2 fix --- .github/workflows/build.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3bbcc65..98a6861 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -102,6 +102,7 @@ jobs: --python-version 3.12 \ --implementation cp \ -d wheelhouse/arm64 \ + 'cffi>=2.0.0' \ Pillow==12.1.0 \ psutil==7.0.0 @@ -111,6 +112,7 @@ jobs: --python-version 3.12 \ --implementation cp \ -d wheelhouse/x86_64 \ + 'cffi>=2.0.0' \ Pillow==12.1.0 python3.12 -m pip download \ @@ -121,6 +123,11 @@ jobs: -d wheelhouse/x86_64 \ psutil==7.0.0 + delocate-merge \ + wheelhouse/arm64/cffi-*.whl \ + wheelhouse/x86_64/cffi-*.whl \ + -w wheelhouse/universal2 + delocate-merge \ wheelhouse/arm64/pillow-12.1.0-*.whl \ wheelhouse/x86_64/pillow-12.1.0-*.whl \ From 7572258a282007d5b869f507dcdaa4a057f811f8 Mon Sep 17 00:00:00 2001 From: Flowseal Date: Wed, 18 Mar 2026 19:22:46 +0300 Subject: [PATCH 08/22] MacOS build simplify, readme update --- .github/workflows/build.yml | 23 ++++++- README.md | 63 ++---------------- packaging/create_icon.sh | 30 --------- packaging/merge_universal2.sh | 116 ---------------------------------- 4 files changed, 26 insertions(+), 206 deletions(-) delete mode 100644 packaging/create_icon.sh delete mode 100644 packaging/merge_universal2.sh diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 98a6861..ac716ee 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -145,8 +145,27 @@ jobs: - name: Create macOS icon from ICO run: | set -euo pipefail - chmod +x packaging/create_icon.sh - packaging/create_icon.sh + python3.12 - <<'PY' + from PIL import Image + + image = Image.open('icon.ico') + image = image.resize((1024, 1024), Image.LANCZOS) + image.save('icon_1024.png', 'PNG') + PY + + mkdir -p icon.iconset + sips -z 16 16 icon_1024.png --out icon.iconset/icon_16x16.png + sips -z 32 32 icon_1024.png --out icon.iconset/icon_16x16@2x.png + sips -z 32 32 icon_1024.png --out icon.iconset/icon_32x32.png + sips -z 64 64 icon_1024.png --out icon.iconset/icon_32x32@2x.png + sips -z 128 128 icon_1024.png --out icon.iconset/icon_128x128.png + sips -z 256 256 icon_1024.png --out icon.iconset/icon_128x128@2x.png + sips -z 256 256 icon_1024.png --out icon.iconset/icon_256x256.png + sips -z 512 512 icon_1024.png --out icon.iconset/icon_256x256@2x.png + sips -z 512 512 icon_1024.png --out icon.iconset/icon_512x512.png + sips -z 1024 1024 icon_1024.png --out icon.iconset/icon_512x512@2x.png + iconutil -c icns icon.iconset -o icon.icns + rm -rf icon.iconset icon_1024.png - name: Build app with PyInstaller run: python3.12 -m PyInstaller packaging/macos.spec --noconfirm diff --git a/README.md b/README.md index 8cefc59..68271c4 100644 --- a/README.md +++ b/README.md @@ -10,22 +10,20 @@ # TG WS Proxy -Локальный SOCKS5-прокси для Telegram Desktop, который перенаправляет трафик через WebSocket-соединения к указанным серверам, помогая частично ускорить работу Telegram. - -**Ожидаемый результат аналогичен прокидыванию hosts для Web Telegram**: ускорение загрузки и скачивания файлов, загрузки сообщений и части медиа. +**Локальный SOCKS5-прокси** для Telegram Desktop, который **ускоряет работу Telegram**, перенаправляя трафик через WebSocket-соединения. Данные передаются в том же зашифрованном виде, а для работы не нужны сторонние сервера. image ## Как это работает ``` -Telegram Desktop → SOCKS5 (127.0.0.1:1080) → TG WS Proxy → WSS (kws*.web.telegram.org) → Telegram DC +Telegram Desktop → SOCKS5 (127.0.0.1:1080) → TG WS Proxy → WSS → Telegram DC ``` 1. Приложение поднимает локальный SOCKS5-прокси на `127.0.0.1:1080` 2. Перехватывает подключения к IP-адресам Telegram 3. Извлекает DC ID из MTProto obfuscation init-пакета -4. Устанавливает WebSocket (TLS) соединение к соответствующему DC через домены `kws{N}.web.telegram.org` +4. Устанавливает WebSocket (TLS) соединение к соответствующему DC через домены Telegram 5. Если WS недоступен (302 redirect) — автоматически переключается на прямое TCP-соединение ## 🚀 Быстрый старт @@ -42,25 +40,6 @@ Telegram Desktop → SOCKS5 (127.0.0.1:1080) → TG WS Proxy → WSS (kws*.web.t - **Открыть логи** — открыть файл логов - **Выход** — остановить прокси и закрыть приложение -## Установка из исходников - -> Для Windows: - -```bash -pip install -r requirements.txt -``` - -> Для MacOS: -```bash -pip install -r requirements-macos.txt -``` - -### Windows (Tray-приложение) - -```bash -python windows.py -``` - ### MacOS (Tray-приложение) Перейдите на [страницу релизов](https://github.com/Flowseal/tg-ws-proxy/releases) и скачайте **`TgWsProxy.dmg`** — универсальная сборка для Apple Silicon и Intel. @@ -69,7 +48,7 @@ python windows.py 2. Перенести **TG WS Proxy.app** в папку **Applications** 3. При первом запуске macOS может попросить подтвердить открытие: **Системные настройки → Конфиденциальность и безопасность → Всё равно открыть** -### Консольный режим (Windows) +### Консольный режим из исходников ```bash python proxy/tg_ws_proxy.py [--port PORT] [--dc-ip DC:IP ...] [-v] @@ -96,11 +75,6 @@ python proxy/tg_ws_proxy.py --port 9050 --dc-ip 1:149.154.175.205 --dc-ip 2:149. python proxy/tg_ws_proxy.py -v ``` -### Консольный режим (MacOS) -```bash -python macos.py -``` - ## Настройка Telegram Desktop ### Автоматически @@ -133,34 +107,7 @@ Tray-приложение хранит данные в `%APPDATA%/TgWsProxy`: ## Автоматическая сборка -Проект содержит спецификацию PyInstaller ([`windows.spec`](packaging/windows.spec)) и GitHub Actions workflow ([`.github/workflows/build.yml`](.github/workflows/build.yml)) для автоматической сборки. - -> Для Windows: - -```bash -pip install pyinstaller -pyinstaller packaging/windows.spec -``` - -> Для MacOS: -```bash -pip install pyinstaller -packaging/create_icon.sh -pyinstaller packaging/macos.spec -``` - -> Создать универсальный DMG (local-тест с двумя копями одной архитектуры): -```bash -pip install pyinstaller -packaging/create_icon.sh -pyinstaller packaging/macos.spec --noconfirm -cp -R "dist/TG WS Proxy.app" "dist/TG WS Proxy-intel.app" -packaging/merge_universal2.sh \ - "dist/TG WS Proxy.app" \ - "dist/TG WS Proxy-intel.app" \ - "dist/TG WS Proxy-universal.app" -``` -- В результате в папке `dist/` появятся `.app` под текущую архитектуру и `TgWsProxy.dmg`. +Проект содержит спецификации PyInstaller ([`packaging/windows.spec`](packaging/windows.spec), [`packaging/macos.spec`](packaging/macos.spec)) и GitHub Actions workflow ([`.github/workflows/build.yml`](.github/workflows/build.yml)) для автоматической сборки. ## Лицензия diff --git a/packaging/create_icon.sh b/packaging/create_icon.sh deleted file mode 100644 index e222e7a..0000000 --- a/packaging/create_icon.sh +++ /dev/null @@ -1,30 +0,0 @@ -#!/bin/bash -# Create icon.icns from icon.ico for macOS app -set -e - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -PROJECT_DIR="$(dirname "$SCRIPT_DIR")" - -python3 -c " -from PIL import Image -img = Image.open('$PROJECT_DIR/icon.ico') -img = img.resize((1024, 1024), Image.LANCZOS) -img.save('$PROJECT_DIR/icon_1024.png', 'PNG') -" - -mkdir -p "$PROJECT_DIR/icon.iconset" -sips -z 16 16 "$PROJECT_DIR/icon_1024.png" --out "$PROJECT_DIR/icon.iconset/icon_16x16.png" -sips -z 32 32 "$PROJECT_DIR/icon_1024.png" --out "$PROJECT_DIR/icon.iconset/icon_16x16@2x.png" -sips -z 32 32 "$PROJECT_DIR/icon_1024.png" --out "$PROJECT_DIR/icon.iconset/icon_32x32.png" -sips -z 64 64 "$PROJECT_DIR/icon_1024.png" --out "$PROJECT_DIR/icon.iconset/icon_32x32@2x.png" -sips -z 128 128 "$PROJECT_DIR/icon_1024.png" --out "$PROJECT_DIR/icon.iconset/icon_128x128.png" -sips -z 256 256 "$PROJECT_DIR/icon_1024.png" --out "$PROJECT_DIR/icon.iconset/icon_128x128@2x.png" -sips -z 256 256 "$PROJECT_DIR/icon_1024.png" --out "$PROJECT_DIR/icon.iconset/icon_256x256.png" -sips -z 512 512 "$PROJECT_DIR/icon_1024.png" --out "$PROJECT_DIR/icon.iconset/icon_256x256@2x.png" -sips -z 512 512 "$PROJECT_DIR/icon_1024.png" --out "$PROJECT_DIR/icon.iconset/icon_512x512.png" -sips -z 1024 1024 "$PROJECT_DIR/icon_1024.png" --out "$PROJECT_DIR/icon.iconset/icon_512x512@2x.png" -iconutil -c icns "$PROJECT_DIR/icon.iconset" -o "$PROJECT_DIR/icon.icns" - -rm -rf "$PROJECT_DIR/icon.iconset" "$PROJECT_DIR/icon_1024.png" - -echo "icon.icns created: $PROJECT_DIR/icon.icns" diff --git a/packaging/merge_universal2.sh b/packaging/merge_universal2.sh deleted file mode 100644 index d5fc4bb..0000000 --- a/packaging/merge_universal2.sh +++ /dev/null @@ -1,116 +0,0 @@ -#!/bin/bash -# Merge arm64 and x86_64 .app bundles into a universal2 .app and create DMG -set -euo pipefail - -trap 'echo "merge_universal2.sh failed at line $LINENO" >&2' ERR - -ARM_APP="$1" -INTEL_APP="$2" -OUT_APP="$3" - -if [ -z "$ARM_APP" ] || [ -z "$INTEL_APP" ] || [ -z "$OUT_APP" ]; then - echo "Usage: $0 " - exit 1 -fi - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -PROJECT_DIR="$(dirname "$SCRIPT_DIR")" -DIST_DIR="$PROJECT_DIR/dist" -APP_NAME="$(basename "$OUT_APP" .app)" -DMG_NAME="TgWsProxy" - -echo "Starting universal2 merge" -echo "ARM_APP=$ARM_APP" -echo "INTEL_APP=$INTEL_APP" -echo "OUT_APP=$OUT_APP" -echo "DIST_DIR=$DIST_DIR" - -if [ ! -d "$ARM_APP" ]; then - echo "ARM app bundle not found: $ARM_APP" >&2 - exit 1 -fi - -if [ ! -d "$INTEL_APP" ]; then - echo "Intel app bundle not found: $INTEL_APP" >&2 - exit 1 -fi - -has_arch() { - local arches="$1" - local target="$2" - for arch in $arches; do - if [ "$arch" = "$target" ]; then - return 0 - fi - done - return 1 -} - -is_subset() { - local subset="$1" - local superset="$2" - for arch in $subset; do - has_arch "$superset" "$arch" || return 1 - done - return 0 -} - -# --- Merge --- - -echo "Merging '$ARM_APP' + '$INTEL_APP' -> '$OUT_APP'" - -rm -rf "$OUT_APP" -cp -R "$ARM_APP" "$OUT_APP" - -find "$OUT_APP" -type f | while read -r file; do - rel="${file#"$OUT_APP"/}" - intel_file="$INTEL_APP/$rel" - - [ -f "$intel_file" ] || continue - - if file "$file" | grep -qE "Mach-O (64-bit )?executable|Mach-O (64-bit )?dynamically linked|Mach-O (64-bit )?bundle"; then - arm_arch=$(lipo -archs "$file" 2>/dev/null || echo "") - intel_arch=$(lipo -archs "$intel_file" 2>/dev/null || echo "") - echo "Processing Mach-O: $rel" - echo " arm_arch=$arm_arch" - echo " intel_arch=$intel_arch" - if [ -z "$arm_arch" ] || [ -z "$intel_arch" ]; then - echo " action=skip (unable to determine architecture)" - continue - fi - if is_subset "$intel_arch" "$arm_arch"; then - echo " action=skip (arm binary already contains intel slices)" - continue - fi - if is_subset "$arm_arch" "$intel_arch"; then - echo " action=copy-intel (intel binary is a superset)" - cp "$intel_file" "$file" - continue - fi - echo " action=lipo-create" - lipo -create "$file" "$intel_file" -output "$file" - echo " merged_arch=$(lipo -archs "$file" 2>/dev/null || echo "")" - fi -done - -echo "Merge done: $OUT_APP" - -# --- Create DMG --- - -DMG_TEMP="$DIST_DIR/dmg_temp" -rm -rf "$DMG_TEMP" -mkdir -p "$DMG_TEMP" - -cp -R "$OUT_APP" "$DMG_TEMP/" -ln -s /Applications "$DMG_TEMP/Applications" - -hdiutil create \ - -volname "$APP_NAME" \ - -srcfolder "$DMG_TEMP" \ - -ov \ - -format UDZO \ - "$DIST_DIR/$DMG_NAME.dmg" - -rm -rf "$DMG_TEMP" - -echo "DMG created: $DIST_DIR/$DMG_NAME.dmg" From 9924440c489b5d0ff939a0e4f9c92687461665b6 Mon Sep 17 00:00:00 2001 From: "kek.of" Date: Wed, 18 Mar 2026 22:27:16 +0500 Subject: [PATCH 09/22] Update macos.py (#272) --- macos.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/macos.py b/macos.py index 0ed175b..e1806cf 100644 --- a/macos.py +++ b/macos.py @@ -552,7 +552,7 @@ class TgWsProxyApp(_TgWsProxyAppBase): super().__init__( "TG WS Proxy", icon=icon_path, - template=True, + template=False, quit_button="Выход", menu=[ self._open_tg_item, From 99b5c722e1eccaba25c49f8be86e0bc2887c0cf5 Mon Sep 17 00:00:00 2001 From: delewer <108271242+IMDelewer@users.noreply.github.com> Date: Thu, 19 Mar 2026 01:33:12 +0700 Subject: [PATCH 10/22] build: migrate deps to pyproject.toml (#201) --- .github/workflows/build.yml | 4 +-- README.md | 39 ++++++++++++++++++--- proxy/__init__.py | 1 + pyproject.toml | 69 +++++++++++++++++++++++++++++++++++++ requirements-win7.txt | 7 +--- requirements.txt | 4 +++ windows.py | 5 +-- 7 files changed, 115 insertions(+), 14 deletions(-) create mode 100644 proxy/__init__.py create mode 100644 pyproject.toml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ac716ee..4bf2274 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -30,7 +30,7 @@ jobs: cache: "pip" - name: Install dependencies - run: pip install -r requirements.txt + run: pip install ".[win10]" - name: Install pyinstaller run: pip install "pyinstaller==6.13.0" @@ -58,7 +58,7 @@ jobs: cache: "pip" - name: Install dependencies (Win7-compatible) - run: pip install -r requirements-win7.txt + run: pip install ".[win7]" - name: Install pyinstaller run: pip install "pyinstaller==5.13.2" diff --git a/README.md b/README.md index 68271c4..94562ce 100644 --- a/README.md +++ b/README.md @@ -42,16 +42,34 @@ Telegram Desktop → SOCKS5 (127.0.0.1:1080) → TG WS Proxy → WSS → Telegra ### MacOS (Tray-приложение) +<<<<<<< build/pyproject-migration +```bash +pip install -e ".[win10]" +``` + +### Windows 7 + +```bash +pip install -e ".[win7]" +``` + +### Windows (Tray-приложение) + +```bash +tg-ws-proxy-tray +``` +======= Перейдите на [страницу релизов](https://github.com/Flowseal/tg-ws-proxy/releases) и скачайте **`TgWsProxy.dmg`** — универсальная сборка для Apple Silicon и Intel. 1. Открыть образ 2. Перенести **TG WS Proxy.app** в папку **Applications** 3. При первом запуске macOS может попросить подтвердить открытие: **Системные настройки → Конфиденциальность и безопасность → Всё равно открыть** +>>>>>>> main ### Консольный режим из исходников ```bash -python proxy/tg_ws_proxy.py [--port PORT] [--dc-ip DC:IP ...] [-v] +tg-ws-proxy [--port PORT] [--host HOST] [--dc-ip DC:IP ...] [-v] ``` **Аргументы:** @@ -59,6 +77,7 @@ python proxy/tg_ws_proxy.py [--port PORT] [--dc-ip DC:IP ...] [-v] | Аргумент | По умолчанию | Описание | |---|---|---| | `--port` | `1080` | Порт SOCKS5-прокси | +| `--host` | `127.0.0.1` | Хост SOCKS5-прокси | | `--dc-ip` | `2:149.154.167.220`, `4:149.154.167.220` | Целевой IP для DC (можно указать несколько раз) | | `-v`, `--verbose` | выкл. | Подробное логирование (DEBUG) | @@ -66,13 +85,25 @@ python proxy/tg_ws_proxy.py [--port PORT] [--dc-ip DC:IP ...] [-v] ```bash # Стандартный запуск -python proxy/tg_ws_proxy.py +tg-ws-proxy # Другой порт и дополнительные DC -python proxy/tg_ws_proxy.py --port 9050 --dc-ip 1:149.154.175.205 --dc-ip 2:149.154.167.220 +tg-ws-proxy --port 9050 --dc-ip 1:149.154.175.205 --dc-ip 2:149.154.167.220 # С подробным логированием -python proxy/tg_ws_proxy.py -v +tg-ws-proxy -v +``` + +## CLI-скрипты (pyproject.toml) + +CLI команды объявляются в `pyproject.toml` в секции `[project.scripts]` и должны указывать на `module:function`. + +Пример: + +```toml +[project.scripts] +tg-ws-proxy = "proxy.tg_ws_proxy:main" +tg-ws-proxy-tray = "windows:main" ``` ## Настройка Telegram Desktop diff --git a/proxy/__init__.py b/proxy/__init__.py new file mode 100644 index 0000000..f74f503 --- /dev/null +++ b/proxy/__init__.py @@ -0,0 +1 @@ +__version__ = "1.1.3" \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..ab2f8c0 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,69 @@ +[build-system] +requires = ["hatchling>=1.25.0"] +build-backend = "hatchling.build" + +[project] +name = "tg-ws-proxy" +dynamic=["version"] + +description = "Telegram Desktop WebSocket Bridge Proxy" +readme = "README.md" +requires-python = ">=3.8" + +license = { name = "MIT", file = "LICENSE" } + +authors = [ + { name = "Flowseal" } +] + +keywords = [ + "telegram", + "proxy", + "websocket" +] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Environment :: Console", + "Environment :: Win32 (MS Windows)", + "Intended Audience :: Customer Service", + "Programming Language :: Python :: 3", + "License :: OSI Approved :: MIT License", + "Operating System :: Microsoft :: Windows", + "Topic :: System :: Networking :: Firewalls", +] + +dependencies = [ + "customtkinter==5.2.2", + "pystray==0.19.5", + "pyperclip==1.9.0", +] + +[project.optional-dependencies] +win7 = [ + "cryptography==41.0.7", + "Pillow==10.4.0", + "psutil==5.9.8", +] + +win10 = [ + "cryptography==46.0.5", + "Pillow==12.1.1", + "psutil==7.0.0", +] + +[project.scripts] +tg-ws-proxy = "proxy.tg_ws_proxy:main" +tg-ws-proxy-tray = "windows:main" + +[project.urls] +Source = "https://github.com/Flowseal/tg-ws-proxy" +Issues = "https://github.com/Flowseal/tg-ws-proxy/issues" + +[tool.hatch.build.targets.wheel] +packages = ["proxy"] + +[tool.hatch.build.force-include] +"windows.py" = "windows.py" + +[tool.hatch.version] +path = "proxy/__init__.py" diff --git a/requirements-win7.txt b/requirements-win7.txt index 41a8174..f73b306 100644 --- a/requirements-win7.txt +++ b/requirements-win7.txt @@ -1,6 +1 @@ -cryptography==41.0.7 -customtkinter==5.2.2 -Pillow==10.4.0 -psutil==5.9.8 -pystray==0.19.5 -pyperclip==1.9.0 +-e .[win7] diff --git a/requirements.txt b/requirements.txt index ab67f74..e4ecb10 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,10 @@ +<<<<<<< build/pyproject-migration +-e .[win10] +======= cryptography==46.0.5 customtkinter==5.2.2 Pillow==12.1.0 psutil==7.0.0 pystray==0.19.5 pyperclip==1.9.0 +>>>>>>> main diff --git a/windows.py b/windows.py index 8e8ed5d..94ea7b0 100644 --- a/windows.py +++ b/windows.py @@ -9,12 +9,13 @@ import sys import threading import time import webbrowser -import pystray import pyperclip import asyncio as _asyncio -import customtkinter as ctk from pathlib import Path from typing import Dict, Optional + +import pystray +import customtkinter as ctk from PIL import Image, ImageDraw, ImageFont import proxy.tg_ws_proxy as tg_ws_proxy From 54c6f3881bca802f1473b6b2309e66b7e893a83e Mon Sep 17 00:00:00 2001 From: Flowseal Date: Wed, 18 Mar 2026 21:54:58 +0300 Subject: [PATCH 11/22] pyproject fixes; macos support --- README.md | 62 ++++++++++++++++++++++++++++++++++-------- pyproject.toml | 26 ++++++++++++++---- requirements-macos.txt | 6 +--- requirements.txt | 9 ------ 4 files changed, 72 insertions(+), 31 deletions(-) diff --git a/README.md b/README.md index 94562ce..e77f4a2 100644 --- a/README.md +++ b/README.md @@ -40,31 +40,64 @@ Telegram Desktop → SOCKS5 (127.0.0.1:1080) → TG WS Proxy → WSS → Telegra - **Открыть логи** — открыть файл логов - **Выход** — остановить прокси и закрыть приложение -### MacOS (Tray-приложение) +### macOS +Перейдите на [страницу релизов](https://github.com/Flowseal/tg-ws-proxy/releases) и скачайте **`TgWsProxy.dmg`** — универсальная сборка для Apple Silicon и Intel. + +1. Открыть образ +2. Перенести **TG WS Proxy.app** в папку **Applications** +3. При первом запуске macOS может попросить подтвердить открытие: **Системные настройки → Конфиденциальность и безопасность → Всё равно открыть** + +## Установка из исходников + +### Консольный proxy + +Для запуска только SOCKS5/WebSocket proxy без tray-интерфейса достаточно базовой установки: + +```bash +pip install -e . +``` + +После этого доступна команда: + +```bash +tg-ws-proxy +``` + +### Windows 10+ -<<<<<<< build/pyproject-migration ```bash pip install -e ".[win10]" ``` +Запуск tray-приложения: + +```bash +tg-ws-proxy-tray-win +``` + ### Windows 7 ```bash pip install -e ".[win7]" ``` -### Windows (Tray-приложение) +Запуск tray-приложения: ```bash -tg-ws-proxy-tray +tg-ws-proxy-tray-win ``` -======= -Перейдите на [страницу релизов](https://github.com/Flowseal/tg-ws-proxy/releases) и скачайте **`TgWsProxy.dmg`** — универсальная сборка для Apple Silicon и Intel. -1. Открыть образ -2. Перенести **TG WS Proxy.app** в папку **Applications** -3. При первом запуске macOS может попросить подтвердить открытие: **Системные настройки → Конфиденциальность и безопасность → Всё равно открыть** ->>>>>>> main +### macOS + +```bash +pip install -e ".[macos]" +``` + +Запуск tray-приложения: + +```bash +tg-ws-proxy-tray-macos +``` ### Консольный режим из исходников @@ -103,7 +136,8 @@ CLI команды объявляются в `pyproject.toml` в секции `[ ```toml [project.scripts] tg-ws-proxy = "proxy.tg_ws_proxy:main" -tg-ws-proxy-tray = "windows:main" +tg-ws-proxy-tray-win = "windows:main" +tg-ws-proxy-tray-macos = "macos:main" ``` ## Настройка Telegram Desktop @@ -140,6 +174,12 @@ Tray-приложение хранит данные в `%APPDATA%/TgWsProxy`: Проект содержит спецификации PyInstaller ([`packaging/windows.spec`](packaging/windows.spec), [`packaging/macos.spec`](packaging/macos.spec)) и GitHub Actions workflow ([`.github/workflows/build.yml`](.github/workflows/build.yml)) для автоматической сборки. +Минимально поддерживаемые версии ОС для текущих бинарных сборок: +- Windows 10+ для `TgWsProxy.exe` +- Windows 7 для `TgWsProxy-win7.exe` +- Intel macOS 10.15+ +- Apple Silicon macOS 11.0+ + ## Лицензия [MIT License](LICENSE) diff --git a/pyproject.toml b/pyproject.toml index ab2f8c0..fab74bc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,36 +24,49 @@ keywords = [ classifiers = [ "Development Status :: 5 - Production/Stable", "Environment :: Console", + "Environment :: MacOS X :: Cocoa", "Environment :: Win32 (MS Windows)", "Intended Audience :: Customer Service", "Programming Language :: Python :: 3", "License :: OSI Approved :: MIT License", + "Operating System :: MacOS :: MacOS X", "Operating System :: Microsoft :: Windows", "Topic :: System :: Networking :: Firewalls", ] dependencies = [ - "customtkinter==5.2.2", - "pystray==0.19.5", - "pyperclip==1.9.0", + "cryptography==41.0.7; platform_system == 'Windows' and python_version < '3.9'", + "cryptography==46.0.5; platform_system != 'Windows' or python_version >= '3.9'", ] [project.optional-dependencies] win7 = [ - "cryptography==41.0.7", + "customtkinter==5.2.2", "Pillow==10.4.0", "psutil==5.9.8", + "pystray==0.19.5", + "pyperclip==1.9.0", ] win10 = [ - "cryptography==46.0.5", + "customtkinter==5.2.2", "Pillow==12.1.1", "psutil==7.0.0", + "pystray==0.19.5", + "pyperclip==1.9.0", +] + +macos = [ + "Pillow==12.1.0", + "psutil==7.0.0", + "pyperclip==1.9.0", + "rumps==0.4.0", ] [project.scripts] tg-ws-proxy = "proxy.tg_ws_proxy:main" -tg-ws-proxy-tray = "windows:main" +tg-ws-proxy-tray-win = "windows:main" +tg-ws-proxy-tray-macos = "macos:main" [project.urls] Source = "https://github.com/Flowseal/tg-ws-proxy" @@ -64,6 +77,7 @@ packages = ["proxy"] [tool.hatch.build.force-include] "windows.py" = "windows.py" +"macos.py" = "macos.py" [tool.hatch.version] path = "proxy/__init__.py" diff --git a/requirements-macos.txt b/requirements-macos.txt index aa60e3a..b5937da 100644 --- a/requirements-macos.txt +++ b/requirements-macos.txt @@ -1,5 +1 @@ -cryptography==46.0.5 -Pillow==12.1.0 -psutil==7.0.0 -rumps==0.4.0 -pyperclip==1.9.0 +-e .[macos] diff --git a/requirements.txt b/requirements.txt index e4ecb10..baf343f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,10 +1 @@ -<<<<<<< build/pyproject-migration -e .[win10] -======= -cryptography==46.0.5 -customtkinter==5.2.2 -Pillow==12.1.0 -psutil==7.0.0 -pystray==0.19.5 -pyperclip==1.9.0 ->>>>>>> main From 6b9ddda7f0476321b8a41755c886ba9695718d91 Mon Sep 17 00:00:00 2001 From: Flowseal Date: Wed, 18 Mar 2026 21:58:35 +0300 Subject: [PATCH 12/22] readme simplify --- README.md | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/README.md b/README.md index e77f4a2..e9cd12a 100644 --- a/README.md +++ b/README.md @@ -55,11 +55,6 @@ Telegram Desktop → SOCKS5 (127.0.0.1:1080) → TG WS Proxy → WSS → Telegra ```bash pip install -e . -``` - -После этого доступна команда: - -```bash tg-ws-proxy ``` @@ -67,11 +62,6 @@ tg-ws-proxy ```bash pip install -e ".[win10]" -``` - -Запуск tray-приложения: - -```bash tg-ws-proxy-tray-win ``` @@ -79,11 +69,6 @@ tg-ws-proxy-tray-win ```bash pip install -e ".[win7]" -``` - -Запуск tray-приложения: - -```bash tg-ws-proxy-tray-win ``` @@ -91,11 +76,6 @@ tg-ws-proxy-tray-win ```bash pip install -e ".[macos]" -``` - -Запуск tray-приложения: - -```bash tg-ws-proxy-tray-macos ``` From 51aca9009fdae3e8c6363600cc5c316e2a694b1a Mon Sep 17 00:00:00 2001 From: Flowseal Date: Wed, 18 Mar 2026 22:03:57 +0300 Subject: [PATCH 13/22] removed req files --- .github/workflows/build.yml | 2 +- requirements-macos.txt | 1 - requirements-win7.txt | 1 - requirements.txt | 1 - 4 files changed, 1 insertion(+), 4 deletions(-) delete mode 100644 requirements-macos.txt delete mode 100644 requirements-win7.txt delete mode 100644 requirements.txt diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4bf2274..6b2d389 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -139,7 +139,7 @@ jobs: -w wheelhouse/universal2 python3.12 -m pip install --no-deps wheelhouse/universal2/*.whl - python3.12 -m pip install -r requirements-macos.txt + python3.12 -m pip install ".[macos]" python3.12 -m pip install pyinstaller==6.13.0 - name: Create macOS icon from ICO diff --git a/requirements-macos.txt b/requirements-macos.txt deleted file mode 100644 index b5937da..0000000 --- a/requirements-macos.txt +++ /dev/null @@ -1 +0,0 @@ --e .[macos] diff --git a/requirements-win7.txt b/requirements-win7.txt deleted file mode 100644 index f73b306..0000000 --- a/requirements-win7.txt +++ /dev/null @@ -1 +0,0 @@ --e .[win7] diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index baf343f..0000000 --- a/requirements.txt +++ /dev/null @@ -1 +0,0 @@ --e .[win10] From 646468680cd700af2ab766f6f0bb7a5d23c77ed6 Mon Sep 17 00:00:00 2001 From: Flowseal Date: Thu, 19 Mar 2026 02:36:17 +0300 Subject: [PATCH 14/22] Speed improvements --- proxy/tg_ws_proxy.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/proxy/tg_ws_proxy.py b/proxy/tg_ws_proxy.py index 7912fd8..6647cd6 100644 --- a/proxy/tg_ws_proxy.py +++ b/proxy/tg_ws_proxy.py @@ -18,8 +18,8 @@ DEFAULT_PORT = 1080 log = logging.getLogger('tg-ws-proxy') _TCP_NODELAY = True -_RECV_BUF = 65536 -_SEND_BUF = 65536 +_RECV_BUF = 256 * 1024 +_SEND_BUF = 256 * 1024 _WS_POOL_SIZE = 4 _WS_POOL_MAX_AGE = 120.0 From 96383057c6fe20bad3c4f61a3aa6e2fddb8848dd Mon Sep 17 00:00:00 2001 From: Flowseal Date: Thu, 19 Mar 2026 05:42:40 +0300 Subject: [PATCH 15/22] dc203 for possible overriding --- proxy/tg_ws_proxy.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/proxy/tg_ws_proxy.py b/proxy/tg_ws_proxy.py index 6647cd6..d075e83 100644 --- a/proxy/tg_ws_proxy.py +++ b/proxy/tg_ws_proxy.py @@ -64,6 +64,8 @@ _IP_TO_DC: Dict[str, Tuple[int, bool]] = { '149.154.171.5': (5, False), '91.108.56.102': (5, True), '91.108.56.128': (5, True), '91.108.56.151': (5, True), + # DC203 + '91.105.192.100': (203, False), } _dc_opt: Dict[int, Optional[str]] = {} @@ -375,7 +377,7 @@ def _dc_from_init(data: bytes) -> Tuple[Optional[int], bool]: proto, dc_raw, plain.hex()) if proto in (0xEFEFEFEF, 0xEEEEEEEE, 0xDDDDDDDD): dc = abs(dc_raw) - if 1 <= dc <= 5: + if 1 <= dc <= 5 or dc == 203: return dc, (dc_raw < 0) except Exception as exc: log.debug("DC extraction failed: %s", exc) From e6ee4e615929ab71eca50f0712225ed16f23169d Mon Sep 17 00:00:00 2001 From: Flowseal Date: Thu, 19 Mar 2026 05:53:14 +0300 Subject: [PATCH 16/22] Hardcoded dc override for 203 --- proxy/tg_ws_proxy.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/proxy/tg_ws_proxy.py b/proxy/tg_ws_proxy.py index d075e83..7bcbc81 100644 --- a/proxy/tg_ws_proxy.py +++ b/proxy/tg_ws_proxy.py @@ -68,6 +68,11 @@ _IP_TO_DC: Dict[str, Tuple[int, bool]] = { '91.105.192.100': (203, False), } +# This case might work but not actually sure +_DC_OVERRIDES: Dict[int, int] = { + 203: 2 +} + _dc_opt: Dict[int, Optional[str]] = {} # DCs where WS is known to fail (302 redirect) @@ -464,6 +469,7 @@ class _MsgSplitter: def _ws_domains(dc: int, is_media) -> List[str]: + dc = _DC_OVERRIDES.get(dc, dc) if is_media is None or is_media: return [f'kws{dc}-1.web.telegram.org', f'kws{dc}.web.telegram.org'] return [f'kws{dc}.web.telegram.org', f'kws{dc}-1.web.telegram.org'] From 26542558c6adc2f1d7c91e2c09def794cf1a0746 Mon Sep 17 00:00:00 2001 From: Flowseal Date: Thu, 19 Mar 2026 06:23:58 +0300 Subject: [PATCH 17/22] dc fail logic rewrite for independent usability --- proxy/tg_ws_proxy.py | 21 ++++++--------------- 1 file changed, 6 insertions(+), 15 deletions(-) diff --git a/proxy/tg_ws_proxy.py b/proxy/tg_ws_proxy.py index 7bcbc81..9180986 100644 --- a/proxy/tg_ws_proxy.py +++ b/proxy/tg_ws_proxy.py @@ -82,7 +82,8 @@ _ws_blacklist: Set[Tuple[int, bool]] = set() # Rate-limit re-attempts per (dc, is_media) _dc_fail_until: Dict[Tuple[int, bool], float] = {} -_DC_FAIL_COOLDOWN = 60.0 # seconds +_DC_FAIL_COOLDOWN = 30.0 # seconds to keep reduced WS timeout after failure +_WS_FAIL_TIMEOUT = 2.0 # quick-retry timeout after a recent WS failure _ssl_ctx = ssl.create_default_context() @@ -910,20 +911,10 @@ async def _handle_client(reader, writer): label, dc, media_tag) return - # -- Cooldown check -- - fail_until = _dc_fail_until.get(dc_key, 0) - if now < fail_until: - remaining = fail_until - now - log.debug("[%s] DC%d%s WS cooldown (%.0fs) -> TCP", - label, dc, media_tag, remaining) - ok = await _tcp_fallback(reader, writer, dst, port, init, - label, dc=dc, is_media=is_media) - if ok: - log.info("[%s] DC%d%s TCP fallback closed", - label, dc, media_tag) - return - # -- Try WebSocket via direct connection -- + fail_until = _dc_fail_until.get(dc_key, 0) + ws_timeout = _WS_FAIL_TIMEOUT if now < fail_until else 10.0 + domains = _ws_domains(dc, is_media) target = _dc_opt[dc] ws = None @@ -941,7 +932,7 @@ async def _handle_client(reader, writer): label, dc, media_tag, dst, port, url, target) try: ws = await RawWebSocket.connect(target, domain, - timeout=10) + timeout=ws_timeout) all_redirects = False break except WsHandshakeError as exc: From 692157b0f550a128dd18ec5b36c1a11d59e58644 Mon Sep 17 00:00:00 2001 From: pitoni <114021843+Pitonicx@users.noreply.github.com> Date: Thu, 19 Mar 2026 06:55:55 +0300 Subject: [PATCH 18/22] Linux binary, github actions (#282) --- .github/workflows/build.yml | 43 +- README.md | 34 +- linux.py | 844 ++++++++++++++++++++++++++++++++++++ packaging/linux.spec | 80 ++++ pyproject.toml | 12 + 5 files changed, 1010 insertions(+), 3 deletions(-) create mode 100644 linux.py create mode 100644 packaging/linux.spec diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6b2d389..fc811e2 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -219,8 +219,42 @@ jobs: name: TgWsProxy-macOS path: dist/TgWsProxy.dmg + build-linux: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install system dependencies + run: | + sudo apt-get update + sudo apt-get install -y \ + python3-venv \ + python3-dev \ + python3-gi \ + gir1.2-ayatanaappindicator3-0.1 \ + python3-tk + + - name: Create venv with system site-packages + run: python3 -m venv --system-site-packages .venv + + - name: Install dependencies + run: | + .venv/bin/pip install --upgrade pip + .venv/bin/pip install ".[linux]" + .venv/bin/pip install "pyinstaller==6.13.0" + + - name: Build binary with PyInstaller + run: .venv/bin/pyinstaller packaging/linux.spec --noconfirm + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: TgWsProxy-linux + path: dist/TgWsProxy + release: - needs: [build, build-win7, build-macos] + needs: [build, build-win7, build-macos, build-linux] runs-on: ubuntu-latest if: ${{ github.event.inputs.make_release == 'true' }} steps: @@ -242,6 +276,12 @@ jobs: name: TgWsProxy-macOS path: dist + - name: Download Linux build + uses: actions/download-artifact@v4 + with: + name: TgWsProxy-linux + path: dist + - name: Create GitHub Release uses: softprops/action-gh-release@v2 with: @@ -253,6 +293,7 @@ jobs: dist/TgWsProxy.exe dist/TgWsProxy-win7.exe dist/TgWsProxy.dmg + dist/TgWsProxy draft: false prerelease: false env: diff --git a/README.md b/README.md index e9cd12a..66bf750 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,10 @@ > [!CAUTION] > > ### Реакция антивирусов +> > Windows Defender часто ошибочно помечает приложение как **Wacatac**. > Если вы не можете скачать из-за блокировки, то: +> > 1) Попробуйте скачать версию win7 (она ничем не отличается в плане функционала) > 2) Отключите антивирус на время скачивания, добавьте файл в исключения и включите обратно > @@ -29,11 +31,13 @@ Telegram Desktop → SOCKS5 (127.0.0.1:1080) → TG WS Proxy → WSS → Telegra ## 🚀 Быстрый старт ### Windows + Перейдите на [страницу релизов](https://github.com/Flowseal/tg-ws-proxy/releases) и скачайте **`TgWsProxy.exe`**. Он собирается автоматически через [Github Actions](https://github.com/Flowseal/tg-ws-proxy/actions) из открытого исходного кода. При первом запуске откроется окно с инструкцией по подключению Telegram Desktop. Приложение сворачивается в системный трей. **Меню трея:** + - **Открыть в Telegram** — автоматически настроить прокси через `tg://socks` ссылку - **Перезапустить прокси** — перезапуск без выхода из приложения - **Настройки...** — GUI-редактор конфигурации @@ -41,12 +45,24 @@ Telegram Desktop → SOCKS5 (127.0.0.1:1080) → TG WS Proxy → WSS → Telegra - **Выход** — остановить прокси и закрыть приложение ### macOS + Перейдите на [страницу релизов](https://github.com/Flowseal/tg-ws-proxy/releases) и скачайте **`TgWsProxy.dmg`** — универсальная сборка для Apple Silicon и Intel. 1. Открыть образ 2. Перенести **TG WS Proxy.app** в папку **Applications** 3. При первом запуске macOS может попросить подтвердить открытие: **Системные настройки → Конфиденциальность и безопасность → Всё равно открыть** +### Linux + +Перейдите на [страницу релизов](https://github.com/Flowseal/tg-ws-proxy/releases) и скачайте **`TgWsProxy`** (бинарный файл для x86_64). + +```bash +chmod +x TgWsProxy +./TgWsProxy +``` + +При первом запуске откроется окно с инструкцией. Приложение работает в системном трее (требуется AppIndicator). + ## Установка из исходников ### Консольный proxy @@ -79,6 +95,13 @@ pip install -e ".[macos]" tg-ws-proxy-tray-macos ``` +### Linux + +```bash +pip install -e ".[linux]" +tg-ws-proxy-tray-linux +``` + ### Консольный режим из исходников ```bash @@ -118,6 +141,7 @@ CLI команды объявляются в `pyproject.toml` в секции `[ tg-ws-proxy = "proxy.tg_ws_proxy:main" tg-ws-proxy-tray-win = "windows:main" tg-ws-proxy-tray-macos = "macos:main" +tg-ws-proxy-tray-linux = "linux:main" ``` ## Настройка Telegram Desktop @@ -137,7 +161,11 @@ tg-ws-proxy-tray-macos = "macos:main" ## Конфигурация -Tray-приложение хранит данные в `%APPDATA%/TgWsProxy`: +Tray-приложение хранит данные в: + +- **Windows:** `%APPDATA%/TgWsProxy` +- **macOS:** `~/Library/Application Support/TgWsProxy` +- **Linux:** `~/.config/TgWsProxy` (или `$XDG_CONFIG_HOME/TgWsProxy`) ```json { @@ -152,13 +180,15 @@ Tray-приложение хранит данные в `%APPDATA%/TgWsProxy`: ## Автоматическая сборка -Проект содержит спецификации PyInstaller ([`packaging/windows.spec`](packaging/windows.spec), [`packaging/macos.spec`](packaging/macos.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)) для автоматической сборки. Минимально поддерживаемые версии ОС для текущих бинарных сборок: + - Windows 10+ для `TgWsProxy.exe` - Windows 7 для `TgWsProxy-win7.exe` - Intel macOS 10.15+ - Apple Silicon macOS 11.0+ +- Linux x86_64 (требуется AppIndicator для системного трея) ## Лицензия diff --git a/linux.py b/linux.py new file mode 100644 index 0000000..24e6c2a --- /dev/null +++ b/linux.py @@ -0,0 +1,844 @@ +from __future__ import annotations + +import asyncio as _asyncio +import json +import logging +import os +import subprocess +import sys +import threading +import time +import webbrowser +from pathlib import Path +from typing import Dict, Optional + +import customtkinter as ctk +import psutil +import pyperclip +import pystray +from PIL import Image, ImageDraw, ImageFont + +import proxy.tg_ws_proxy as tg_ws_proxy + +APP_NAME = "TgWsProxy" +APP_DIR = Path(os.environ.get("XDG_CONFIG_HOME", Path.home() / ".config")) / APP_NAME +CONFIG_FILE = APP_DIR / "config.json" +LOG_FILE = APP_DIR / "proxy.log" +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, +} + + +_proxy_thread: Optional[threading.Thread] = None +_async_stop: Optional[object] = None +_tray_icon: Optional[object] = None +_config: dict = {} +_exiting: bool = False +_lock_file_path: Optional[Path] = None + +log = logging.getLogger("tg-ws-tray") + + +def _same_process(lock_meta: dict, proc: psutil.Process) -> bool: + try: + lock_ct = float(lock_meta.get("create_time", 0.0)) + proc_ct = float(proc.create_time()) + if lock_ct > 0 and abs(lock_ct - proc_ct) > 1.0: + return False + except Exception: + return False + + try: + cmdline = proc.cmdline() + for arg in cmdline: + if "linux.py" in arg: + return True + except Exception: + pass + + frozen = bool(getattr(sys, "frozen", False)) + if frozen: + return APP_NAME.lower() in proc.name().lower() + + return False + + +def _release_lock(): + global _lock_file_path + if not _lock_file_path: + return + try: + _lock_file_path.unlink(missing_ok=True) + except Exception: + pass + _lock_file_path = None + + +def _acquire_lock() -> bool: + global _lock_file_path + _ensure_dirs() + lock_files = list(APP_DIR.glob("*.lock")) + + for f in lock_files: + pid = None + meta: dict = {} + + try: + pid = int(f.stem) + except Exception: + f.unlink(missing_ok=True) + continue + + try: + raw = f.read_text(encoding="utf-8").strip() + if raw: + meta = json.loads(raw) + except Exception: + meta = {} + + try: + proc = psutil.Process(pid) + if _same_process(meta, proc): + return False + except Exception: + pass + + f.unlink(missing_ok=True) + + lock_file = APP_DIR / f"{os.getpid()}.lock" + try: + proc = psutil.Process(os.getpid()) + payload = { + "create_time": proc.create_time(), + } + lock_file.write_text(json.dumps(payload, ensure_ascii=False), encoding="utf-8") + except Exception: + lock_file.touch() + + _lock_file_path = lock_file + return True + + +def _ensure_dirs(): + APP_DIR.mkdir(parents=True, exist_ok=True) + + +def load_config() -> dict: + _ensure_dirs() + if CONFIG_FILE.exists(): + try: + with open(CONFIG_FILE, "r", encoding="utf-8") as f: + data = json.load(f) + for k, v in DEFAULT_CONFIG.items(): + data.setdefault(k, v) + return data + except Exception as exc: + log.warning("Failed to load config: %s", exc) + return dict(DEFAULT_CONFIG) + + +def save_config(cfg: dict): + _ensure_dirs() + with open(CONFIG_FILE, "w", encoding="utf-8") as f: + json.dump(cfg, f, indent=2, ensure_ascii=False) + + +def setup_logging(verbose: bool = False): + _ensure_dirs() + root = logging.getLogger() + root.setLevel(logging.DEBUG if verbose else logging.INFO) + + fh = logging.FileHandler(str(LOG_FILE), encoding="utf-8") + fh.setLevel(logging.DEBUG) + fh.setFormatter( + logging.Formatter( + "%(asctime)s %(levelname)-5s %(name)s %(message)s", + datefmt="%Y-%m-%d %H:%M:%S", + ) + ) + root.addHandler(fh) + + if not getattr(sys, "frozen", False): + ch = logging.StreamHandler(sys.stdout) + ch.setLevel(logging.DEBUG if verbose else logging.INFO) + ch.setFormatter( + logging.Formatter( + "%(asctime)s %(levelname)-5s %(message)s", datefmt="%H:%M:%S" + ) + ) + root.addHandler(ch) + + +def _make_icon_image(size: int = 64): + if Image is None: + raise RuntimeError("Pillow is required for tray icon") + img = Image.new("RGBA", (size, size), (0, 0, 0, 0)) + draw = ImageDraw.Draw(img) + + margin = 2 + draw.ellipse( + [margin, margin, size - margin, size - margin], fill=(0, 136, 204, 255) + ) + + try: + font = ImageFont.truetype( + "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", + size=int(size * 0.55), + ) + except Exception: + try: + font = ImageFont.truetype( + "/usr/share/fonts/TTF/DejaVuSans-Bold.ttf", size=int(size * 0.55) + ) + except Exception: + font = ImageFont.load_default() + bbox = draw.textbbox((0, 0), "T", font=font) + tw, th = bbox[2] - bbox[0], bbox[3] - bbox[1] + tx = (size - tw) // 2 - bbox[0] + ty = (size - th) // 2 - bbox[1] + draw.text((tx, ty), "T", fill=(255, 255, 255, 255), font=font) + + return img + + +def _load_icon(): + icon_path = Path(__file__).parent / "icon.ico" + if icon_path.exists() and Image: + try: + return Image.open(str(icon_path)) + except Exception: + pass + return _make_icon_image() + + +def _run_proxy_thread( + port: int, dc_opt: Dict[int, str], verbose: bool, host: str = "127.0.0.1" +): + global _async_stop + loop = _asyncio.new_event_loop() + _asyncio.set_event_loop(loop) + stop_ev = _asyncio.Event() + _async_stop = (loop, stop_ev) + + try: + loop.run_until_complete( + tg_ws_proxy._run(port, dc_opt, stop_event=stop_ev, host=host) + ) + except Exception as exc: + log.error("Proxy thread crashed: %s", exc) + if "Address already in use" in str(exc): + _show_error( + "Не удалось запустить прокси:\nПорт уже используется другим приложением.\n\nЗакройте приложение, использующее этот порт, или измените порт в настройках прокси и перезапустите." + ) + finally: + loop.close() + _async_stop = None + + +def start_proxy(): + global _proxy_thread, _config + if _proxy_thread and _proxy_thread.is_alive(): + log.info("Proxy already running") + return + + cfg = _config + port = cfg.get("port", DEFAULT_CONFIG["port"]) + host = cfg.get("host", DEFAULT_CONFIG["host"]) + dc_ip_list = cfg.get("dc_ip", DEFAULT_CONFIG["dc_ip"]) + verbose = cfg.get("verbose", False) + + try: + dc_opt = tg_ws_proxy.parse_dc_ip_list(dc_ip_list) + except ValueError as e: + log.error("Bad config dc_ip: %s", e) + _show_error(f"Ошибка конфигурации:\n{e}") + return + + log.info("Starting proxy on %s:%d ...", host, port) + _proxy_thread = threading.Thread( + target=_run_proxy_thread, + args=(port, dc_opt, verbose, host), + daemon=True, + name="proxy", + ) + _proxy_thread.start() + + +def stop_proxy(): + global _proxy_thread, _async_stop + if _async_stop: + loop, stop_ev = _async_stop + loop.call_soon_threadsafe(stop_ev.set) + if _proxy_thread: + _proxy_thread.join(timeout=2) + _proxy_thread = None + log.info("Proxy stopped") + + +def restart_proxy(): + log.info("Restarting proxy...") + stop_proxy() + time.sleep(0.3) + start_proxy() + + +def _show_error(text: str, title: str = "TG WS Proxy — Ошибка"): + import tkinter as _tk + from tkinter import messagebox as _mb + + root = _tk.Tk() + root.withdraw() + _mb.showerror(title, text, parent=root) + root.destroy() + + +def _show_info(text: str, title: str = "TG WS Proxy"): + import tkinter as _tk + from tkinter import messagebox as _mb + + root = _tk.Tk() + root.withdraw() + _mb.showinfo(title, text, parent=root) + root.destroy() + + +def _on_open_in_telegram(icon=None, item=None): + port = _config.get("port", DEFAULT_CONFIG["port"]) + url = f"tg://socks?server=127.0.0.1&port={port}" + log.info("Copying %s", url) + + try: + pyperclip.copy(url) + _show_info( + f"Ссылка скопирована в буфер обмена, отправьте её в Telegram и нажмите по ней ЛКМ:\n{url}", + "TG WS Proxy", + ) + except Exception as exc: + log.error("Clipboard copy failed: %s", exc) + _show_error(f"Не удалось скопировать ссылку:\n{exc}") + + +def _on_restart(icon=None, item=None): + threading.Thread(target=restart_proxy, daemon=True).start() + + +def _on_edit_config(icon=None, item=None): + threading.Thread(target=_edit_config_dialog, daemon=True).start() + + +def _edit_config_dialog(): + if ctk is None: + _show_error("customtkinter не установлен.") + return + + cfg = dict(_config) + + ctk.set_appearance_mode("light") + ctk.set_default_color_theme("blue") + + root = ctk.CTk() + root.title("TG WS Proxy — Настройки") + root.resizable(False, False) + root.attributes("-topmost", True) + + icon_img = _load_icon() + if icon_img: + from PIL import ImageTk + + _photo = ImageTk.PhotoImage(icon_img.resize((64, 64))) + root.iconphoto(False, _photo) + + TG_BLUE = "#3390ec" + TG_BLUE_HOVER = "#2b7cd4" + BG = "#ffffff" + FIELD_BG = "#f0f2f5" + FIELD_BORDER = "#d6d9dc" + TEXT_PRIMARY = "#000000" + TEXT_SECONDARY = "#707579" + FONT_FAMILY = "Sans" + + w, h = 420, 480 + sw = root.winfo_screenwidth() + sh = root.winfo_screenheight() + root.geometry(f"{w}x{h}+{(sw - w) // 2}+{(sh - h) // 2}") + root.configure(fg_color=BG) + + frame = ctk.CTkFrame(root, fg_color=BG, corner_radius=0) + frame.pack(fill="both", expand=True, padx=24, pady=20) + + # Host + ctk.CTkLabel( + frame, + text="IP-адрес прокси", + font=(FONT_FAMILY, 13), + text_color=TEXT_PRIMARY, + anchor="w", + ).pack(anchor="w", pady=(0, 4)) + host_var = ctk.StringVar(value=cfg.get("host", "127.0.0.1")) + host_entry = ctk.CTkEntry( + frame, + textvariable=host_var, + width=200, + height=36, + font=(FONT_FAMILY, 13), + corner_radius=10, + fg_color=FIELD_BG, + border_color=FIELD_BORDER, + border_width=1, + text_color=TEXT_PRIMARY, + ) + host_entry.pack(anchor="w", pady=(0, 12)) + + # Port + ctk.CTkLabel( + frame, + text="Порт прокси", + font=(FONT_FAMILY, 13), + text_color=TEXT_PRIMARY, + anchor="w", + ).pack(anchor="w", pady=(0, 4)) + port_var = ctk.StringVar(value=str(cfg.get("port", 1080))) + port_entry = ctk.CTkEntry( + frame, + textvariable=port_var, + width=120, + height=36, + font=(FONT_FAMILY, 13), + corner_radius=10, + fg_color=FIELD_BG, + border_color=FIELD_BORDER, + border_width=1, + text_color=TEXT_PRIMARY, + ) + port_entry.pack(anchor="w", pady=(0, 12)) + + # DC-IP mappings + ctk.CTkLabel( + frame, + text="DC → IP маппинги (по одному на строку, формат DC:IP)", + font=(FONT_FAMILY, 13), + text_color=TEXT_PRIMARY, + anchor="w", + ).pack(anchor="w", pady=(0, 4)) + dc_textbox = ctk.CTkTextbox( + frame, + width=370, + height=120, + font=("Monospace", 12), + corner_radius=10, + fg_color=FIELD_BG, + border_color=FIELD_BORDER, + border_width=1, + text_color=TEXT_PRIMARY, + ) + dc_textbox.pack(anchor="w", pady=(0, 12)) + dc_textbox.insert("1.0", "\n".join(cfg.get("dc_ip", DEFAULT_CONFIG["dc_ip"]))) + + # Verbose + verbose_var = ctk.BooleanVar(value=cfg.get("verbose", False)) + ctk.CTkCheckBox( + frame, + text="Подробное логирование (verbose)", + variable=verbose_var, + font=(FONT_FAMILY, 13), + text_color=TEXT_PRIMARY, + fg_color=TG_BLUE, + hover_color=TG_BLUE_HOVER, + corner_radius=6, + border_width=2, + border_color=FIELD_BORDER, + ).pack(anchor="w", pady=(0, 8)) + + # Info label + ctk.CTkLabel( + frame, + text="Изменения вступят в силу после перезапуска прокси.", + font=(FONT_FAMILY, 11), + text_color=TEXT_SECONDARY, + anchor="w", + ).pack(anchor="w", pady=(0, 16)) + + def on_save(): + import socket as _sock + + host_val = host_var.get().strip() + try: + _sock.inet_aton(host_val) + except OSError: + _show_error("Некорректный IP-адрес.") + return + + try: + port_val = int(port_var.get().strip()) + if not (1 <= port_val <= 65535): + raise ValueError + except ValueError: + _show_error("Порт должен быть числом 1-65535") + return + + lines = [ + l.strip() + for l in dc_textbox.get("1.0", "end").strip().splitlines() + if l.strip() + ] + try: + tg_ws_proxy.parse_dc_ip_list(lines) + except ValueError as e: + _show_error(str(e)) + return + + new_cfg = { + "host": host_val, + "port": port_val, + "dc_ip": lines, + "verbose": verbose_var.get(), + } + save_config(new_cfg) + _config.update(new_cfg) + log.info("Config saved: %s", new_cfg) + + _tray_icon.menu = _build_menu() + + from tkinter import messagebox + + if messagebox.askyesno( + "Перезапустить?", + "Настройки сохранены.\n\nПерезапустить прокси сейчас?", + parent=root, + ): + root.destroy() + restart_proxy() + else: + root.destroy() + + def on_cancel(): + root.destroy() + + btn_frame = ctk.CTkFrame(frame, fg_color="transparent") + btn_frame.pack(fill="x") + ctk.CTkButton( + btn_frame, + text="Сохранить", + width=140, + height=38, + font=(FONT_FAMILY, 14, "bold"), + corner_radius=10, + fg_color=TG_BLUE, + hover_color=TG_BLUE_HOVER, + text_color="#ffffff", + command=on_save, + ).pack(side="left", padx=(0, 10)) + ctk.CTkButton( + btn_frame, + text="Отмена", + width=140, + height=38, + font=(FONT_FAMILY, 14), + corner_radius=10, + fg_color=FIELD_BG, + hover_color=FIELD_BORDER, + text_color=TEXT_PRIMARY, + border_width=1, + border_color=FIELD_BORDER, + command=on_cancel, + ).pack(side="left") + + root.mainloop() + + +def _on_open_logs(icon=None, item=None): + log.info("Opening log file: %s", LOG_FILE) + if LOG_FILE.exists(): + env = os.environ.copy() + env.pop("VIRTUAL_ENV", None) + env.pop("PYTHONPATH", None) + env.pop("PYTHONHOME", None) + + subprocess.Popen( + ["xdg-open", str(LOG_FILE)], + env=env, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + stdin=subprocess.DEVNULL, + start_new_session=True, + ) + else: + _show_info("Файл логов ещё не создан.", "TG WS Proxy") + + +def _on_exit(icon=None, item=None): + global _exiting + if _exiting: + os._exit(0) + return + _exiting = True + log.info("User requested exit") + + def _force_exit(): + time.sleep(3) + os._exit(0) + + threading.Thread(target=_force_exit, daemon=True, name="force-exit").start() + + if icon: + icon.stop() + + +def _show_first_run(): + _ensure_dirs() + if FIRST_RUN_MARKER.exists(): + return + + host = _config.get("host", DEFAULT_CONFIG["host"]) + port = _config.get("port", DEFAULT_CONFIG["port"]) + tg_url = f"tg://socks?server={host}&port={port}" + + if ctk is None: + FIRST_RUN_MARKER.touch() + return + + ctk.set_appearance_mode("light") + ctk.set_default_color_theme("blue") + + TG_BLUE = "#3390ec" + TG_BLUE_HOVER = "#2b7cd4" + BG = "#ffffff" + FIELD_BG = "#f0f2f5" + FIELD_BORDER = "#d6d9dc" + TEXT_PRIMARY = "#000000" + TEXT_SECONDARY = "#707579" + FONT_FAMILY = "Sans" + + root = ctk.CTk() + root.title("TG WS Proxy") + root.resizable(False, False) + root.attributes("-topmost", True) + + icon_img = _load_icon() + if icon_img: + from PIL import ImageTk + + _photo = ImageTk.PhotoImage(icon_img.resize((64, 64))) + root.iconphoto(False, _photo) + + w, h = 520, 440 + sw = root.winfo_screenwidth() + sh = root.winfo_screenheight() + root.geometry(f"{w}x{h}+{(sw - w) // 2}+{(sh - h) // 2}") + root.configure(fg_color=BG) + + frame = ctk.CTkFrame(root, fg_color=BG, corner_radius=0) + frame.pack(fill="both", expand=True, padx=28, pady=24) + + title_frame = ctk.CTkFrame(frame, fg_color="transparent") + title_frame.pack(anchor="w", pady=(0, 16), fill="x") + + # Blue accent bar + accent_bar = ctk.CTkFrame( + title_frame, fg_color=TG_BLUE, width=4, height=32, corner_radius=2 + ) + accent_bar.pack(side="left", padx=(0, 12)) + + ctk.CTkLabel( + title_frame, + text="Прокси запущен и работает в системном трее", + font=(FONT_FAMILY, 17, "bold"), + text_color=TEXT_PRIMARY, + ).pack(side="left") + + # Info sections + sections = [ + ("Как подключить Telegram Desktop:", True), + (" Автоматически:", True), + (f" ПКМ по иконке в трее → «Открыть в Telegram»", False), + (f" Или ссылка: {tg_url}", False), + ("\n Вручную:", True), + (" Настройки → Продвинутые → Тип подключения → Прокси", False), + (f" SOCKS5 → {host} : {port} (без логина/пароля)", False), + ] + + for text, bold in sections: + weight = "bold" if bold else "normal" + ctk.CTkLabel( + frame, + text=text, + font=(FONT_FAMILY, 13, weight), + text_color=TEXT_PRIMARY, + anchor="w", + justify="left", + ).pack(anchor="w", pady=1) + + # Spacer + ctk.CTkFrame(frame, fg_color="transparent", height=16).pack() + + # Separator + ctk.CTkFrame(frame, fg_color=FIELD_BORDER, height=1, corner_radius=0).pack( + fill="x", pady=(0, 12) + ) + + # Checkbox + auto_var = ctk.BooleanVar(value=True) + ctk.CTkCheckBox( + frame, + text="Открыть прокси в Telegram сейчас", + variable=auto_var, + font=(FONT_FAMILY, 13), + text_color=TEXT_PRIMARY, + fg_color=TG_BLUE, + hover_color=TG_BLUE_HOVER, + corner_radius=6, + border_width=2, + border_color=FIELD_BORDER, + ).pack(anchor="w", pady=(0, 16)) + + def on_ok(): + FIRST_RUN_MARKER.touch() + open_tg = auto_var.get() + root.destroy() + if open_tg: + _on_open_in_telegram() + + ctk.CTkButton( + frame, + text="Начать", + width=180, + height=42, + font=(FONT_FAMILY, 15, "bold"), + corner_radius=10, + fg_color=TG_BLUE, + hover_color=TG_BLUE_HOVER, + text_color="#ffffff", + command=on_ok, + ).pack(pady=(0, 0)) + + root.protocol("WM_DELETE_WINDOW", on_ok) + root.mainloop() + + +def _has_ipv6_enabled() -> bool: + import socket as _sock + + try: + addrs = _sock.getaddrinfo(_sock.gethostname(), None, _sock.AF_INET6) + for addr in addrs: + ip = addr[4][0] + if ip and not ip.startswith("::1") and not ip.startswith("fe80::1"): + return True + except Exception: + pass + try: + s = _sock.socket(_sock.AF_INET6, _sock.SOCK_STREAM) + s.bind(("::1", 0)) + s.close() + return True + except Exception: + return False + + +def _check_ipv6_warning(): + _ensure_dirs() + if IPV6_WARN_MARKER.exists(): + return + if not _has_ipv6_enabled(): + return + + IPV6_WARN_MARKER.touch() + + threading.Thread(target=_show_ipv6_dialog, daemon=True).start() + + +def _show_ipv6_dialog(): + _show_info( + "На вашем компьютере включена поддержка подключения по IPv6.\n\n" + "Telegram может пытаться подключаться через IPv6, " + "что не поддерживается и может привести к ошибкам.\n\n" + "Если прокси не работает или в логах присутствуют ошибки, " + "связанные с попытками подключения по IPv6 - " + "попробуйте отключить в настройках прокси Telegram попытку соединения " + "по IPv6. Если данная мера не помогает, попробуйте отключить IPv6 " + "в системе.\n\n" + "Это предупреждение будет показано только один раз.", + "TG WS Proxy", + ) + + +def _build_menu(): + if pystray is None: + return None + host = _config.get("host", DEFAULT_CONFIG["host"]) + port = _config.get("port", DEFAULT_CONFIG["port"]) + return pystray.Menu( + pystray.MenuItem( + f"Открыть в Telegram ({host}:{port})", _on_open_in_telegram, default=True + ), + pystray.Menu.SEPARATOR, + pystray.MenuItem("Перезапустить прокси", _on_restart), + pystray.MenuItem("Настройки...", _on_edit_config), + pystray.MenuItem("Открыть логи", _on_open_logs), + pystray.Menu.SEPARATOR, + pystray.MenuItem("Выход", _on_exit), + ) + + +def run_tray(): + global _tray_icon, _config + + _config = load_config() + save_config(_config) + + if LOG_FILE.exists(): + try: + LOG_FILE.unlink() + except Exception: + pass + + setup_logging(_config.get("verbose", False)) + log.info("TG WS Proxy tray app starting") + log.info("Config: %s", _config) + log.info("Log file: %s", LOG_FILE) + + if pystray is None or Image is None: + log.error("pystray or Pillow not installed; running in console mode") + start_proxy() + try: + while True: + time.sleep(1) + except KeyboardInterrupt: + stop_proxy() + return + + start_proxy() + + _show_first_run() + _check_ipv6_warning() + + icon_image = _load_icon() + _tray_icon = pystray.Icon(APP_NAME, icon_image, "TG WS Proxy", menu=_build_menu()) + + log.info("Tray icon running") + _tray_icon.run() + + stop_proxy() + log.info("Tray app exited") + + +def main(): + if not _acquire_lock(): + _show_info("Приложение уже запущено.", os.path.basename(sys.argv[0])) + return + + try: + run_tray() + finally: + _release_lock() + + +if __name__ == "__main__": + main() diff --git a/packaging/linux.spec b/packaging/linux.spec new file mode 100644 index 0000000..ab27315 --- /dev/null +++ b/packaging/linux.spec @@ -0,0 +1,80 @@ +# -*- mode: python ; coding: utf-8 -*- + +import sys +import os +import glob + +from PyInstaller.utils.hooks import collect_submodules, collect_data_files + +block_cipher = None + +# customtkinter ships JSON themes + assets that must be bundled +import customtkinter +ctk_path = os.path.dirname(customtkinter.__file__) + +# Collect gi (PyGObject) submodules and data so pystray._appindicator works +gi_hiddenimports = collect_submodules('gi') +gi_datas = collect_data_files('gi') + +# Collect GObject typelib files from the system +typelib_dirs = glob.glob('/usr/lib/*/girepository-1.0') +typelib_datas = [] +for d in typelib_dirs: + typelib_datas.append((d, 'gi_typelibs')) + +a = Analysis( + [os.path.join(os.path.dirname(SPEC), os.pardir, 'linux.py')], + pathex=[], + binaries=[], + datas=[(ctk_path, 'customtkinter/')] + gi_datas + typelib_datas, + hiddenimports=[ + 'pystray._appindicator', + 'PIL._tkinter_finder', + 'customtkinter', + 'cryptography.hazmat.primitives.ciphers', + 'cryptography.hazmat.primitives.ciphers.algorithms', + 'cryptography.hazmat.primitives.ciphers.modes', + 'cryptography.hazmat.backends.openssl', + 'gi', + '_gi', + 'gi.repository.GLib', + 'gi.repository.GObject', + 'gi.repository.Gtk', + 'gi.repository.Gdk', + 'gi.repository.AyatanaAppIndicator3', + ] + gi_hiddenimports, + hookspath=[], + hooksconfig={}, + runtime_hooks=[], + excludes=[], + noarchive=False, + cipher=block_cipher, +) + +icon_path = os.path.join(os.path.dirname(SPEC), os.pardir, 'icon.ico') +if os.path.exists(icon_path): + a.datas += [('icon.ico', icon_path, 'DATA')] + +pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher) + +exe = EXE( + pyz, + a.scripts, + a.binaries, + a.zipfiles, + a.datas, + [], + name='TgWsProxy', + debug=False, + bootloader_ignore_signals=False, + strip=True, + upx=True, + upx_exclude=[], + runtime_tmpdir=None, + console=False, + disable_windowed_traceback=False, + argv_emulation=False, + target_arch=None, + codesign_identity=None, + entitlements_file=None, +) diff --git a/pyproject.toml b/pyproject.toml index fab74bc..bbd9c1a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,11 +26,13 @@ classifiers = [ "Environment :: Console", "Environment :: MacOS X :: Cocoa", "Environment :: Win32 (MS Windows)", + "Environment :: X11 Applications :: GTK", "Intended Audience :: Customer Service", "Programming Language :: Python :: 3", "License :: OSI Approved :: MIT License", "Operating System :: MacOS :: MacOS X", "Operating System :: Microsoft :: Windows", + "Operating System :: POSIX :: Linux", "Topic :: System :: Networking :: Firewalls", ] @@ -63,10 +65,19 @@ macos = [ "rumps==0.4.0", ] +linux = [ + "customtkinter==5.2.2", + "Pillow==12.1.1", + "psutil==7.0.0", + "pystray==0.19.5", + "pyperclip==1.9.0", +] + [project.scripts] tg-ws-proxy = "proxy.tg_ws_proxy:main" tg-ws-proxy-tray-win = "windows:main" tg-ws-proxy-tray-macos = "macos:main" +tg-ws-proxy-tray-linux = "linux:main" [project.urls] Source = "https://github.com/Flowseal/tg-ws-proxy" @@ -78,6 +89,7 @@ packages = ["proxy"] [tool.hatch.build.force-include] "windows.py" = "windows.py" "macos.py" = "macos.py" +"linux.py" = "linux.py" [tool.hatch.version] path = "proxy/__init__.py" From 5e53a8a4705e27eab7907a3df9c36ce46b3d695e Mon Sep 17 00:00:00 2001 From: Flowseal Date: Thu, 19 Mar 2026 07:03:11 +0300 Subject: [PATCH 19/22] unused import --- linux.py | 1 - 1 file changed, 1 deletion(-) diff --git a/linux.py b/linux.py index 24e6c2a..9b2ff9e 100644 --- a/linux.py +++ b/linux.py @@ -8,7 +8,6 @@ import subprocess import sys import threading import time -import webbrowser from pathlib import Path from typing import Dict, Optional From 7943c539b62640b16fe03f308cb0dc26923d66a1 Mon Sep 17 00:00:00 2001 From: Flowseal Date: Thu, 19 Mar 2026 07:28:46 +0300 Subject: [PATCH 20/22] .deb build test --- .github/workflows/build.yml | 60 ++++++++++++++++++++++++++++++++++++- 1 file changed, 59 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index fc811e2..ceae1ef 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -247,11 +247,68 @@ jobs: - name: Build binary with PyInstaller run: .venv/bin/pyinstaller packaging/linux.spec --noconfirm + - name: Create .deb package + run: | + set -euo pipefail + VERSION="${{ github.event.inputs.version }}" + VERSION="${VERSION#v}" + PKG_ROOT="pkg" + + rm -rf "$PKG_ROOT" + mkdir -p \ + "$PKG_ROOT/DEBIAN" \ + "$PKG_ROOT/usr/bin" \ + "$PKG_ROOT/usr/share/applications" \ + "$PKG_ROOT/usr/share/icons/hicolor/256x256/apps" + + install -m 755 dist/TgWsProxy "$PKG_ROOT/usr/bin/tg-ws-proxy" + + .venv/bin/python - < "$PKG_ROOT/usr/share/applications/tg-ws-proxy.desktop" < "$PKG_ROOT/DEBIAN/control" < Date: Thu, 19 Mar 2026 07:43:42 +0300 Subject: [PATCH 21/22] built files rename --- .github/workflows/build.yml | 34 ++++++++++++++++++++-------------- README.md | 16 +++++++++------- 2 files changed, 29 insertions(+), 21 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ceae1ef..0721ad9 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -38,12 +38,15 @@ jobs: - name: Build EXE with PyInstaller run: pyinstaller packaging/windows.spec --noconfirm + - name: Rename artifact + run: mv dist/TgWsProxy.exe dist/TgWsProxy_windows.exe + - name: Upload artifact uses: actions/upload-artifact@v4 with: name: TgWsProxy path: | - dist/TgWsProxy.exe + dist/TgWsProxy_windows.exe build-win7: runs-on: windows-latest @@ -67,13 +70,13 @@ jobs: run: pyinstaller packaging/windows.spec --noconfirm - name: Rename artifact - run: mv dist/TgWsProxy.exe dist/TgWsProxy-win7.exe + run: mv dist/TgWsProxy.exe dist/TgWsProxy_windows_7.exe - name: Upload artifact uses: actions/upload-artifact@v4 with: name: TgWsProxy-win7 - path: dist/TgWsProxy-win7.exe + path: dist/TgWsProxy_windows_7.exe build-macos: runs-on: macos-latest @@ -209,7 +212,7 @@ jobs: -srcfolder "$DMG_TEMP" \ -ov \ -format UDZO \ - "dist/TgWsProxy.dmg" + "dist/TgWsProxy_macos_universal.dmg" rm -rf "$DMG_TEMP" @@ -217,7 +220,7 @@ jobs: uses: actions/upload-artifact@v4 with: name: TgWsProxy-macOS - path: dist/TgWsProxy.dmg + path: dist/TgWsProxy_macos_universal.dmg build-linux: runs-on: ubuntu-latest @@ -247,6 +250,9 @@ jobs: - name: Build binary with PyInstaller run: .venv/bin/pyinstaller packaging/linux.spec --noconfirm + - name: Rename binary artifact + run: mv dist/TgWsProxy dist/TgWsProxy_linux_amd64 + - name: Create .deb package run: | set -euo pipefail @@ -261,7 +267,7 @@ jobs: "$PKG_ROOT/usr/share/applications" \ "$PKG_ROOT/usr/share/icons/hicolor/256x256/apps" - install -m 755 dist/TgWsProxy "$PKG_ROOT/usr/bin/tg-ws-proxy" + install -m 755 dist/TgWsProxy_linux_amd64 "$PKG_ROOT/usr/bin/tg-ws-proxy" .venv/bin/python - < Date: Thu, 19 Mar 2026 11:09:07 +0300 Subject: [PATCH 22/22] fix for default dc options --- proxy/tg_ws_proxy.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/proxy/tg_ws_proxy.py b/proxy/tg_ws_proxy.py index 9180986..8bd0c45 100644 --- a/proxy/tg_ws_proxy.py +++ b/proxy/tg_ws_proxy.py @@ -1117,13 +1117,16 @@ def main(): ap.add_argument('--host', type=str, default='127.0.0.1', help='Listen host (default 127.0.0.1)') ap.add_argument('--dc-ip', metavar='DC:IP', action='append', - default=['2:149.154.167.220', '4:149.154.167.220'], + default=[], help='Target IP for a DC, e.g. --dc-ip 1:149.154.175.205' ' --dc-ip 2:149.154.167.220') ap.add_argument('-v', '--verbose', action='store_true', help='Debug logging') args = ap.parse_args() + if not args.dc_ip: + args.dc_ip = ['2:149.154.167.220', '4:149.154.167.220'] + try: dc_opt = parse_dc_ip_list(args.dc_ip) except ValueError as e: