v2.5.0: add key disable switches and pro UI polish

This commit is contained in:
Виталий Литвинов
2026-04-25 09:44:53 +03:00
parent 6b89c3ea81
commit e54778c08c
17 changed files with 683 additions and 136 deletions

View File

@@ -1,4 +1,4 @@
# GoTelegram Pro — техническая документация для ИИ-агентов
# goTelegram Pro — техническая документация для ИИ-агентов
**Версия:** 2.5.0
**Репозиторий:** `anten-ka/gotelegram_pro`
@@ -11,7 +11,7 @@
## 1. Общая картина
GoTelegram Pro — это менеджер MTProxy для Telegram, собранный вокруг Rust-ядра **telemt** (порт mtproto-proxy с поддержкой fake-TLS, v3.3.x). Проект даёт три вещи:
goTelegram Pro — это менеджер MTProxy для Telegram, собранный вокруг Rust-ядра **telemt** (порт mtproto-proxy с поддержкой fake-TLS, v3.3.x). Проект даёт три вещи:
1. **CLI-меню на bash** (`install.sh` + `lib/*.sh`) — установка, настройка, обновление, бекап, перезапуск, смена режима, управление сайтом-маскировкой, выбор шаблона. Единая точка входа `gotelegram` (symlink → `/opt/gotelegram/install.sh`).
2. **Сайт-маскировку** — поднимает рядом nginx с настоящим сайтом на настоящем домене (Let's Encrypt) из каталога 1801 HTML-шаблонов (html5up / startbootstrap / ThemeWagon / dawidolko). В stealth-режиме telemt слушает 443, маскировочный трафик проксирует на локальный nginx (127.0.0.1:8443) через `dns_overrides`.
@@ -99,7 +99,7 @@ gotelegram-bot/locales/*.json 99 ключей i18n для бота с per-user
| --- | --- |
| Репо-скрипты | `/opt/gotelegram/` |
| Symlink запуска | `/usr/local/bin/gotelegram``/opt/gotelegram/install.sh` |
| Конфиг GoTelegram (JSON) | `/opt/gotelegram/config.json` |
| Конфиг goTelegram Pro (JSON) | `/opt/gotelegram/config.json` |
| Конфиг telemt | `/etc/telemt/config.toml` |
| Бинарник telemt | `/usr/local/bin/telemt` |
| Systemd юнит telemt | `/etc/systemd/system/telemt.service` |
@@ -109,7 +109,7 @@ gotelegram-bot/locales/*.json 99 ключей i18n для бота с per-user
| nginx конфиг | `/etc/nginx/sites-available/gotelegram` |
| nginx enabled | `/etc/nginx/sites-enabled/gotelegram` |
| Бекапы | `/opt/gotelegram/backups/` |
| Лог GoTelegram | `/var/log/gotelegram.log` |
| Лог goTelegram Pro | `/var/log/gotelegram.log` |
| Логи telemt | `journalctl -u telemt` |
| Логи бота | `journalctl -u gotelegram-bot` |
@@ -196,7 +196,7 @@ dns_overrides = ["anten-ka.com:8443:127.0.0.1"]
Генерируется функцией `install_telemt_service()` в `lib/telemt.sh`:
```ini
[Unit]
Description=GoTelegram MTProxy (telemt engine)
Description=goTelegram Pro MTProxy (telemt engine)
After=network-online.target
Wants=network-online.target
@@ -448,12 +448,14 @@ switch_language ru|en
- токена нет: после SSH tunnel открывается `http://127.0.0.1:1984/`;
- write-запросы дополнительно требуют `X-GoTelegram-Admin: 1`, фронтенд добавляет его автоматически.
- язык панели читается из `config.json.language`, затем из `/opt/gotelegram/.language`, fallback `en`; `POST /api/settings/language` сохраняет RU/EN в общий конфиг, marker file и bot `.env`; `gotelegram-bot/i18n.py` использует тот же источник как default до per-user override;
- UI построен вкладками (`dashboard`, `traffic`, `keys`, `backups`, `logs`, `settings`), есть light/dark theme в `localStorage`;
- UI построен вкладками (`dashboard`, `traffic`, `keys`, `backups`, `logs`, `settings`), есть иконки меню, графический overview-блок, light/dark theme в `localStorage` и promo-modal раз в 24 часа через `localStorage`;
- `/api/overview` отдаёт `stats_status`, `admin_bind` и `site_status`; `/api/site/check` проверяет `https://config.domain/` и считает OK только HTTP 200; `/api/stats/collect` делает разовый сбор, `/api/stats/repair` устанавливает/перезапускает `gotelegram-stats`.
Функции: overview, проверка сайта на HTTP 200, service status/restart, чтение/запись `[access.users]`, генерация proxy links, traffic history из `/opt/gotelegram/stats_history.csv`, current stats из `/run/gotelegram/stats_current.json`, список/создание backup, структурированные journal logs (`service`, `ok`, `exit_code`, `line_count`, `text`).
Функции: overview, проверка сайта на HTTP 200, service status/restart, чтение/запись `[access.users]`, enable/disable ключей через `/api/users/<name>/enabled`, генерация proxy links, traffic history из `/opt/gotelegram/stats_history.csv`, current stats из `/run/gotelegram/stats_current.json`, список/создание backup, структурированные journal logs (`service`, `ok`, `exit_code`, `line_count`, `text`).
`install_admin_web` вызывается при установке Telegram-бота. `auto_install_admin_web_if_possible` подхватывает админку после bootstrap/update, если Python уже установлен и файлы отличаются. При установке админки скрипт пытается установить/перезапустить `gotelegram-stats`; если это не удалось, оператор может нажать Repair stats в Traffic. Backup v1.3 сохраняет `admin_web/server.py` и `admin_web/static/`, restore возвращает их, удаляет legacy `admin_web/token` и пробует перезапустить `gotelegram-admin`.
Отключённые ключи хранятся в `/opt/gotelegram/disabled_users.json`: active keys остаются в `/etc/telemt/config.toml` под `[access.users]`, disabled keys удаляются из active block и могут быть возвращены обратно без потери secret. `main` защищён от удаления и отключения.
`install_admin_web` вызывается при установке Telegram-бота. `auto_install_admin_web_if_possible` подхватывает админку после bootstrap/update, если Python уже установлен и файлы отличаются. При установке админки скрипт пытается установить/перезапустить `gotelegram-stats`; если это не удалось, оператор может нажать Repair stats в Traffic. Backup v1.4 сохраняет `admin_web/server.py`, `admin_web/static/` и `disabled_users.json`, restore возвращает их, удаляет legacy `admin_web/token` и пробует перезапустить `gotelegram-admin`.
### 13.2 Upgrade migration (v2.5.0)
@@ -629,7 +631,7 @@ with socket.create_connection(("95.163.176.222", 443), timeout=5) as s:
## 17. Changelog
- **2.5.0 (2026-04-24)** — крупный maintenance pass в ветке `codex`: единая версия `2.5.0` в runtime и документации; удалён дефолтный PAT из `bootstrap.sh` (токен теперь только через `GOTELEGRAM_PAT`); `generate_telemt_toml` добавляет `[server.api]` на `127.0.0.1:9091` и metrics на `127.0.0.1:9090`, что нужно для управления пользователями и статистики; Telegram-бот получил меню `🔑 Keys` для `[access.users]` (добавить/удалить/показать ссылку/runtime info); добавлена локальная web-админка `gotelegram-admin` на `127.0.0.1:1984` с SSH-tunnel инструкцией в боте без отдельного web-admin токена, вкладочной UI-навигацией, i18n от языка установки, ручным переключателем RU/EN, site check на HTTP 200, structured journal logs, light/dark theme, адаптивом и stats repair endpoint; исправлено чтение traffic CSV в боте (header больше не ломает parsing); бот сам делает `stats_collect` перед показом статистики; `iptables` добавлен в optional deps и stats collector пытается установить его; CLI-смена шаблона теперь обновляет `config.json.template_id`, чтобы бот не показывал первый установленный шаблон; backup/restore версии `1.3` сохраняет bot `.env`, bot lang files, web-admin server/static, custom templates, templates catalog, stats history и полноценную структуру Let's Encrypt (`live/archive/renewal`) для переезда на новый сервер; добавлен безопасный детект 3x-ui/Xray на 443 и генерируется `/opt/gotelegram/shared-443-3xui.md` с объяснением shared-443 ограничений.
- **2.5.0 (2026-04-24)** — крупный maintenance pass в ветке `codex`: единая версия `2.5.0` в runtime и документации; удалён дефолтный PAT из `bootstrap.sh` (токен теперь только через `GOTELEGRAM_PAT`); `generate_telemt_toml` добавляет `[server.api]` на `127.0.0.1:9091` и metrics на `127.0.0.1:9090`, что нужно для управления пользователями и статистики; Telegram-бот получил меню `🔑 Keys` для `[access.users]` (добавить/отключить/включить/удалить/показать ссылку/runtime info); добавлена локальная web-админка goTelegram Pro `gotelegram-admin` на `127.0.0.1:1984` с SSH-tunnel инструкцией в боте без отдельного web-admin токена, вкладочной UI-навигацией, иконками, графическим overview, promo-modal раз в 24 часа, i18n от языка установки, ручным переключателем RU/EN, site check на HTTP 200, structured journal logs, light/dark theme, адаптивом и stats repair endpoint; исправлено чтение traffic CSV в боте (header больше не ломает parsing); бот сам делает `stats_collect` перед показом статистики; `iptables` добавлен в optional deps и stats collector пытается установить его; CLI-смена шаблона теперь обновляет `config.json.template_id`, чтобы бот не показывал первый установленный шаблон; backup/restore версии `1.4` сохраняет bot `.env`, bot lang files, disabled user keys, web-admin server/static, custom templates, templates catalog, stats history и полноценную структуру Let's Encrypt (`live/archive/renewal`) для переезда на новый сервер; добавлен безопасный детект 3x-ui/Xray на 443 и генерируется `/opt/gotelegram/shared-443-3xui.md` с объяснением shared-443 ограничений.
- **2.4.6 (2026-04-10)** — universal `apt_lock_wait` helper: ожидание dpkg/apt lock при unattended-upgrades, исправляет установку nginx/certbot/python на свежих VPS.
- **2.4.3 (2026-04-10)** — iter3-фикс: `bot_action_dispatch` оборачивается во `flock -w 30` на `/var/lock/gotelegram-bot-action.lock`. Обнаружена гонка: параллельные `change-lite-domain` получали `"no secret in config"`, потому что один процесс читал `config.json`, пока другой делал `jq ... > tmp && mv`. `util-linux` (содержит `flock`) добавлен в `critical` deps, `check_deps_present` и маппинги `apt_pkg_for_cmd`/`dnf_pkg_for_cmd`.
- **2.4.2 (2026-04-10)** — реализация non-interactive `bot_action_*` в install.sh (change-template + change-lite-domain с JSON-ответом). bot.py подключает `run_bot_action()` и делает реальную работу вместо stub'ов. Критфиксы: (a) `safe_edit_message` принимает `disable_web_page_preview` (иначе TypeError в success-пути cb_pro_confirm); (b) чтение/запись `config['template_id']` вместо `config['template']` (save_gotelegram_config всегда писал `template_id`, бот смотрел не туда); (c) `bot_update_config_field` использует shell `date -Iseconds` вместо `jq now|todate` (jq 1.5 совместимость для Debian 10); (d) `asyncio.Lock _BOT_ACTION_LOCK` сериализует callback'и в процессе бота; (e) валидация `_TPL_ID_RE`/`_DOMAIN_RE` до subprocess. Полный аудит и автоустановка зависимостей: `ensure_deps` разделяет critical/optional, дедуплицирует пакеты, re-verify после install; `apt_pkg_for_cmd` и `dnf_pkg_for_cmd` мапят команды на пакеты (dig→dnsutils/bind-utils, xxd→xxd/vim-common, flock→util-linux). `check_deps_present` — быстрый чек без `apt-get update`.

View File

@@ -1,4 +1,4 @@
# GoTelegram Pro — руководство пользователя
# goTelegram Pro — руководство пользователя
**Версия:** 2.5.0
**Репозиторий:** `anten-ka/gotelegram_pro`
@@ -8,7 +8,7 @@
## 1. Что это такое
GoTelegram Pro — это готовый менеджер прокси-сервера MTProxy для Telegram. Он делает три вещи, которые иначе пришлось бы собирать вручную:
goTelegram Pro — это готовый менеджер прокси-сервера MTProxy для Telegram. Он делает три вещи, которые иначе пришлось бы собирать вручную:
1. Ставит и настраивает ядро **telemt** (это современный Rust-порт mtproto-proxy с fake-TLS маскировкой).
2. Запускает рядом обычный HTTPS-сайт на настоящем домене, так что провайдеру со стороны всё выглядит как посещение безобидного лендинга — а на самом деле в том же соединении ходит Telegram-трафик. Это называется «stealth» или «Pro-режим».
@@ -125,7 +125,7 @@ CLI и бот переведены на русский и английский.
## 7. Бекап и восстановление
Пункт 8 делает один файл `.tar.gz` со всем, что нужно: `/etc/telemt/config.toml`, `/opt/gotelegram/config.json`, данные nginx-сайта, сертификаты, состояние Telegram-бота и локальной web-админки. По умолчанию в `/opt/gotelegram/backups/`.
Пункт 8 делает один файл `.tar.gz` со всем, что нужно: `/etc/telemt/config.toml`, `/opt/gotelegram/config.json`, `/opt/gotelegram/disabled_users.json`, данные nginx-сайта, сертификаты, состояние Telegram-бота и локальной web-админки. По умолчанию в `/opt/gotelegram/backups/`.
Пункт 9 принимает такой архив и восстанавливает всё обратно (конфиг, сервис, ссылка — всё то же, что было).
@@ -145,7 +145,9 @@ CLI и бот переведены на русский и английский.
- есть светлая/тёмная тема, вкладки и адаптивная вёрстка под desktop/mobile;
- Telegram-бот показывает инструкцию для Termius и обычную команду `ssh -L 1984:127.0.0.1:1984 root@SERVER`.
В админке есть dashboard, проверка сайта `https://домен/` на HTTP 200, статус сервисов, управление `[access.users]`, генерация ссылок, SVG-график traffic history, кнопка восстановления сборщика статистики, список бекапов и просмотр логов с количеством строк и статусом `journalctl`.
В админке есть dashboard, проверка сайта `https://домен/` на HTTP 200, статус сервисов, управление ключами `[access.users]` с добавлением, удалением и отключением через switch, генерация ссылок, SVG-график traffic history, кнопка восстановления сборщика статистики, список бекапов и просмотр логов с количеством строк и статусом `journalctl`.
Отключённые ключи убираются из активного telemt-конфига и сохраняются в `/opt/gotelegram/disabled_users.json`, поэтому их можно включить обратно без потери secret. Основной ключ `main` защищён от удаления и отключения.
---
@@ -154,7 +156,7 @@ CLI и бот переведены на русский и английский.
Два типа обновлений:
- **Обновление ядра telemt** (пункт 10) — тянет свежий бинарник с GitHub Releases `telemt/telemt`, сохраняет старый в `.bak`, перезапускает сервис. Конфиг остаётся как был.
- **Обновление самого GoTelegram** (пункт 1) — переустанавливает скрипты и lib/. Файл `config.json` не трогается, ключ и домен не меняются.
- **Обновление самого goTelegram Pro** (пункт 1) — переустанавливает скрипты и lib/. Файл `config.json` не трогается, ключ и домен не меняются.
Bootstrap.sh умеет сам обновлять всё, если запустить его повторно.
@@ -214,7 +216,7 @@ A: Сам MTProxy — да, это публичная технология из
| Скрипты (`install.sh`, `lib/`) | `/opt/gotelegram/` |
| Симлинк запуска | `/usr/local/bin/gotelegram` |
| Конфиг telemt | `/etc/telemt/config.toml` |
| Конфиг GoTelegram (JSON) | `/opt/gotelegram/config.json` |
| Конфиг goTelegram Pro (JSON) | `/opt/gotelegram/config.json` |
| Бинарник telemt | `/usr/local/bin/telemt` |
| Systemd юнит | `/etc/systemd/system/telemt.service` |
| Бот | `/opt/gotelegram-bot/` |
@@ -222,7 +224,7 @@ A: Сам MTProxy — да, это публичная технология из
| Сайт (Pro-режим) | `/var/www/gotelegram-site/` |
| nginx site | `/etc/nginx/sites-available/gotelegram` |
| Бекапы | `/opt/gotelegram/backups/` |
| Лог GoTelegram | `/var/log/gotelegram.log` |
| Лог goTelegram Pro | `/var/log/gotelegram.log` |
| Логи telemt | `journalctl -u telemt` |
| Логи бота | `journalctl -u gotelegram-bot` |
@@ -238,7 +240,7 @@ A: Сам MTProxy — да, это публичная технология из
## Changelog (коротко)
- **2.5.0** — единая версия по коду и документации; удалён дефолтный PAT из `bootstrap.sh`; исправлена статистика в боте (CSV header больше не ломает чтение истории, бот сам обновляет snapshot); CLI-смена шаблона теперь обновляет `config.json.template_id`, поэтому бот показывает текущий шаблон; telemt TOML включает локальный API `127.0.0.1:9091` и metrics `127.0.0.1:9090`; добавлено меню Telegram-бота для отдельных ключей пользователей (`[access.users]`): список, добавление, удаление, ссылка и runtime/API-информация; добавлена локальная web-админка на `127.0.0.1:1984` под SSH tunnel без отдельного токена, с вкладками, i18n от языка установки, ручным переключателем RU/EN, проверкой сайта на HTTP 200, тёмной темой, адаптивом и repair-кнопкой для статистики; backup/restore сохраняет bot `.env`, языки бота, web-admin server/static, custom templates, stats history и структуру Let's Encrypt для переезда на новый VPS; добавлен безопасный детект 3x-ui/Xray на 443 с предупреждением и заметкой по shared-443.
- **2.5.0** — единая версия по коду и документации; удалён дефолтный PAT из `bootstrap.sh`; исправлена статистика в боте (CSV header больше не ломает чтение истории, бот сам обновляет snapshot); CLI-смена шаблона теперь обновляет `config.json.template_id`, поэтому бот показывает текущий шаблон; telemt TOML включает локальный API `127.0.0.1:9091` и metrics на `127.0.0.1:9090`; добавлено меню Telegram-бота для отдельных ключей пользователей (`[access.users]`): список, добавление, отключение/включение, удаление, ссылка и runtime/API-информация; добавлена локальная web-админка goTelegram Pro на `127.0.0.1:1984` под SSH tunnel без отдельного токена, с вкладками, иконками, promo-разделом раз в 24 часа, i18n от языка установки, ручным переключателем RU/EN, проверкой сайта на HTTP 200, тёмной темой, адаптивом и repair-кнопкой для статистики; backup/restore сохраняет bot `.env`, языки бота, отключённые ключи, web-admin server/static, custom templates, stats history и структуру Let's Encrypt для переезда на новый VPS; добавлен безопасный детект 3x-ui/Xray на 443 с предупреждением и заметкой по shared-443.
- **2.4.6** — ожидание apt/dpkg lock на свежих Ubuntu/Debian, чтобы установка nginx/certbot/Python не падала во время unattended-upgrades.
- **2.4.3** — фикс гонки в `bot_action_dispatch`: параллельные вызовы `change-lite-domain`/`change-template` (например, два пользователя бота одновременно) могли получить ошибку «no secret in config», если один процесс читал `config.json` в момент, когда другой его перезаписывал через `jq`. Теперь диспетчер оборачивается в `flock(1)` с таймаутом 30 с; `util-linux` (содержит `flock`) добавлен в критические зависимости.
- **2.4.2** — смена шаблона и домена маскировки **прямо из Telegram-бота** без SSH. Раньше эти пункты меню показывали сообщение «сделай через CLI», теперь бот вызывает `install.sh --action=change-template --json` / `--action=change-lite-domain --json` и разбирает ответ. Плюс: безопасный `safe_edit_message` принимает `disable_web_page_preview`; поле статуса шаблона наконец-то отображается (раньше читалось не из того ключа JSON); полный аудит и автоустановка системных зависимостей при первом запуске (`curl jq openssl git xxd tar dig flock` + опциональные `qrencode bc`); `asyncio.Lock` в боте сериализует параллельные callback'и; валидация tpl\_id (`[A-Za-z0-9_-]{1,64}`) и домена до subprocess.

View File

@@ -1,6 +1,6 @@
#!/usr/bin/env python3
"""
GoTelegram local web admin.
goTelegram Pro local web admin.
The service is intentionally bound to 127.0.0.1:1984. Operators reach it
through an SSH tunnel; it must never be exposed directly on the public network.
@@ -37,6 +37,7 @@ CURRENT_STATS = Path(os.getenv("GOTELEGRAM_STATS_CURRENT", "/run/gotelegram/stat
BACKUP_DIR = Path(os.getenv("GOTELEGRAM_BACKUP_DIR", "/opt/gotelegram/backups"))
INSTALL_DIR = Path(os.getenv("GOTELEGRAM_DIR", "/opt/gotelegram"))
BOT_DIR = Path(os.getenv("GOTELEGRAM_BOT_DIR", "/opt/gotelegram-bot"))
DISABLED_USERS_FILE = Path(os.getenv("GOTELEGRAM_DISABLED_USERS", "/opt/gotelegram/disabled_users.json"))
HOST = os.getenv("GOTELEGRAM_ADMIN_HOST", "127.0.0.1")
PORT = int(os.getenv("GOTELEGRAM_ADMIN_PORT", "1984"))
@@ -152,6 +153,44 @@ def read_telemt_users() -> dict[str, str]:
return users
def read_disabled_users() -> dict[str, str]:
raw = load_json(DISABLED_USERS_FILE, {}) or {}
if not isinstance(raw, dict):
return {}
users = raw.get("users") if isinstance(raw.get("users"), dict) else raw
if not isinstance(users, dict):
return {}
clean: dict[str, str] = {}
for name, secret in users.items():
if name in {"version", "updated_at"}:
continue
name_s = str(name).strip()
secret_s = str(secret or "").strip()
if USER_RE.match(name_s) and secret_s:
clean[name_s] = secret_s
return clean
def write_disabled_users(users: dict[str, str]) -> None:
payload = {
"version": 1,
"updated_at": utc_now(),
"users": {name: users[name] for name in sorted(users)},
}
save_json(DISABLED_USERS_FILE, payload)
def read_user_records() -> dict[str, dict[str, Any]]:
active = read_telemt_users()
disabled = read_disabled_users()
records: dict[str, dict[str, Any]] = {}
for name, secret in disabled.items():
records[name] = {"secret": secret, "enabled": False}
for name, secret in active.items():
records[name] = {"secret": secret, "enabled": True}
return records
def _ordered_user_lines(users: dict[str, str]) -> list[str]:
names = []
if "main" in users:
@@ -462,14 +501,15 @@ def read_log_payload(service: str) -> dict[str, Any]:
}
def user_payload(name: str, secret: str, include_runtime: bool = False) -> dict[str, Any]:
def user_payload(name: str, secret: str, enabled: bool = True, include_runtime: bool = False) -> dict[str, Any]:
item: dict[str, Any] = {
"name": name,
"secret": secret,
"link": proxy_link(secret),
"main": name == "main",
"enabled": bool(enabled),
}
if include_runtime:
if include_runtime and enabled:
item["runtime"] = telemt_api(f"/v1/users/{urllib.parse.quote(name, safe='')}")
return item
@@ -477,7 +517,7 @@ def user_payload(name: str, secret: str, include_runtime: bool = False) -> dict[
def overview_payload() -> dict[str, Any]:
config = load_json(GOTELEGRAM_CONFIG, {}) or {}
language = read_language(config)
users = read_telemt_users()
users = read_user_records()
current = load_json(CURRENT_STATS, {}) or {}
history = load_stats_history()
summary = telemt_api("/v1/stats/summary")
@@ -506,7 +546,7 @@ def overview_payload() -> dict[str, Any]:
class AdminHandler(BaseHTTPRequestHandler):
server_version = "GoTelegramAdmin/2.5.0"
server_version = "goTelegramProAdmin/2.5.0"
def log_message(self, fmt: str, *args: Any) -> None:
print("%s - %s" % (self.address_string(), fmt % args))
@@ -542,15 +582,20 @@ class AdminHandler(BaseHTTPRequestHandler):
if path == "/api/overview":
self.send_json({"ok": True, "data": overview_payload()})
elif path == "/api/users":
users = read_telemt_users()
self.send_json({"ok": True, "data": [user_payload(k, v) for k, v in sorted(users.items())]})
users = read_user_records()
items = []
for name in sorted(users, key=lambda item: (item != "main", item)):
record = users[name]
items.append(user_payload(name, record["secret"], record["enabled"]))
self.send_json({"ok": True, "data": items})
elif path.startswith("/api/users/"):
name = urllib.parse.unquote(path[len("/api/users/"):])
users = read_telemt_users()
users = read_user_records()
if name not in users:
self.send_error_json(404, "user not found")
return
self.send_json({"ok": True, "data": user_payload(name, users[name], include_runtime=True)})
record = users[name]
self.send_json({"ok": True, "data": user_payload(name, record["secret"], record["enabled"], include_runtime=True)})
elif path == "/api/backups":
self.send_json({"ok": True, "data": list_backups()})
elif path == "/api/stats":
@@ -586,10 +631,11 @@ class AdminHandler(BaseHTTPRequestHandler):
if not USER_RE.match(name):
self.send_error_json(400, "invalid user name")
return
users = read_telemt_users()
if name in users:
records = read_user_records()
if name in records:
self.send_error_json(409, "user already exists")
return
users = read_telemt_users()
seed = f"{name}:{time.time()}:{secrets.token_hex(32)}".encode()
secret = hashlib.sha256(seed).hexdigest()[:32]
users[name] = secret
@@ -599,7 +645,37 @@ class AdminHandler(BaseHTTPRequestHandler):
self.send_error_json(500, f"failed to save config: {exc}")
return
restarted = restart_service("telemt")
self.send_json({"ok": True, "data": user_payload(name, secret), "restarted": restarted})
self.send_json({"ok": True, "data": user_payload(name, secret, True), "restarted": restarted})
elif path.startswith("/api/users/") and path.endswith("/enabled"):
name = urllib.parse.unquote(path[len("/api/users/"):-len("/enabled")])
if name == "main":
self.send_error_json(400, "main user cannot be disabled")
return
enabled = bool(body.get("enabled"))
active = read_telemt_users()
disabled = read_disabled_users()
records = read_user_records()
if name not in records:
self.send_error_json(404, "user not found")
return
if enabled:
secret = disabled.pop(name, records[name]["secret"])
active[name] = secret
else:
secret = active.pop(name, records[name]["secret"])
disabled[name] = secret
try:
if enabled:
write_telemt_users(active)
write_disabled_users(disabled)
else:
write_disabled_users(disabled)
write_telemt_users(active)
except Exception as exc:
self.send_error_json(500, f"failed to save config: {exc}")
return
restarted = restart_service("telemt")
self.send_json({"ok": True, "data": user_payload(name, secret, enabled), "restarted": restarted})
elif path == "/api/backups":
ok, result = create_backup()
self.send_json({"ok": ok, "data": {"path": result, "backups": list_backups()}}, 200 if ok else 500)
@@ -640,13 +716,17 @@ class AdminHandler(BaseHTTPRequestHandler):
if name == "main":
self.send_error_json(400, "main user cannot be deleted")
return
users = read_telemt_users()
if name not in users:
active = read_telemt_users()
disabled = read_disabled_users()
records = read_user_records()
if name not in records:
self.send_error_json(404, "user not found")
return
users.pop(name, None)
active.pop(name, None)
disabled.pop(name, None)
try:
write_telemt_users(users)
write_telemt_users(active)
write_disabled_users(disabled)
except Exception as exc:
self.send_error_json(500, f"failed to save config: {exc}")
return
@@ -702,7 +782,7 @@ def main() -> None:
if not STATIC_DIR.exists():
raise SystemExit(f"static dir not found: {STATIC_DIR}")
httpd = ThreadingHTTPServer((HOST, PORT), AdminHandler)
print(f"GoTelegram admin listening on http://{HOST}:{PORT}")
print(f"goTelegram Pro admin listening on http://{HOST}:{PORT}")
httpd.serve_forever()

View File

@@ -43,6 +43,7 @@ const i18n = {
collectStats: "Collect",
repairStats: "Repair stats",
tableTime: "Time",
tableStatus: "Status",
tableProxyDelta: "Proxy delta",
tableSiteDelta: "Site delta",
tableProxyTotal: "Proxy total",
@@ -56,6 +57,10 @@ const i18n = {
copyLink: "Copy link",
copySecret: "Copy secret",
delete: "Delete",
enabled: "Enabled",
disabled: "Disabled",
disableKey: "Disable key",
enableKey: "Enable key",
main: "main",
createBackup: "Create backup",
loadLogs: "Load",
@@ -121,6 +126,15 @@ const i18n = {
logsLines: "lines",
logsNoData: "No log lines",
languageSaved: "Language saved",
keyEnabled: "Key enabled",
keyDisabled: "Key disabled",
visualTitle: "443 shared edge",
visualText: "Website, MTProxy and local admin status in one operational view.",
promoEyebrow: "Promo",
promoTitle: "Support goTelegram Pro",
promoHosting1: "Hosting #1",
promoHosting2: "Hosting #2",
promoTips: "Tips",
pageDashboardTitle: "Dashboard",
pageDashboardKicker: "Local Admin",
pageTrafficTitle: "Traffic",
@@ -175,6 +189,7 @@ const i18n = {
collectStats: "Собрать",
repairStats: "Починить статистику",
tableTime: "Время",
tableStatus: "Статус",
tableProxyDelta: "Proxy delta",
tableSiteDelta: "Site delta",
tableProxyTotal: "Proxy всего",
@@ -188,6 +203,10 @@ const i18n = {
copyLink: "Копировать ссылку",
copySecret: "Копировать secret",
delete: "Удалить",
enabled: "Включён",
disabled: "Отключён",
disableKey: "Отключить ключ",
enableKey: "Включить ключ",
main: "основной",
createBackup: "Создать бекап",
loadLogs: "Загрузить",
@@ -253,6 +272,15 @@ const i18n = {
logsLines: "строк",
logsNoData: "Строк логов нет",
languageSaved: "Язык сохранён",
keyEnabled: "Ключ включён",
keyDisabled: "Ключ отключён",
visualTitle: "Единый 443 edge",
visualText: "Сайт, MTProxy и локальная админка в одном рабочем обзоре.",
promoEyebrow: "Промо",
promoTitle: "Поддержать goTelegram Pro",
promoHosting1: "Хостинг #1",
promoHosting2: "Хостинг #2",
promoTips: "Чаевые",
pageDashboardTitle: "Обзор",
pageDashboardKicker: "Локальная админка",
pageTrafficTitle: "Трафик",
@@ -356,6 +384,8 @@ function applyI18n() {
$("#languageSelect").value = state.lang;
$("#settingsLanguage").textContent = state.lang === "ru" ? "Русский" : "English";
$("#settingsTheme").textContent = state.theme === "dark" ? t("darkTheme") : t("lightTheme");
$("#visualTitle").textContent = t("visualTitle");
$("#visualText").textContent = t("visualText");
updatePageTitle();
}
@@ -607,16 +637,25 @@ function renderHistoryTable(rows) {
function renderUsers() {
const tbody = $("#usersTable");
if (!state.users.length) {
tbody.innerHTML = `<tr><td colspan="4" class="empty-cell">${escapeHtml(t("noKeys"))}</td></tr>`;
tbody.innerHTML = `<tr><td colspan="5" class="empty-cell">${escapeHtml(t("noKeys"))}</td></tr>`;
return;
}
tbody.innerHTML = state.users.map((user) => `
<tr>
<tr class="${user.enabled ? "" : "disabled-row"}">
<td data-label="${escapeAttr(t("tableUser"))}">
<strong>${escapeHtml(user.name)}</strong>${user.main ? ` <small>${escapeHtml(t("main"))}</small>` : ""}
</td>
<td data-label="${escapeAttr(t("tableStatus"))}">
<div class="status-control">
<label class="switch" title="${escapeAttr(user.main ? t("main") : (user.enabled ? t("disableKey") : t("enableKey")))}">
<input type="checkbox" data-toggle-user="${escapeAttr(user.name)}" ${user.enabled ? "checked" : ""} ${user.main ? "disabled" : ""}>
<span></span>
</label>
<strong class="${user.enabled ? "state-on" : "state-off"}">${escapeHtml(user.enabled ? t("enabled") : t("disabled"))}</strong>
</div>
</td>
<td data-label="${escapeAttr(t("tableSecret"))}"><code title="${escapeAttr(user.secret)}">${escapeHtml(user.secret)}</code></td>
<td data-label="${escapeAttr(t("tableLink"))}"><button class="soft" data-copy="${escapeAttr(user.link)}">${escapeHtml(t("copyLink"))}</button></td>
<td data-label="${escapeAttr(t("tableLink"))}"><button class="soft" data-copy="${escapeAttr(user.link)}" ${user.enabled ? "" : "disabled"}>${escapeHtml(t("copyLink"))}</button></td>
<td data-label="${escapeAttr(t("tableActions"))}" class="actions">
<button class="soft" data-copy="${escapeAttr(user.secret)}">${escapeHtml(t("copySecret"))}</button>
<button class="danger" data-delete="${escapeAttr(user.name)}" ${user.main ? "disabled" : ""}>${escapeHtml(t("delete"))}</button>
@@ -708,6 +747,17 @@ async function deleteUser(name) {
await refreshAll();
}
async function setUserEnabled(name, enabled) {
const data = await api(`/api/users/${encodeURIComponent(name)}/enabled`, {
method: "POST",
body: JSON.stringify({ enabled }),
});
const message = data.enabled ? t("keyEnabled") : t("keyDisabled");
addEvent(message, name);
toast(message);
await refreshAll();
}
async function createBackup() {
const btn = $("#createBackupBtn");
btn.disabled = true;
@@ -802,6 +852,15 @@ async function copyText(value) {
}
}
function maybeShowPromo() {
const key = "gotelegram-promo-last";
const now = Math.floor(Date.now() / 1000);
const last = Number(localStorage.getItem(key) || 0);
if (now - last < 86400) return;
localStorage.setItem(key, String(now));
$("#promoModal").hidden = false;
}
document.addEventListener("click", async (eventObj) => {
const nav = eventObj.target.closest("[data-nav]");
if (nav) {
@@ -827,6 +886,17 @@ document.addEventListener("click", async (eventObj) => {
}
});
document.addEventListener("change", (eventObj) => {
const input = eventObj.target.closest("[data-toggle-user]");
if (!input) return;
input.disabled = true;
setUserEnabled(input.dataset.toggleUser, input.checked).catch((err) => {
input.checked = !input.checked;
input.disabled = false;
toast(err.message);
});
});
$("#addUserForm").addEventListener("submit", (eventObj) => {
eventObj.preventDefault();
const input = $("#userName");
@@ -841,6 +911,9 @@ $("#addUserForm").addEventListener("submit", (eventObj) => {
$("#refreshBtn").addEventListener("click", refreshAll);
$("#languageSelect").addEventListener("change", (eventObj) => setLanguage(eventObj.target.value));
$("#promoClose").addEventListener("click", () => {
$("#promoModal").hidden = true;
});
$("#createBackupBtn").addEventListener("click", createBackup);
$("#loadLogsBtn").addEventListener("click", loadLogs);
$("#repairStatsBtn").addEventListener("click", repairStats);
@@ -852,3 +925,4 @@ setTheme(state.theme);
renderEvents();
refreshAll();
loadLogs();
maybeShowPromo();

View File

@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>GoTelegram Admin</title>
<title>goTelegram Pro Admin</title>
<script>
(function () {
var stored = localStorage.getItem("gotelegram-theme");
@@ -11,7 +11,7 @@
document.documentElement.dataset.theme = theme;
}());
</script>
<link rel="stylesheet" href="/styles.css?v=2.5.0-admin4">
<link rel="stylesheet" href="/styles.css?v=2.5.0-admin5">
</head>
<body>
<div class="app-shell">
@@ -19,18 +19,18 @@
<div class="brand">
<div class="brand-mark">GT</div>
<div>
<strong>GoTelegram</strong>
<strong>goTelegram Pro</strong>
<span data-i18n="brandSubtitle">Local Admin</span>
</div>
</div>
<nav class="nav-tabs" aria-label="Admin sections">
<button type="button" class="nav-item active" data-nav="dashboard" data-i18n="navDashboard">Dashboard</button>
<button type="button" class="nav-item" data-nav="traffic" data-i18n="navTraffic">Traffic</button>
<button type="button" class="nav-item" data-nav="keys" data-i18n="navKeys">Keys</button>
<button type="button" class="nav-item" data-nav="backups" data-i18n="navBackups">Backups</button>
<button type="button" class="nav-item" data-nav="logs" data-i18n="navLogs">Logs</button>
<button type="button" class="nav-item" data-nav="settings" data-i18n="navSettings">Settings</button>
<button type="button" class="nav-item active" data-nav="dashboard"><span class="nav-icon"></span><span data-i18n="navDashboard">Dashboard</span></button>
<button type="button" class="nav-item" data-nav="traffic"><span class="nav-icon"></span><span data-i18n="navTraffic">Traffic</span></button>
<button type="button" class="nav-item" data-nav="keys"><span class="nav-icon"></span><span data-i18n="navKeys">Keys</span></button>
<button type="button" class="nav-item" data-nav="backups"><span class="nav-icon"></span><span data-i18n="navBackups">Backups</span></button>
<button type="button" class="nav-item" data-nav="logs"><span class="nav-icon"></span><span data-i18n="navLogs">Logs</span></button>
<button type="button" class="nav-item" data-nav="settings"><span class="nav-icon"></span><span data-i18n="navSettings">Settings</span></button>
</nav>
<div class="sidebar-foot">
@@ -59,6 +59,19 @@
<main class="content">
<section class="page-panel active" data-page="dashboard">
<section class="visual-overview">
<div>
<p class="eyebrow">goTelegram Pro</p>
<h2 id="visualTitle">443 shared edge</h2>
<p id="visualText">Website, MTProxy and local admin status in one operational view.</p>
</div>
<div class="signal-map" aria-hidden="true">
<span class="node node-site">HTTPS</span>
<span class="node node-proxy">MTProto</span>
<span class="node node-admin">Admin</span>
</div>
</section>
<div class="metric-grid">
<article class="metric-card accent-blue">
<span data-i18n="metricMode">Mode</span>
@@ -169,6 +182,7 @@
<thead>
<tr>
<th data-i18n="tableUser">User</th>
<th data-i18n="tableStatus">Status</th>
<th data-i18n="tableSecret">Secret</th>
<th data-i18n="tableLink">Link</th>
<th data-i18n="tableActions">Actions</th>
@@ -269,6 +283,27 @@
</div>
<div id="toast" class="toast"></div>
<script src="/app.js?v=2.5.0-admin4" type="module"></script>
<div id="promoModal" class="promo-modal" hidden>
<div class="promo-card" role="dialog" aria-modal="true" aria-labelledby="promoTitle">
<button id="promoClose" class="icon-btn ghost" type="button" aria-label="Close">×</button>
<p class="eyebrow" data-i18n="promoEyebrow">Promo</p>
<h2 id="promoTitle" data-i18n="promoTitle">Support goTelegram Pro</h2>
<div class="promo-grid">
<a href="https://vk.cc/ct29NQ" target="_blank" rel="noreferrer">
<strong data-i18n="promoHosting1">Hosting #1</strong>
<span>OFF60 · antenka20 · antenka6</span>
</a>
<a href="https://vk.cc/cUxAhj" target="_blank" rel="noreferrer">
<strong data-i18n="promoHosting2">Hosting #2</strong>
<span>OFF60</span>
</a>
<a href="https://pay.cloudtips.ru/p/7410814f" target="_blank" rel="noreferrer">
<strong data-i18n="promoTips">Tips</strong>
<span>CloudTips</span>
</a>
</div>
</div>
</div>
<script src="/app.js?v=2.5.0-admin5" type="module"></script>
</body>
</html>

View File

@@ -140,6 +140,10 @@ h2 {
font-weight: 800;
}
.brand strong {
letter-spacing: 0;
}
.brand span {
display: block;
color: var(--sidebar-muted);
@@ -153,6 +157,9 @@ h2 {
.nav-item {
width: 100%;
display: flex;
align-items: center;
gap: 10px;
justify-content: flex-start;
text-align: left;
background: transparent;
@@ -160,6 +167,18 @@ h2 {
box-shadow: none;
}
.nav-icon {
display: inline-grid;
place-items: center;
flex: 0 0 28px;
width: 28px;
height: 28px;
border-radius: 8px;
background: rgba(255, 255, 255, .07);
color: var(--sidebar-text);
font-weight: 800;
}
.nav-item:hover,
.nav-item.active {
background: rgba(255, 255, 255, .08);
@@ -251,6 +270,79 @@ h2 {
gap: 18px;
}
.visual-overview {
position: relative;
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(260px, 420px);
gap: 20px;
min-height: 170px;
overflow: hidden;
border: 1px solid var(--line);
border-radius: 8px;
background:
linear-gradient(135deg, color-mix(in srgb, var(--green) 14%, transparent), transparent 42%),
linear-gradient(120deg, var(--panel), var(--panel-soft));
box-shadow: var(--shadow);
padding: 22px;
}
.visual-overview h2 {
margin-top: 4px;
font-size: clamp(24px, 3vw, 34px);
}
.visual-overview p:not(.eyebrow) {
max-width: 620px;
margin-top: 8px;
color: var(--muted);
}
.signal-map {
position: relative;
min-height: 128px;
border: 1px solid var(--line);
border-radius: 8px;
background:
linear-gradient(90deg, color-mix(in srgb, var(--line) 45%, transparent) 1px, transparent 1px),
linear-gradient(0deg, color-mix(in srgb, var(--line) 45%, transparent) 1px, transparent 1px);
background-size: 28px 28px;
}
.signal-map::before,
.signal-map::after {
content: "";
position: absolute;
inset: 50% 38px auto 38px;
height: 2px;
background: linear-gradient(90deg, var(--green), var(--blue), var(--violet));
}
.signal-map::after {
inset: 30px auto 30px 50%;
width: 2px;
height: auto;
}
.node {
position: absolute;
z-index: 1;
display: grid;
place-items: center;
min-width: 82px;
min-height: 38px;
border: 1px solid var(--line);
border-radius: 8px;
background: var(--panel);
color: var(--text);
font-size: 12px;
font-weight: 900;
box-shadow: 0 12px 28px rgba(15, 23, 42, .12);
}
.node-site { left: 24px; top: 18px; }
.node-proxy { right: 24px; top: 50%; transform: translateY(-50%); }
.node-admin { left: 50%; bottom: 18px; transform: translateX(-50%); }
.eyebrow {
color: var(--muted);
font-size: 12px;
@@ -549,6 +641,10 @@ tr:last-child td {
border-bottom: 0;
}
.disabled-row {
opacity: .72;
}
td code {
display: inline-block;
max-width: 320px;
@@ -567,6 +663,65 @@ td small {
gap: 8px;
}
.status-control {
display: inline-flex;
align-items: center;
gap: 10px;
}
.state-on { color: var(--green); }
.state-off { color: var(--amber); }
.switch {
position: relative;
display: inline-flex;
width: 46px;
height: 26px;
}
.switch input {
position: absolute;
opacity: 0;
width: 1px;
height: 1px;
}
.switch span {
position: absolute;
inset: 0;
border: 1px solid var(--line);
border-radius: 999px;
background: var(--panel-strong);
transition: background .16s ease, border-color .16s ease;
}
.switch span::before {
content: "";
position: absolute;
width: 20px;
height: 20px;
left: 2px;
top: 2px;
border-radius: 50%;
background: var(--panel);
box-shadow: 0 3px 9px rgba(15, 23, 42, .22);
transition: transform .16s ease;
}
.switch input:checked + span {
border-color: color-mix(in srgb, var(--green) 70%, var(--line));
background: color-mix(in srgb, var(--green) 64%, var(--panel));
}
.switch input:checked + span::before {
transform: translateX(20px);
}
.switch input:disabled + span {
opacity: .55;
cursor: not-allowed;
}
.empty-cell {
color: var(--muted);
text-align: center;
@@ -643,6 +798,64 @@ td small {
transform: translateY(0);
}
.promo-modal {
position: fixed;
inset: 0;
z-index: 30;
display: grid;
place-items: center;
padding: 18px;
background: rgba(5, 8, 16, .48);
}
.promo-modal[hidden] {
display: none;
}
.promo-card {
position: relative;
width: min(620px, 100%);
border: 1px solid var(--line);
border-radius: 8px;
background: var(--panel);
box-shadow: 0 22px 70px rgba(0, 0, 0, .28);
padding: 22px;
}
.promo-card .icon-btn {
position: absolute;
top: 14px;
right: 14px;
}
.promo-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 10px;
margin-top: 18px;
}
.promo-grid a {
display: grid;
gap: 6px;
min-height: 98px;
border: 1px solid var(--line);
border-radius: 8px;
background: var(--panel-soft);
color: var(--text);
padding: 14px;
text-decoration: none;
}
.promo-grid a:hover {
border-color: var(--green);
}
.promo-grid span {
color: var(--muted);
font-size: 12px;
}
@media (max-width: 1280px) {
.service-grid {
grid-template-columns: repeat(3, minmax(150px, 1fr));
@@ -681,6 +894,11 @@ td small {
}
@media (max-width: 720px) {
.visual-overview,
.promo-grid {
grid-template-columns: 1fr;
}
.topbar {
align-items: flex-start;
padding: 16px;

View File

@@ -1,4 +1,4 @@
# GoTelegram v2.5.0 Bot
# goTelegram Pro v2.5.0 Bot
Production-quality Telegram bot for managing MTProxy (telemt engine) on Linux servers.
@@ -67,7 +67,7 @@ For systemd service:
```bash
[Unit]
Description=GoTelegram Bot
Description=goTelegram Pro Bot
After=network.target
[Service]
@@ -146,4 +146,4 @@ code, stdout, stderr = await sh("command", "arg1", "arg2")
## License
GoTelegram v2.5.0 - Open source community project
goTelegram Pro v2.5.0 - Open source community project

View File

@@ -1,6 +1,6 @@
#!/usr/bin/env python3
"""
GoTelegram v2.5.0 Bot - MTProxy Management for Linux
goTelegram Pro v2.5.0 Bot - MTProxy Management for Linux
Manages telemt engine via Telegram interface with full CLI feature parity
Uses python-telegram-bot v21+
Supports EN/RU UI with per-user language preferences.
@@ -103,6 +103,7 @@ logger = logging.getLogger(__name__)
GOTELEGRAM_VERSION = "2.5.0"
GOTELEGRAM_CONFIG = "/opt/gotelegram/config.json"
DISABLED_USERS_FILE = "/opt/gotelegram/disabled_users.json"
TELEMT_CONFIG = "/etc/telemt/config.toml"
TELEMT_SERVICE = "telemt"
WEBSITE_ROOT = "/var/www/gotelegram-site"
@@ -113,6 +114,7 @@ INSTALL_SH = "/opt/gotelegram/install.sh"
PROMO_LINK_1 = "https://vk.cc/ct29NQ"
PROMO_LINK_2 = "https://vk.cc/cUxAhj"
TIP_LINK = "https://pay.cloudtips.ru/p/7410814f"
YOUTUBE_LINK = os.getenv("GOTELEGRAM_YOUTUBE_LINK", "").strip()
PROMO_STAMP_FILE = "/opt/gotelegram/.promo_bot_last_shown"
BOT_TOKEN = os.getenv("BOT_TOKEN")
@@ -1412,6 +1414,48 @@ def load_telemt_users() -> Dict[str, str]:
}
def load_disabled_users() -> Dict[str, str]:
raw = load_json(DISABLED_USERS_FILE) or {}
if not isinstance(raw, dict):
return {}
users = raw.get("users") if isinstance(raw.get("users"), dict) else raw
if not isinstance(users, dict):
return {}
clean: Dict[str, str] = {}
for name, secret in users.items():
if name in {"version", "updated_at"}:
continue
name_s = str(name).strip()
secret_s = str(secret or "").strip()
if _USER_NAME_RE.match(name_s) and secret_s:
clean[name_s] = secret_s
return clean
def save_disabled_users(users: Dict[str, str]) -> bool:
payload = {
"version": 1,
"updated_at": datetime.utcnow().isoformat() + "Z",
"users": {name: users[name] for name in sorted(users)},
}
ok = save_json(DISABLED_USERS_FILE, payload)
if ok:
try:
os.chmod(DISABLED_USERS_FILE, 0o600)
except OSError:
pass
return ok
def load_user_records() -> Dict[str, Dict[str, Any]]:
records: Dict[str, Dict[str, Any]] = {}
for name, secret in load_disabled_users().items():
records[name] = {"secret": secret, "enabled": False}
for name, secret in load_telemt_users().items():
records[name] = {"secret": secret, "enabled": True}
return records
def save_telemt_users(users: Dict[str, str]) -> bool:
"""Persist [access.users] while keeping the rest of the TOML structure."""
telemt_cfg = load_toml(TELEMT_CONFIG) or {}
@@ -1589,10 +1633,12 @@ async def cb_menu_share(update: Update, context: ContextTypes.DEFAULT_TYPE) -> N
# ============================================================================
def _users_keyboard(users: Dict[str, str], user_id: Optional[int]) -> InlineKeyboardMarkup:
def _users_keyboard(users: Dict[str, Dict[str, Any]], user_id: Optional[int]) -> InlineKeyboardMarkup:
rows = []
for name in sorted(users):
rows.append([InlineKeyboardButton(f"👤 {name}", callback_data=f"user_view_{name}")])
for name in sorted(users, key=lambda item: (item != "main", item)):
enabled = bool(users[name].get("enabled"))
icon = "🟢" if enabled else ""
rows.append([InlineKeyboardButton(f"{icon} {name}", callback_data=f"user_view_{name}")])
rows.append([InlineKeyboardButton(" Добавить ключ", callback_data="user_add")])
rows.append([
InlineKeyboardButton(_t(user_id, "btn_refresh"), callback_data="menu_users"),
@@ -1605,10 +1651,13 @@ async def cb_menu_users(update: Update, context: ContextTypes.DEFAULT_TYPE) -> N
query = update.callback_query
await query.answer()
user_id = _uid(update)
users = load_telemt_users()
users = load_user_records()
if users:
user_lines = "\n".join(f"• <code>{html.escape(name)}</code>" for name in sorted(users))
user_lines = "\n".join(
f"{'🟢' if users[name].get('enabled') else ''} <code>{html.escape(name)}</code>"
for name in sorted(users, key=lambda item: (item != "main", item))
)
else:
user_lines = "<i>Ключей пока нет</i>"
@@ -1635,9 +1684,9 @@ async def cb_menu_users(update: Update, context: ContextTypes.DEFAULT_TYPE) -> N
await safe_edit_message(query, text, reply_markup=_users_keyboard(users, user_id), parse_mode="HTML")
async def _user_detail_text(name: str, secret: str) -> str:
async def _user_detail_text(name: str, secret: str, enabled: bool = True) -> str:
link = await get_proxy_link_for_secret(secret)
api = await telemt_api_get(f"/v1/users/{quote(name, safe='')}")
api = await telemt_api_get(f"/v1/users/{quote(name, safe='')}") if enabled else None
details = ""
if api:
data = api.get("data", api)
@@ -1656,12 +1705,16 @@ async def _user_detail_text(name: str, secret: str) -> str:
else:
compact = json.dumps(data, ensure_ascii=False)[:600]
details = f"\n<pre>{html.escape(compact)}</pre>"
elif enabled:
details = "\n<i>Runtime API недоступен. Новые установки goTelegram Pro включают его автоматически.</i>"
else:
details = "\n<i>Runtime API недоступен. Новые установки GoTelegram включают его автоматически.</i>"
details = "\n<i>Ключ отключён и сейчас не принимается telemt.</i>"
link_line = html.escape(link) if link else "link unavailable"
status_line = "🟢 enabled" if enabled else "⏸ disabled"
return (
f"<b>👤 {html.escape(name)}</b>\n\n"
f"Status: <b>{status_line}</b>\n"
f"Secret: <code>{html.escape(secret)}</code>\n\n"
f"<b>Ссылка:</b>\n<code>{link_line}</code>\n"
f"{details}"
@@ -1673,23 +1726,31 @@ async def cb_user_view(update: Update, context: ContextTypes.DEFAULT_TYPE) -> No
await query.answer()
user_id = _uid(update)
name = query.data.removeprefix("user_view_")
users = load_telemt_users()
secret = users.get(name)
if not secret:
users = load_user_records()
record = users.get(name)
if not record:
await safe_edit_message(
query,
"❌ Пользователь не найден.",
reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton(_t(user_id, "btn_back"), callback_data="menu_users")]]),
)
return
enabled = bool(record.get("enabled"))
secret = str(record.get("secret", ""))
buttons = [
[InlineKeyboardButton("⏸ Отключить" if enabled else "▶️ Включить", callback_data=f"user_toggle_{name}")],
[InlineKeyboardButton("🗑 Удалить", callback_data=f"user_del_{name}")],
[InlineKeyboardButton(_t(user_id, "btn_back"), callback_data="menu_users")],
]
if name == "main":
buttons = [
[InlineKeyboardButton("🔒 Main key", callback_data=f"user_view_{name}")],
[InlineKeyboardButton(_t(user_id, "btn_back"), callback_data="menu_users")],
]
await safe_edit_message(
query,
await _user_detail_text(name, secret),
await _user_detail_text(name, secret, enabled),
reply_markup=InlineKeyboardMarkup(buttons),
parse_mode="HTML",
disable_web_page_preview=True,
@@ -1714,6 +1775,45 @@ async def cb_user_add(update: Update, context: ContextTypes.DEFAULT_TYPE) -> Non
)
async def cb_user_toggle(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
query = update.callback_query
await query.answer()
user_id = _uid(update)
name = query.data.removeprefix("user_toggle_")
if name == "main":
await query.answer("main нельзя отключить", show_alert=True)
return
active = load_telemt_users()
disabled = load_disabled_users()
records = load_user_records()
record = records.get(name)
if not record:
await query.answer("Ключ не найден", show_alert=True)
return
enabled = not bool(record.get("enabled"))
secret = str(record.get("secret", ""))
if enabled:
disabled.pop(name, None)
active[name] = secret
else:
active.pop(name, None)
disabled[name] = secret
if enabled:
saved = save_telemt_users(active) and save_disabled_users(disabled)
else:
saved = save_disabled_users(disabled) and save_telemt_users(active)
if not saved:
await safe_edit_message(query, "Не удалось сохранить состояние ключа")
return
await refresh_telemt_after_user_change()
await safe_edit_message(
query,
f"{'✅ Ключ включён' if enabled else '⏸ Ключ отключён'}: <code>{html.escape(name)}</code>",
reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton(_t(user_id, "btn_back"), callback_data=f"user_view_{name}")]]),
parse_mode="HTML",
)
async def cb_user_delete(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
query = update.callback_query
await query.answer()
@@ -1735,12 +1835,15 @@ async def cb_user_delete_confirm(update: Update, context: ContextTypes.DEFAULT_T
await query.answer()
user_id = _uid(update)
name = query.data.removeprefix("user_del_yes_")
users = load_telemt_users()
if name == "main" or name not in users:
active = load_telemt_users()
disabled = load_disabled_users()
records = load_user_records()
if name == "main" or name not in records:
await query.answer("Нельзя удалить этот ключ", show_alert=True)
return
users.pop(name, None)
if not save_telemt_users(users):
active.pop(name, None)
disabled.pop(name, None)
if not save_telemt_users(active) or not save_disabled_users(disabled):
await safe_edit_message(query, "Не удалось сохранить config.toml")
return
await refresh_telemt_after_user_change()
@@ -1757,10 +1860,11 @@ async def create_user_from_text(update: Update, context: ContextTypes.DEFAULT_TY
if not _USER_NAME_RE.match(name):
await update.message.reply_text("❌ Некорректное имя. Используйте латиницу, цифры, _ . - и до 48 символов.")
return
users = load_telemt_users()
if name in users:
records = load_user_records()
if name in records:
await update.message.reply_text("❌ Такой пользователь уже есть.")
return
users = load_telemt_users()
secret = hashlib.sha256(f"{name}:{time.time()}:{os.urandom(16).hex()}".encode()).hexdigest()[:32]
users[name] = secret
if not save_telemt_users(users):
@@ -1773,7 +1877,7 @@ async def create_user_from_text(update: Update, context: ContextTypes.DEFAULT_TY
f"Пользователь: <code>{html.escape(name)}</code>\n"
f"Secret: <code>{secret}</code>\n\n"
f"<code>{html.escape(link or '')}</code>",
reply_markup=_users_keyboard(load_telemt_users(), user_id),
reply_markup=_users_keyboard(load_user_records(), user_id),
parse_mode="HTML",
disable_web_page_preview=True,
)
@@ -2332,8 +2436,8 @@ async def cmd_deladmin(update: Update, context: ContextTypes.DEFAULT_TYPE) -> No
def get_promo_text() -> str:
"""Return promo text with 2 hosters + donate."""
return (
"""Return promo text with 2 hosters, optional YouTube link and donate."""
text = (
"<b>💰 Хостинг #1 — скидка до 60%</b>\n"
f"<a href='{PROMO_LINK_1}'>{PROMO_LINK_1}</a>\n\n"
"<b>Промокоды:</b>\n"
@@ -2349,6 +2453,13 @@ def get_promo_text() -> str:
"<b>☕ Донат / Чаевые</b>\n"
f"<a href='{TIP_LINK}'>{TIP_LINK}</a>"
)
if YOUTUBE_LINK:
text += (
"\n\n━━━━━━━━━━━━━━━━━━━━━━━━━\n\n"
"<b>▶ YouTube-канал</b>\n"
f"<a href='{html.escape(YOUTUBE_LINK)}'>{html.escape(YOUTUBE_LINK)}</a>"
)
return text
def should_show_promo_bot() -> bool:
@@ -2393,7 +2504,7 @@ async def cb_menu_credits(update: Update, context: ContextTypes.DEFAULT_TYPE) ->
text = (
f"<b> Credits & Acknowledgements</b>\n\n"
f"<b>GoTelegram v{GOTELEGRAM_VERSION}</b>\n\n"
f"<b>goTelegram Pro v{GOTELEGRAM_VERSION}</b>\n\n"
f"Built with love for the Telegram community\n\n"
f"<b>Special thanks to:</b>\n\n"
f"🙏 <b>telemt</b> - MTProxy engine\n"
@@ -2405,7 +2516,7 @@ async def cb_menu_credits(update: Update, context: ContextTypes.DEFAULT_TYPE) ->
f"🚀 <b>Start Bootstrap</b> - Bootstrap templates\n"
f" Professional design framework\n\n"
f"💬 <b>Community</b> - Your feedback & support\n\n"
f"<i>GoTelegram is open-source and community-driven</i>"
f"<i>goTelegram Pro is open-source and community-driven</i>"
)
keyboard = InlineKeyboardMarkup(
@@ -2425,7 +2536,7 @@ async def cb_menu_remove(update: Update, context: ContextTypes.DEFAULT_TYPE) ->
await query.answer()
text = (
"<b>⚠️ Remove GoTelegram</b>\n\n"
"<b>⚠️ Remove goTelegram Pro</b>\n\n"
"This will completely remove the installation.\n"
"Are you sure?"
)
@@ -2443,7 +2554,7 @@ async def cb_remove_confirm(update: Update, context: ContextTypes.DEFAULT_TYPE)
query = update.callback_query
await query.answer()
await safe_edit_message(query,"⏳ Removing GoTelegram...")
await safe_edit_message(query,"⏳ Removing goTelegram Pro...")
# Stop service
await sh("systemctl", "stop", TELEMT_SERVICE)
@@ -2452,7 +2563,7 @@ async def cb_remove_confirm(update: Update, context: ContextTypes.DEFAULT_TYPE)
for path in ["/opt/gotelegram", WEBSITE_ROOT]:
await sh("rm", "-rf", path)
text = "GoTelegram removed successfully"
text = "goTelegram Pro removed successfully"
keyboard = InlineKeyboardMarkup(
[[InlineKeyboardButton(_t(_uid(update), "btn_back"), callback_data="menu_main")]]
)
@@ -2617,6 +2728,8 @@ async def handle_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) ->
await cb_user_add(update, context)
elif data.startswith("user_view_"):
await cb_user_view(update, context)
elif data.startswith("user_toggle_"):
await cb_user_toggle(update, context)
elif data.startswith("user_del_yes_"):
await cb_user_delete_confirm(update, context)
elif data.startswith("user_del_"):
@@ -2663,7 +2776,7 @@ async def handle_text_message(update: Update, context: ContextTypes.DEFAULT_TYPE
if not ok:
await update.message.reply_text(_t(user_id, info), parse_mode="HTML")
return
# Success — record in GoTelegram config. Use "template_id" (canonical
# Success — record in goTelegram Pro config. Use "template_id" (canonical
# field name written by install.sh/save_gotelegram_config).
config = load_json(GOTELEGRAM_CONFIG) or {}
config["template_id"] = tpl_id
@@ -2734,7 +2847,7 @@ def main() -> None:
application.add_error_handler(error_handler)
# Run the bot
logger.info(f"GoTelegram v{GOTELEGRAM_VERSION} bot starting...")
logger.info(f"goTelegram Pro v{GOTELEGRAM_VERSION} bot starting...")
application.run_polling(allowed_updates=Update.ALL_TYPES)

View File

@@ -1,4 +1,4 @@
# GoTelegram v2.5.0 Bot Configuration
# goTelegram Pro v2.5.0 Bot Configuration
# Copy this to .env and fill in your values
# Telegram Bot Token from @BotFather

View File

@@ -1,5 +1,5 @@
"""
GoTelegram v2.5.0 Bot — i18n module
goTelegram Pro v2.5.0 Bot — i18n module
Provides per-user language preferences and a simple t()/tf() API.
Usage:
@@ -40,12 +40,12 @@ def _detect_default_lang() -> str:
if isinstance(data, dict):
candidates.extend([data.get("language"), data.get("lang")])
except Exception as e:
logger.warning("failed to read GoTelegram language config: %s", e)
logger.warning("failed to read goTelegram Pro language config: %s", e)
try:
if GOTELEGRAM_LANG_MARKER.exists():
candidates.append(GOTELEGRAM_LANG_MARKER.read_text(encoding="utf-8").strip()[:2])
except Exception as e:
logger.warning("failed to read GoTelegram language marker: %s", e)
logger.warning("failed to read goTelegram Pro language marker: %s", e)
candidates.append(os.getenv("BOT_LANG", ""))
for raw in candidates:
code = str(raw or "").strip().lower()

View File

@@ -6,7 +6,7 @@
"lang_saved": "Language saved: %s",
"lang_choose": "Choose your language:",
"welcome_title": "GoTelegram v%s",
"welcome_title": "goTelegram Pro v%s",
"welcome_subtitle": "🤖 MTProxy Management Bot",
"welcome_powered": "Powered by telemt engine",
"welcome_prompt": "Select an action from the menu below:",
@@ -17,7 +17,7 @@
"btn_no": "❌ No",
"access_denied": "⛔ Access denied.\nYour ID: <code>%s</code>",
"help_title": "GoTelegram Bot — Commands",
"help_title": "goTelegram Pro Bot — Commands",
"help_lines": "/start — Main menu\n/help — This help\n/status — Quick status\n/logs — Latest logs\n/lang — Change language\n/addadmin ID — Add admin\n/deladmin ID — Remove admin\n\nUse the menu buttons for other operations.",
"menu_install": "⚙️ Install",

View File

@@ -6,7 +6,7 @@
"lang_saved": "Язык сохранён: %s",
"lang_choose": "Выберите язык:",
"welcome_title": "GoTelegram v%s",
"welcome_title": "goTelegram Pro v%s",
"welcome_subtitle": "🤖 Бот управления MTProxy",
"welcome_powered": "На базе движка telemt",
"welcome_prompt": "Выберите действие в меню ниже:",
@@ -17,7 +17,7 @@
"btn_no": "❌ Нет",
"access_denied": "⛔ Доступ запрещён.\nВаш ID: <code>%s</code>",
"help_title": "GoTelegram Bot — Команды",
"help_title": "goTelegram Pro Bot — Команды",
"help_lines": "/start — Главное меню\n/help — Эта справка\n/status — Быстрый статус\n/logs — Последние логи\n/lang — Сменить язык\n/addadmin ID — Добавить админа\n/deladmin ID — Удалить админа\n\nИспользуйте кнопки меню для остальных операций.",
"menu_install": "⚙️ Установить",

View File

@@ -1,6 +1,6 @@
#!/bin/bash
# ══════════════════════════════════════════════════════════════════════════════
# GoTelegram v2.5.0 — MTProxy powered by telemt (Rust + Tokio)
# goTelegram Pro v2.5.0 — MTProxy powered by telemt (Rust + Tokio)
# Anti-DPI • Fake TLS • TCP Splice • JA3/JA4 Resistance • i18n (EN/RU)
#
# Install:
@@ -45,7 +45,7 @@ show_main_menu() {
# ── Header (no right border — ANSI breaks alignment) ──
echo ""
echo -e " ${BOLD}${CYAN}${line}${NC}"
echo -e " ${BOLD}${WHITE} GoTelegram v${GOTELEGRAM_VERSION}${NC} ${DIM}$(t dashboard_title)${NC}"
echo -e " ${BOLD}${WHITE} goTelegram Pro v${GOTELEGRAM_VERSION}${NC} ${DIM}$(t dashboard_title)${NC}"
echo -e " ${BOLD}${CYAN}${line}${NC}"
# ── Service health ──
@@ -354,7 +354,7 @@ auto_migrate_legacy_state() {
[ -f "$TELEMT_CONFIG" ] || [ -f "$GOTELEGRAM_CONFIG" ] || [ -d "$WEBSITE_ROOT" ] || return 0
log_step "Миграция состояния GoTelegram"
log_step "Миграция состояния goTelegram Pro"
snapshot_preupgrade_state
local mode port secret mask_host domain mask_port tpl_id tpl_source users_block tls_emulation changed=0 users_block_needs_write=0
@@ -411,7 +411,7 @@ auto_migrate_legacy_state() {
if [ -f "$TELEMT_CONFIG" ]; then
if ! grep -q '\[server.api\]' "$TELEMT_CONFIG" 2>/dev/null || \
! grep -q 'metrics_listen' "$TELEMT_CONFIG" 2>/dev/null || \
! grep -q "GoTelegram v${GOTELEGRAM_VERSION}" "$TELEMT_CONFIG" 2>/dev/null; then
! grep -q "goTelegram Pro v${GOTELEGRAM_VERSION}" "$TELEMT_CONFIG" 2>/dev/null; then
generate_telemt_toml "$secret" "$port" "$mode" "$mask_host" "$mask_port" "$TELEMT_CONFIG" >&2
replace_telemt_users_block "$users_block" "$TELEMT_CONFIG"
changed=1
@@ -519,7 +519,7 @@ install_lite_mode() {
# Start
start_telemt || return
# Save GoTelegram config
# Save goTelegram Pro config
save_gotelegram_config "telemt" "lite" "$port" "$secret" "$domain" "" ""
# Credits
@@ -905,7 +905,7 @@ install_admin_web() {
python_bin=$(command -v python3)
cat > "/etc/systemd/system/${ADMIN_WEB_SERVICE}.service" << SVCEOF
[Unit]
Description=GoTelegram v${GOTELEGRAM_VERSION} Local Web Admin
Description=goTelegram Pro v${GOTELEGRAM_VERSION} Local Web Admin
After=network.target
[Service]
@@ -1190,7 +1190,7 @@ bot_install() {
# Systemd
cat > "/etc/systemd/system/${BOT_SERVICE}.service" << SVCEOF
[Unit]
Description=GoTelegram v${GOTELEGRAM_VERSION} Telegram Bot
Description=goTelegram Pro v${GOTELEGRAM_VERSION} Telegram Bot
After=network.target
[Service]
@@ -1380,6 +1380,7 @@ bot_remove() {
_promo_block() {
# Print a promo section without width-fragile box borders (i18n safe)
local line2; line2=$(printf '─%.0s' {1..54})
local youtube_link="${GOTELEGRAM_YOUTUBE_LINK:-}"
echo ""
echo -e " ${DIM}${line2}${NC}"
echo -e " ${BOLD}${YELLOW}$(t promo_host1_title)${NC}"
@@ -1394,6 +1395,11 @@ _promo_block() {
echo -e " ${DIM}${line2}${NC}"
echo -e " ${BOLD}${YELLOW}$(t promo_tips_title)${NC}"
echo -e " ${CYAN}https://pay.cloudtips.ru/p/7410814f${NC}"
if [ -n "$youtube_link" ]; then
echo -e " ${DIM}${line2}${NC}"
echo -e " ${BOLD}${YELLOW}$(t promo_youtube_title)${NC}"
echo -e " $(t promo_link_label) ${CYAN}${youtube_link}${NC}"
fi
echo -e " ${DIM}${line2}${NC}"
echo ""
}
@@ -1423,19 +1429,24 @@ mark_promo_shown() {
date +%s > "$GOTELEGRAM_DIR/.promo_last_shown"
}
_promo_qr() {
local label="$1" url="$2"
[ -n "$url" ] || return 0
echo -e " ${DIM}${label}${NC}"
qrencode -t UTF8 -m 1 "$url" 2>/dev/null | while IFS= read -r qr_line; do
echo " $qr_line"
done
}
# ── Promo with QR + delay (on install + once per day) ───────────────────
# QR показываем ТОЛЬКО для чаевых/донатов. Для хостеров оставлены только
# текстовые ссылки и промокоды (см. _promo_block) — QR-коды хостеров
# визуально конкурировали с чаевыми и перегружали экран.
show_promo_with_qr() {
_promo_block
# QR только для чаевых
if command -v qrencode &>/dev/null; then
echo -e " ${DIM}$(t promo_qr_tips)${NC}"
qrencode -t UTF8 -m 1 "https://pay.cloudtips.ru/p/7410814f" 2>/dev/null | while IFS= read -r qr_line; do
echo " $qr_line"
done
_promo_qr "$(t promo_qr_host1)" "https://vk.cc/ct29NQ"
_promo_qr "$(t promo_qr_host2)" "https://vk.cc/cUxAhj"
_promo_qr "$(t promo_qr_tips)" "https://pay.cloudtips.ru/p/7410814f"
_promo_qr "$(t promo_qr_youtube)" "${GOTELEGRAM_YOUTUBE_LINK:-}"
fi
mark_promo_shown

View File

@@ -1,5 +1,5 @@
#!/bin/bash
# GoTelegram v2.5.0 — backup and restore (i18n-aware)
# goTelegram Pro v2.5.0 — backup and restore (i18n-aware)
# ── Создание бекапа ──────────────────────────────────────────────────────────
create_backup() {
@@ -20,10 +20,13 @@ create_backup() {
cp "$TELEMT_CONFIG" "$tmp_dir/config.toml"
fi
# GoTelegram конфиг
# goTelegram Pro конфиг
if [ -f "$GOTELEGRAM_CONFIG" ]; then
cp "$GOTELEGRAM_CONFIG" "$tmp_dir/gotelegram.json"
fi
if [ -f "$GOTELEGRAM_DIR/disabled_users.json" ]; then
cp "$GOTELEGRAM_DIR/disabled_users.json" "$tmp_dir/disabled_users.json" 2>/dev/null
fi
# Language marker (i18n)
if [ -f "$GOTELEGRAM_DIR/.language" ]; then
@@ -97,7 +100,7 @@ create_backup() {
cat > "$tmp_dir/metadata.json" << EOMETA
{
"backup_version": "1.3",
"backup_version": "1.4",
"gotelegram_version": "$GOTELEGRAM_VERSION",
"created_at": "$(date -Iseconds)",
"hostname": "$(hostname)",
@@ -241,12 +244,17 @@ restore_backup() {
log_success "$(_t_or backup_restored_telemt 'telemt конфиг восстановлен')"
fi
# Восстанавливаем GoTelegram конфиг
# Восстанавливаем goTelegram Pro конфиг
if [ -f "$backup_dir/gotelegram.json" ]; then
mkdir -p "$GOTELEGRAM_DIR"
cp "$backup_dir/gotelegram.json" "$GOTELEGRAM_CONFIG"
log_success "$(_t_or backup_restored_gotelegram 'GoTelegram конфиг восстановлен')"
fi
if [ -f "$backup_dir/disabled_users.json" ]; then
mkdir -p "$GOTELEGRAM_DIR"
cp "$backup_dir/disabled_users.json" "$GOTELEGRAM_DIR/disabled_users.json"
chmod 600 "$GOTELEGRAM_DIR/disabled_users.json" 2>/dev/null || true
fi
# Восстанавливаем language marker (i18n)
if [ -f "$backup_dir/.language" ]; then

View File

@@ -1,10 +1,10 @@
#!/bin/bash
# GoTelegram v2.5.0 — common utilities
# goTelegram Pro v2.5.0 — common utilities
# Colors, logging, spinner, system helpers, v1 compat, i18n-aware
# ── Version ───────────────────────────────────────────────────────────────────
GOTELEGRAM_VERSION="2.5.0"
GOTELEGRAM_NAME="GoTelegram"
GOTELEGRAM_NAME="goTelegram Pro"
# ── Пути ──────────────────────────────────────────────────────────────────────
GOTELEGRAM_DIR="/opt/gotelegram"
@@ -123,7 +123,7 @@ show_banner() {
echo -e " ${DIM}$(t banner_subtitle)${NC}"
echo -e " ${DIM}$(t banner_features)${NC}"
else
echo -e " ${BOLD}${WHITE}🚀 GoTelegram v${GOTELEGRAM_VERSION}${NC}"
echo -e " ${BOLD}${WHITE}🚀 goTelegram Pro v${GOTELEGRAM_VERSION}${NC}"
echo -e " ${DIM}MTProxy powered by telemt (Rust + Tokio)${NC}"
echo -e " ${DIM}Anti-DPI • Fake TLS • TCP Splice • JA3/JA4${NC}"
fi
@@ -483,26 +483,26 @@ detect_3xui_443_listener() {
warn_3xui_443_conflict() {
detect_3xui_443_listener || return 1
log_warning "Обнаружен 3x-ui/Xray, который уже слушает TCP/443."
log_warning "GoTelegram не будет молча останавливать или переписывать 3x-ui."
log_dim "Для настоящего shared-443 нужен один фронтовой TLS/SNI-диспетчер и разные SNI-домены для Xray и GoTelegram."
log_warning "goTelegram Pro не будет молча останавливать или переписывать 3x-ui."
log_dim "Для настоящего shared-443 нужен один фронтовой TLS/SNI-диспетчер и разные SNI-домены для Xray и goTelegram Pro."
mkdir -p "$GOTELEGRAM_DIR" 2>/dev/null
cat > "$GOTELEGRAM_DIR/shared-443-3xui.md" <<'EOF' 2>/dev/null || true
# GoTelegram + 3x-ui on one TCP/443
# goTelegram Pro + 3x-ui on one TCP/443
GoTelegram detected that 3x-ui/Xray already owns TCP/443. Two independent
goTelegram Pro detected that 3x-ui/Xray already owns TCP/443. Two independent
processes cannot bind the same IP:port at the same time. A safe shared setup
needs one front TLS/SNI dispatcher on 443 and internal backends, for example:
- dispatcher: 0.0.0.0:443
- GoTelegram telemt: 127.0.0.1:7443
- goTelegram Pro telemt: 127.0.0.1:7443
- 3x-ui/Xray inbound: 127.0.0.1:9443
- GoTelegram nginx mask site: 127.0.0.1:8443
- goTelegram Pro nginx mask site: 127.0.0.1:8443
The dispatcher must route Xray SNI domains to Xray and route the GoTelegram
SNI domain to telemt. If Xray and GoTelegram use the same SNI domain, automatic
The dispatcher must route Xray SNI domains to Xray and route the goTelegram Pro
SNI domain to telemt. If Xray and goTelegram Pro use the same SNI domain, automatic
sharing is not reliable: the first TLS ClientHello is intentionally identical.
GoTelegram intentionally does not rewrite the 3x-ui SQLite database or generated
goTelegram Pro intentionally does not rewrite the 3x-ui SQLite database or generated
Xray config without explicit operator confirmation, because 3x-ui can overwrite
manual JSON edits on the next panel change.
EOF

View File

@@ -1,5 +1,5 @@
#!/bin/bash
# GoTelegram v2.5.0 — English translations
# goTelegram Pro v2.5.0 — English translations
# shellcheck disable=SC2034,SC2148
# ── Common words ────────────────────────────────────────────────────────
@@ -25,7 +25,7 @@ I18N[success]="Done"
I18N[wait]="Please wait..."
# ── Banner ──────────────────────────────────────────────────────────────
I18N[banner_title]="GoTelegram v%s"
I18N[banner_title]="goTelegram Pro v%s"
I18N[banner_subtitle]="MTProxy powered by telemt (Rust + Tokio)"
I18N[banner_features]="Anti-DPI • Fake TLS • TCP Splice • JA3/JA4"
I18N[credits_title]="Credits / Thanks"
@@ -75,7 +75,7 @@ I18N[submenu_about_title]=" ABOUT"
I18N[about_version_info]="Version info"
I18N[about_promo]="Promo / Donate"
I18N[version_title]="🔍 Information"
I18N[version_label]="GoTelegram:"
I18N[version_label]="goTelegram Pro:"
I18N[version_engine]="Engine:"
I18N[version_tech]="Technology:"
I18N[version_license]="License:"
@@ -106,7 +106,7 @@ I18N[install_cfg_mode]="Mode:"
I18N[install_cfg_domain]="Domain:"
I18N[install_confirm_proxy]="Install proxy?"
I18N[install_confirm_proxy_site]="Install proxy + website?"
I18N[install_done]="GoTelegram v%s installed! (%s mode)"
I18N[install_done]="goTelegram Pro v%s installed! (%s mode)"
I18N[install_arch_desc1]="telemt accepts all traffic on 443 (HTTPS masquerade)"
I18N[install_arch_desc2]="nginx serves the site on internal port %s"
I18N[install_arch_desc3]="ISP only sees HTTPS traffic to %s:443"
@@ -125,7 +125,7 @@ I18N[logs_telemt_title]="📋 telemt logs (last %s lines):"
# ── Link / Share ────────────────────────────────────────────────────────
I18N[link_title]="🔗 Connection link:"
I18N[share_title]="📤 Forward this message:"
I18N[share_line1]="🔐 MTProxy for Telegram (GoTelegram v%s)"
I18N[share_line1]="🔐 MTProxy for Telegram (goTelegram Pro v%s)"
I18N[share_server]="🌍 Server: %s"
I18N[share_port]="🔌 Port: %s"
I18N[share_connect_cta]="👉 Connect with one tap:"
@@ -141,7 +141,7 @@ I18N[website_restart_nginx]="Restart nginx"
I18N[website_change_template]="Change template"
# ── Remove ──────────────────────────────────────────────────────────────
I18N[remove_title]="🗑 Remove GoTelegram"
I18N[remove_title]="🗑 Remove goTelegram Pro"
I18N[remove_proxy_only]="Remove proxy only (telemt)"
I18N[remove_bot_only]="Remove Telegram bot only"
I18N[remove_all]="Remove everything (proxy + bot + settings)"
@@ -151,7 +151,7 @@ I18N[remove_backup_before]="Create a backup before removal?"
I18N[remove_warn_all]="This will remove EVERYTHING: proxy, bot, site, settings."
I18N[remove_confirm_all]="Are you absolutely sure?"
I18N[remove_proxy_done]="Proxy removed"
I18N[remove_all_done]="GoTelegram fully removed (proxy + bot)"
I18N[remove_all_done]="goTelegram Pro fully removed (proxy + bot)"
# ── Telegram bot submenu ────────────────────────────────────────────────
I18N[bot_title]="🤖 Telegram bot"
@@ -218,6 +218,7 @@ I18N[bot_access_ids_fmt]="ID: %s"
I18N[promo_host1_title]="💰 HOSTING #1 — UP TO 60% OFF"
I18N[promo_host2_title]="💰 HOSTING #2 — UP TO 60% OFF"
I18N[promo_tips_title]="☕ Donate / Tips"
I18N[promo_youtube_title]="▶ YouTube Channel"
I18N[promo_link_label]="Link:"
I18N[promo_off60]="60%% discount on the first month"
I18N[promo_ant20]="20%% + 3%% when paid for 3 months"
@@ -225,6 +226,7 @@ I18N[promo_ant6]="15%% + 5%% when paid for 6 months"
I18N[promo_qr_host1]="── QR: Hosting #1 ──"
I18N[promo_qr_host2]="── QR: Hosting #2 ──"
I18N[promo_qr_tips]="── QR: Donate / Tips ──"
I18N[promo_qr_youtube]="── QR: YouTube Channel ──"
I18N[promo_menu_in]="Menu in %d sec..."
# ── Stats ───────────────────────────────────────────────────────────────
@@ -330,7 +332,7 @@ I18N[backup_lang_label]="Language"
I18N[backup_date_label]="Date"
I18N[backup_confirm_restore]="Restore configuration? Current settings will be overwritten."
I18N[backup_restored_telemt]="telemt config restored"
I18N[backup_restored_gotelegram]="GoTelegram config restored"
I18N[backup_restored_gotelegram]="goTelegram Pro config restored"
I18N[backup_restored_lang]="Interface language restored"
I18N[backup_restored_nginx]="nginx config restored"
I18N[backup_restored_ssl]="SSL certificates restored"
@@ -360,7 +362,7 @@ I18N[auto_refresh]="Refresh in 30 sec"
I18N[deps_installing]="Installing dependencies: %s"
# ── Migration ───────────────────────────────────────────────────────────
I18N[v1_detected]="⚠️ GoTelegram v1 (mtg) installation detected"
I18N[v1_detected]="⚠️ goTelegram Pro v1 (mtg) installation detected"
I18N[v1_container]="Container: %s"
I18N[v1_migration_step]="Migrating from v1 (mtg) to v2 (telemt)"
I18N[v1_found_title]="Found v1 (mtg) installation:"

View File

@@ -1,5 +1,5 @@
#!/bin/bash
# GoTelegram v2.5.0 — Russian translations
# goTelegram Pro v2.5.0 — Russian translations
# shellcheck disable=SC2034,SC2148
# ── Common words ────────────────────────────────────────────────────────
@@ -25,7 +25,7 @@ I18N[success]="Готово"
I18N[wait]="Подождите..."
# ── Banner ──────────────────────────────────────────────────────────────
I18N[banner_title]="GoTelegram v%s"
I18N[banner_title]="goTelegram Pro v%s"
I18N[banner_subtitle]="MTProxy на ядре telemt (Rust + Tokio)"
I18N[banner_features]="Anti-DPI • Fake TLS • TCP Splice • JA3/JA4"
I18N[credits_title]="Благодарности / Credits"
@@ -75,7 +75,7 @@ I18N[submenu_about_title]=" О ПРОГРАММЕ"
I18N[about_version_info]="Информация о версии"
I18N[about_promo]="Промо / Донат"
I18N[version_title]="🔍 Информация"
I18N[version_label]="GoTelegram:"
I18N[version_label]="goTelegram Pro:"
I18N[version_engine]="Ядро:"
I18N[version_tech]="Технология:"
I18N[version_license]="Лицензия:"
@@ -106,7 +106,7 @@ I18N[install_cfg_mode]="Режим:"
I18N[install_cfg_domain]="Домен:"
I18N[install_confirm_proxy]="Установить прокси?"
I18N[install_confirm_proxy_site]="Установить прокси + сайт?"
I18N[install_done]="GoTelegram v%s установлен! (%s-режим)"
I18N[install_done]="goTelegram Pro v%s установлен! (%s-режим)"
I18N[install_arch_desc1]="telemt принимает весь трафик на 443 (маскировка под HTTPS)"
I18N[install_arch_desc2]="nginx обслуживает сайт на внутреннем порту %s"
I18N[install_arch_desc3]="Провайдер видит только HTTPS-трафик к %s:443"
@@ -125,7 +125,7 @@ I18N[logs_telemt_title]="📋 Логи telemt (последние %s строк)
# ── Link / Share ────────────────────────────────────────────────────────
I18N[link_title]="🔗 Ссылка для подключения:"
I18N[share_title]="📤 Перешлите это сообщение:"
I18N[share_line1]="🔐 MTProxy для Telegram (GoTelegram v%s)"
I18N[share_line1]="🔐 MTProxy для Telegram (goTelegram Pro v%s)"
I18N[share_server]="🌍 Сервер: %s"
I18N[share_port]="🔌 Порт: %s"
I18N[share_connect_cta]="👉 Подключиться одним нажатием:"
@@ -141,7 +141,7 @@ I18N[website_restart_nginx]="Перезапустить nginx"
I18N[website_change_template]="Сменить шаблон"
# ── Remove ──────────────────────────────────────────────────────────────
I18N[remove_title]="🗑 Удаление GoTelegram"
I18N[remove_title]="🗑 Удаление goTelegram Pro"
I18N[remove_proxy_only]="Удалить только прокси (telemt)"
I18N[remove_bot_only]="Удалить только Telegram-бота"
I18N[remove_all]="Удалить всё (прокси + бот + настройки)"
@@ -151,7 +151,7 @@ I18N[remove_backup_before]="Сделать бекап перед удалени
I18N[remove_warn_all]="Это удалит ВСЁ: прокси, бот, сайт, настройки."
I18N[remove_confirm_all]="Вы точно уверены?"
I18N[remove_proxy_done]="Прокси удалён"
I18N[remove_all_done]="GoTelegram полностью удалён (прокси + бот)"
I18N[remove_all_done]="goTelegram Pro полностью удалён (прокси + бот)"
# ── Telegram bot submenu ────────────────────────────────────────────────
I18N[bot_title]="🤖 Telegram-бот"
@@ -218,6 +218,7 @@ I18N[bot_access_ids_fmt]="ID: %s"
I18N[promo_host1_title]="💰 ХОСТИНГ #1 — СКИДКА ДО 60%"
I18N[promo_host2_title]="💰 ХОСТИНГ #2 — СКИДКА ДО 60%"
I18N[promo_tips_title]="☕ Донат / Чаевые"
I18N[promo_youtube_title]="▶ YouTube-канал"
I18N[promo_link_label]="Ссылка:"
I18N[promo_off60]="60%% скидки на первый месяц"
I18N[promo_ant20]="20%% + 3%% при оплате за 3 месяца"
@@ -225,6 +226,7 @@ I18N[promo_ant6]="15%% + 5%% при оплате за 6 месяцев"
I18N[promo_qr_host1]="── QR: Хостинг #1 ──"
I18N[promo_qr_host2]="── QR: Хостинг #2 ──"
I18N[promo_qr_tips]="── QR: Чаевые / Донат ──"
I18N[promo_qr_youtube]="── QR: YouTube-канал ──"
I18N[promo_menu_in]="Меню через %d сек..."
# ── Stats ───────────────────────────────────────────────────────────────
@@ -330,7 +332,7 @@ I18N[backup_lang_label]="Язык"
I18N[backup_date_label]="Дата"
I18N[backup_confirm_restore]="Восстановить конфигурацию? Текущие настройки будут перезаписаны."
I18N[backup_restored_telemt]="telemt конфиг восстановлен"
I18N[backup_restored_gotelegram]="GoTelegram конфиг восстановлен"
I18N[backup_restored_gotelegram]="goTelegram Pro конфиг восстановлен"
I18N[backup_restored_lang]="Язык интерфейса восстановлен"
I18N[backup_restored_nginx]="nginx конфиг восстановлен"
I18N[backup_restored_ssl]="SSL сертификаты восстановлены"
@@ -360,7 +362,7 @@ I18N[auto_refresh]="Обновление через 30 сек"
I18N[deps_installing]="Установка зависимостей: %s"
# ── Migration ───────────────────────────────────────────────────────────
I18N[v1_detected]="⚠️ Обнаружена установка GoTelegram v1 (mtg)"
I18N[v1_detected]="⚠️ Обнаружена установка goTelegram Pro v1 (mtg)"
I18N[v1_container]="Контейнер: %s"
I18N[v1_migration_step]="Миграция с v1 (mtg) на v2 (telemt)"
I18N[v1_found_title]="Найдена установка v1 (mtg):"