mirror of
https://github.com/anten-ka/gotelegram_pro.git
synced 2026-05-19 11:26:03 +00:00
v2.5.0: add key disable switches and pro UI polish
This commit is contained in:
20
DOCS_AI.md
20
DOCS_AI.md
@@ -1,4 +1,4 @@
|
|||||||
# GoTelegram Pro — техническая документация для ИИ-агентов
|
# goTelegram Pro — техническая документация для ИИ-агентов
|
||||||
|
|
||||||
**Версия:** 2.5.0
|
**Версия:** 2.5.0
|
||||||
**Репозиторий:** `anten-ka/gotelegram_pro`
|
**Репозиторий:** `anten-ka/gotelegram_pro`
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
|
|
||||||
## 1. Общая картина
|
## 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`).
|
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`.
|
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/` |
|
| Репо-скрипты | `/opt/gotelegram/` |
|
||||||
| Symlink запуска | `/usr/local/bin/gotelegram` → `/opt/gotelegram/install.sh` |
|
| 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 | `/etc/telemt/config.toml` |
|
||||||
| Бинарник telemt | `/usr/local/bin/telemt` |
|
| Бинарник telemt | `/usr/local/bin/telemt` |
|
||||||
| Systemd юнит telemt | `/etc/systemd/system/telemt.service` |
|
| 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 конфиг | `/etc/nginx/sites-available/gotelegram` |
|
||||||
| nginx enabled | `/etc/nginx/sites-enabled/gotelegram` |
|
| nginx enabled | `/etc/nginx/sites-enabled/gotelegram` |
|
||||||
| Бекапы | `/opt/gotelegram/backups/` |
|
| Бекапы | `/opt/gotelegram/backups/` |
|
||||||
| Лог GoTelegram | `/var/log/gotelegram.log` |
|
| Лог goTelegram Pro | `/var/log/gotelegram.log` |
|
||||||
| Логи telemt | `journalctl -u telemt` |
|
| Логи telemt | `journalctl -u telemt` |
|
||||||
| Логи бота | `journalctl -u gotelegram-bot` |
|
| Логи бота | `journalctl -u gotelegram-bot` |
|
||||||
|
|
||||||
@@ -196,7 +196,7 @@ dns_overrides = ["anten-ka.com:8443:127.0.0.1"]
|
|||||||
Генерируется функцией `install_telemt_service()` в `lib/telemt.sh`:
|
Генерируется функцией `install_telemt_service()` в `lib/telemt.sh`:
|
||||||
```ini
|
```ini
|
||||||
[Unit]
|
[Unit]
|
||||||
Description=GoTelegram MTProxy (telemt engine)
|
Description=goTelegram Pro MTProxy (telemt engine)
|
||||||
After=network-online.target
|
After=network-online.target
|
||||||
Wants=network-online.target
|
Wants=network-online.target
|
||||||
|
|
||||||
@@ -448,12 +448,14 @@ switch_language ru|en
|
|||||||
- токена нет: после SSH tunnel открывается `http://127.0.0.1:1984/`;
|
- токена нет: после SSH tunnel открывается `http://127.0.0.1:1984/`;
|
||||||
- write-запросы дополнительно требуют `X-GoTelegram-Admin: 1`, фронтенд добавляет его автоматически.
|
- 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;
|
- язык панели читается из `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`.
|
- `/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)
|
### 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
|
## 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.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.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`.
|
- **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`.
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# GoTelegram Pro — руководство пользователя
|
# goTelegram Pro — руководство пользователя
|
||||||
|
|
||||||
**Версия:** 2.5.0
|
**Версия:** 2.5.0
|
||||||
**Репозиторий:** `anten-ka/gotelegram_pro`
|
**Репозиторий:** `anten-ka/gotelegram_pro`
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
## 1. Что это такое
|
## 1. Что это такое
|
||||||
|
|
||||||
GoTelegram Pro — это готовый менеджер прокси-сервера MTProxy для Telegram. Он делает три вещи, которые иначе пришлось бы собирать вручную:
|
goTelegram Pro — это готовый менеджер прокси-сервера MTProxy для Telegram. Он делает три вещи, которые иначе пришлось бы собирать вручную:
|
||||||
|
|
||||||
1. Ставит и настраивает ядро **telemt** (это современный Rust-порт mtproto-proxy с fake-TLS маскировкой).
|
1. Ставит и настраивает ядро **telemt** (это современный Rust-порт mtproto-proxy с fake-TLS маскировкой).
|
||||||
2. Запускает рядом обычный HTTPS-сайт на настоящем домене, так что провайдеру со стороны всё выглядит как посещение безобидного лендинга — а на самом деле в том же соединении ходит Telegram-трафик. Это называется «stealth» или «Pro-режим».
|
2. Запускает рядом обычный HTTPS-сайт на настоящем домене, так что провайдеру со стороны всё выглядит как посещение безобидного лендинга — а на самом деле в том же соединении ходит Telegram-трафик. Это называется «stealth» или «Pro-режим».
|
||||||
@@ -125,7 +125,7 @@ CLI и бот переведены на русский и английский.
|
|||||||
|
|
||||||
## 7. Бекап и восстановление
|
## 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 принимает такой архив и восстанавливает всё обратно (конфиг, сервис, ссылка — всё то же, что было).
|
Пункт 9 принимает такой архив и восстанавливает всё обратно (конфиг, сервис, ссылка — всё то же, что было).
|
||||||
|
|
||||||
@@ -145,7 +145,9 @@ CLI и бот переведены на русский и английский.
|
|||||||
- есть светлая/тёмная тема, вкладки и адаптивная вёрстка под desktop/mobile;
|
- есть светлая/тёмная тема, вкладки и адаптивная вёрстка под desktop/mobile;
|
||||||
- Telegram-бот показывает инструкцию для Termius и обычную команду `ssh -L 1984:127.0.0.1:1984 root@SERVER`.
|
- 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`, перезапускает сервис. Конфиг остаётся как был.
|
- **Обновление ядра telemt** (пункт 10) — тянет свежий бинарник с GitHub Releases `telemt/telemt`, сохраняет старый в `.bak`, перезапускает сервис. Конфиг остаётся как был.
|
||||||
- **Обновление самого GoTelegram** (пункт 1) — переустанавливает скрипты и lib/. Файл `config.json` не трогается, ключ и домен не меняются.
|
- **Обновление самого goTelegram Pro** (пункт 1) — переустанавливает скрипты и lib/. Файл `config.json` не трогается, ключ и домен не меняются.
|
||||||
|
|
||||||
Bootstrap.sh умеет сам обновлять всё, если запустить его повторно.
|
Bootstrap.sh умеет сам обновлять всё, если запустить его повторно.
|
||||||
|
|
||||||
@@ -214,7 +216,7 @@ A: Сам MTProxy — да, это публичная технология из
|
|||||||
| Скрипты (`install.sh`, `lib/`) | `/opt/gotelegram/` |
|
| Скрипты (`install.sh`, `lib/`) | `/opt/gotelegram/` |
|
||||||
| Симлинк запуска | `/usr/local/bin/gotelegram` |
|
| Симлинк запуска | `/usr/local/bin/gotelegram` |
|
||||||
| Конфиг telemt | `/etc/telemt/config.toml` |
|
| Конфиг telemt | `/etc/telemt/config.toml` |
|
||||||
| Конфиг GoTelegram (JSON) | `/opt/gotelegram/config.json` |
|
| Конфиг goTelegram Pro (JSON) | `/opt/gotelegram/config.json` |
|
||||||
| Бинарник telemt | `/usr/local/bin/telemt` |
|
| Бинарник telemt | `/usr/local/bin/telemt` |
|
||||||
| Systemd юнит | `/etc/systemd/system/telemt.service` |
|
| Systemd юнит | `/etc/systemd/system/telemt.service` |
|
||||||
| Бот | `/opt/gotelegram-bot/` |
|
| Бот | `/opt/gotelegram-bot/` |
|
||||||
@@ -222,7 +224,7 @@ A: Сам MTProxy — да, это публичная технология из
|
|||||||
| Сайт (Pro-режим) | `/var/www/gotelegram-site/` |
|
| Сайт (Pro-режим) | `/var/www/gotelegram-site/` |
|
||||||
| nginx site | `/etc/nginx/sites-available/gotelegram` |
|
| nginx site | `/etc/nginx/sites-available/gotelegram` |
|
||||||
| Бекапы | `/opt/gotelegram/backups/` |
|
| Бекапы | `/opt/gotelegram/backups/` |
|
||||||
| Лог GoTelegram | `/var/log/gotelegram.log` |
|
| Лог goTelegram Pro | `/var/log/gotelegram.log` |
|
||||||
| Логи telemt | `journalctl -u telemt` |
|
| Логи telemt | `journalctl -u telemt` |
|
||||||
| Логи бота | `journalctl -u gotelegram-bot` |
|
| Логи бота | `journalctl -u gotelegram-bot` |
|
||||||
|
|
||||||
@@ -238,7 +240,7 @@ A: Сам MTProxy — да, это публичная технология из
|
|||||||
|
|
||||||
## Changelog (коротко)
|
## 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.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.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.
|
- **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.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
#!/usr/bin/env python3
|
#!/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
|
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.
|
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"))
|
BACKUP_DIR = Path(os.getenv("GOTELEGRAM_BACKUP_DIR", "/opt/gotelegram/backups"))
|
||||||
INSTALL_DIR = Path(os.getenv("GOTELEGRAM_DIR", "/opt/gotelegram"))
|
INSTALL_DIR = Path(os.getenv("GOTELEGRAM_DIR", "/opt/gotelegram"))
|
||||||
BOT_DIR = Path(os.getenv("GOTELEGRAM_BOT_DIR", "/opt/gotelegram-bot"))
|
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")
|
HOST = os.getenv("GOTELEGRAM_ADMIN_HOST", "127.0.0.1")
|
||||||
PORT = int(os.getenv("GOTELEGRAM_ADMIN_PORT", "1984"))
|
PORT = int(os.getenv("GOTELEGRAM_ADMIN_PORT", "1984"))
|
||||||
@@ -152,6 +153,44 @@ def read_telemt_users() -> dict[str, str]:
|
|||||||
return users
|
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]:
|
def _ordered_user_lines(users: dict[str, str]) -> list[str]:
|
||||||
names = []
|
names = []
|
||||||
if "main" in users:
|
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] = {
|
item: dict[str, Any] = {
|
||||||
"name": name,
|
"name": name,
|
||||||
"secret": secret,
|
"secret": secret,
|
||||||
"link": proxy_link(secret),
|
"link": proxy_link(secret),
|
||||||
"main": name == "main",
|
"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='')}")
|
item["runtime"] = telemt_api(f"/v1/users/{urllib.parse.quote(name, safe='')}")
|
||||||
return item
|
return item
|
||||||
|
|
||||||
@@ -477,7 +517,7 @@ def user_payload(name: str, secret: str, include_runtime: bool = False) -> dict[
|
|||||||
def overview_payload() -> dict[str, Any]:
|
def overview_payload() -> dict[str, Any]:
|
||||||
config = load_json(GOTELEGRAM_CONFIG, {}) or {}
|
config = load_json(GOTELEGRAM_CONFIG, {}) or {}
|
||||||
language = read_language(config)
|
language = read_language(config)
|
||||||
users = read_telemt_users()
|
users = read_user_records()
|
||||||
current = load_json(CURRENT_STATS, {}) or {}
|
current = load_json(CURRENT_STATS, {}) or {}
|
||||||
history = load_stats_history()
|
history = load_stats_history()
|
||||||
summary = telemt_api("/v1/stats/summary")
|
summary = telemt_api("/v1/stats/summary")
|
||||||
@@ -506,7 +546,7 @@ def overview_payload() -> dict[str, Any]:
|
|||||||
|
|
||||||
|
|
||||||
class AdminHandler(BaseHTTPRequestHandler):
|
class AdminHandler(BaseHTTPRequestHandler):
|
||||||
server_version = "GoTelegramAdmin/2.5.0"
|
server_version = "goTelegramProAdmin/2.5.0"
|
||||||
|
|
||||||
def log_message(self, fmt: str, *args: Any) -> None:
|
def log_message(self, fmt: str, *args: Any) -> None:
|
||||||
print("%s - %s" % (self.address_string(), fmt % args))
|
print("%s - %s" % (self.address_string(), fmt % args))
|
||||||
@@ -542,15 +582,20 @@ class AdminHandler(BaseHTTPRequestHandler):
|
|||||||
if path == "/api/overview":
|
if path == "/api/overview":
|
||||||
self.send_json({"ok": True, "data": overview_payload()})
|
self.send_json({"ok": True, "data": overview_payload()})
|
||||||
elif path == "/api/users":
|
elif path == "/api/users":
|
||||||
users = read_telemt_users()
|
users = read_user_records()
|
||||||
self.send_json({"ok": True, "data": [user_payload(k, v) for k, v in sorted(users.items())]})
|
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/"):
|
elif path.startswith("/api/users/"):
|
||||||
name = urllib.parse.unquote(path[len("/api/users/"):])
|
name = urllib.parse.unquote(path[len("/api/users/"):])
|
||||||
users = read_telemt_users()
|
users = read_user_records()
|
||||||
if name not in users:
|
if name not in users:
|
||||||
self.send_error_json(404, "user not found")
|
self.send_error_json(404, "user not found")
|
||||||
return
|
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":
|
elif path == "/api/backups":
|
||||||
self.send_json({"ok": True, "data": list_backups()})
|
self.send_json({"ok": True, "data": list_backups()})
|
||||||
elif path == "/api/stats":
|
elif path == "/api/stats":
|
||||||
@@ -586,10 +631,11 @@ class AdminHandler(BaseHTTPRequestHandler):
|
|||||||
if not USER_RE.match(name):
|
if not USER_RE.match(name):
|
||||||
self.send_error_json(400, "invalid user name")
|
self.send_error_json(400, "invalid user name")
|
||||||
return
|
return
|
||||||
users = read_telemt_users()
|
records = read_user_records()
|
||||||
if name in users:
|
if name in records:
|
||||||
self.send_error_json(409, "user already exists")
|
self.send_error_json(409, "user already exists")
|
||||||
return
|
return
|
||||||
|
users = read_telemt_users()
|
||||||
seed = f"{name}:{time.time()}:{secrets.token_hex(32)}".encode()
|
seed = f"{name}:{time.time()}:{secrets.token_hex(32)}".encode()
|
||||||
secret = hashlib.sha256(seed).hexdigest()[:32]
|
secret = hashlib.sha256(seed).hexdigest()[:32]
|
||||||
users[name] = secret
|
users[name] = secret
|
||||||
@@ -599,7 +645,37 @@ class AdminHandler(BaseHTTPRequestHandler):
|
|||||||
self.send_error_json(500, f"failed to save config: {exc}")
|
self.send_error_json(500, f"failed to save config: {exc}")
|
||||||
return
|
return
|
||||||
restarted = restart_service("telemt")
|
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":
|
elif path == "/api/backups":
|
||||||
ok, result = create_backup()
|
ok, result = create_backup()
|
||||||
self.send_json({"ok": ok, "data": {"path": result, "backups": list_backups()}}, 200 if ok else 500)
|
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":
|
if name == "main":
|
||||||
self.send_error_json(400, "main user cannot be deleted")
|
self.send_error_json(400, "main user cannot be deleted")
|
||||||
return
|
return
|
||||||
users = read_telemt_users()
|
active = read_telemt_users()
|
||||||
if name not in users:
|
disabled = read_disabled_users()
|
||||||
|
records = read_user_records()
|
||||||
|
if name not in records:
|
||||||
self.send_error_json(404, "user not found")
|
self.send_error_json(404, "user not found")
|
||||||
return
|
return
|
||||||
users.pop(name, None)
|
active.pop(name, None)
|
||||||
|
disabled.pop(name, None)
|
||||||
try:
|
try:
|
||||||
write_telemt_users(users)
|
write_telemt_users(active)
|
||||||
|
write_disabled_users(disabled)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
self.send_error_json(500, f"failed to save config: {exc}")
|
self.send_error_json(500, f"failed to save config: {exc}")
|
||||||
return
|
return
|
||||||
@@ -702,7 +782,7 @@ def main() -> None:
|
|||||||
if not STATIC_DIR.exists():
|
if not STATIC_DIR.exists():
|
||||||
raise SystemExit(f"static dir not found: {STATIC_DIR}")
|
raise SystemExit(f"static dir not found: {STATIC_DIR}")
|
||||||
httpd = ThreadingHTTPServer((HOST, PORT), AdminHandler)
|
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()
|
httpd.serve_forever()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ const i18n = {
|
|||||||
collectStats: "Collect",
|
collectStats: "Collect",
|
||||||
repairStats: "Repair stats",
|
repairStats: "Repair stats",
|
||||||
tableTime: "Time",
|
tableTime: "Time",
|
||||||
|
tableStatus: "Status",
|
||||||
tableProxyDelta: "Proxy delta",
|
tableProxyDelta: "Proxy delta",
|
||||||
tableSiteDelta: "Site delta",
|
tableSiteDelta: "Site delta",
|
||||||
tableProxyTotal: "Proxy total",
|
tableProxyTotal: "Proxy total",
|
||||||
@@ -56,6 +57,10 @@ const i18n = {
|
|||||||
copyLink: "Copy link",
|
copyLink: "Copy link",
|
||||||
copySecret: "Copy secret",
|
copySecret: "Copy secret",
|
||||||
delete: "Delete",
|
delete: "Delete",
|
||||||
|
enabled: "Enabled",
|
||||||
|
disabled: "Disabled",
|
||||||
|
disableKey: "Disable key",
|
||||||
|
enableKey: "Enable key",
|
||||||
main: "main",
|
main: "main",
|
||||||
createBackup: "Create backup",
|
createBackup: "Create backup",
|
||||||
loadLogs: "Load",
|
loadLogs: "Load",
|
||||||
@@ -121,6 +126,15 @@ const i18n = {
|
|||||||
logsLines: "lines",
|
logsLines: "lines",
|
||||||
logsNoData: "No log lines",
|
logsNoData: "No log lines",
|
||||||
languageSaved: "Language saved",
|
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",
|
pageDashboardTitle: "Dashboard",
|
||||||
pageDashboardKicker: "Local Admin",
|
pageDashboardKicker: "Local Admin",
|
||||||
pageTrafficTitle: "Traffic",
|
pageTrafficTitle: "Traffic",
|
||||||
@@ -175,6 +189,7 @@ const i18n = {
|
|||||||
collectStats: "Собрать",
|
collectStats: "Собрать",
|
||||||
repairStats: "Починить статистику",
|
repairStats: "Починить статистику",
|
||||||
tableTime: "Время",
|
tableTime: "Время",
|
||||||
|
tableStatus: "Статус",
|
||||||
tableProxyDelta: "Proxy delta",
|
tableProxyDelta: "Proxy delta",
|
||||||
tableSiteDelta: "Site delta",
|
tableSiteDelta: "Site delta",
|
||||||
tableProxyTotal: "Proxy всего",
|
tableProxyTotal: "Proxy всего",
|
||||||
@@ -188,6 +203,10 @@ const i18n = {
|
|||||||
copyLink: "Копировать ссылку",
|
copyLink: "Копировать ссылку",
|
||||||
copySecret: "Копировать secret",
|
copySecret: "Копировать secret",
|
||||||
delete: "Удалить",
|
delete: "Удалить",
|
||||||
|
enabled: "Включён",
|
||||||
|
disabled: "Отключён",
|
||||||
|
disableKey: "Отключить ключ",
|
||||||
|
enableKey: "Включить ключ",
|
||||||
main: "основной",
|
main: "основной",
|
||||||
createBackup: "Создать бекап",
|
createBackup: "Создать бекап",
|
||||||
loadLogs: "Загрузить",
|
loadLogs: "Загрузить",
|
||||||
@@ -253,6 +272,15 @@ const i18n = {
|
|||||||
logsLines: "строк",
|
logsLines: "строк",
|
||||||
logsNoData: "Строк логов нет",
|
logsNoData: "Строк логов нет",
|
||||||
languageSaved: "Язык сохранён",
|
languageSaved: "Язык сохранён",
|
||||||
|
keyEnabled: "Ключ включён",
|
||||||
|
keyDisabled: "Ключ отключён",
|
||||||
|
visualTitle: "Единый 443 edge",
|
||||||
|
visualText: "Сайт, MTProxy и локальная админка в одном рабочем обзоре.",
|
||||||
|
promoEyebrow: "Промо",
|
||||||
|
promoTitle: "Поддержать goTelegram Pro",
|
||||||
|
promoHosting1: "Хостинг #1",
|
||||||
|
promoHosting2: "Хостинг #2",
|
||||||
|
promoTips: "Чаевые",
|
||||||
pageDashboardTitle: "Обзор",
|
pageDashboardTitle: "Обзор",
|
||||||
pageDashboardKicker: "Локальная админка",
|
pageDashboardKicker: "Локальная админка",
|
||||||
pageTrafficTitle: "Трафик",
|
pageTrafficTitle: "Трафик",
|
||||||
@@ -356,6 +384,8 @@ function applyI18n() {
|
|||||||
$("#languageSelect").value = state.lang;
|
$("#languageSelect").value = state.lang;
|
||||||
$("#settingsLanguage").textContent = state.lang === "ru" ? "Русский" : "English";
|
$("#settingsLanguage").textContent = state.lang === "ru" ? "Русский" : "English";
|
||||||
$("#settingsTheme").textContent = state.theme === "dark" ? t("darkTheme") : t("lightTheme");
|
$("#settingsTheme").textContent = state.theme === "dark" ? t("darkTheme") : t("lightTheme");
|
||||||
|
$("#visualTitle").textContent = t("visualTitle");
|
||||||
|
$("#visualText").textContent = t("visualText");
|
||||||
updatePageTitle();
|
updatePageTitle();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -607,16 +637,25 @@ function renderHistoryTable(rows) {
|
|||||||
function renderUsers() {
|
function renderUsers() {
|
||||||
const tbody = $("#usersTable");
|
const tbody = $("#usersTable");
|
||||||
if (!state.users.length) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
tbody.innerHTML = state.users.map((user) => `
|
tbody.innerHTML = state.users.map((user) => `
|
||||||
<tr>
|
<tr class="${user.enabled ? "" : "disabled-row"}">
|
||||||
<td data-label="${escapeAttr(t("tableUser"))}">
|
<td data-label="${escapeAttr(t("tableUser"))}">
|
||||||
<strong>${escapeHtml(user.name)}</strong>${user.main ? ` <small>${escapeHtml(t("main"))}</small>` : ""}
|
<strong>${escapeHtml(user.name)}</strong>${user.main ? ` <small>${escapeHtml(t("main"))}</small>` : ""}
|
||||||
</td>
|
</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("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">
|
<td data-label="${escapeAttr(t("tableActions"))}" class="actions">
|
||||||
<button class="soft" data-copy="${escapeAttr(user.secret)}">${escapeHtml(t("copySecret"))}</button>
|
<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>
|
<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();
|
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() {
|
async function createBackup() {
|
||||||
const btn = $("#createBackupBtn");
|
const btn = $("#createBackupBtn");
|
||||||
btn.disabled = true;
|
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) => {
|
document.addEventListener("click", async (eventObj) => {
|
||||||
const nav = eventObj.target.closest("[data-nav]");
|
const nav = eventObj.target.closest("[data-nav]");
|
||||||
if (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) => {
|
$("#addUserForm").addEventListener("submit", (eventObj) => {
|
||||||
eventObj.preventDefault();
|
eventObj.preventDefault();
|
||||||
const input = $("#userName");
|
const input = $("#userName");
|
||||||
@@ -841,6 +911,9 @@ $("#addUserForm").addEventListener("submit", (eventObj) => {
|
|||||||
|
|
||||||
$("#refreshBtn").addEventListener("click", refreshAll);
|
$("#refreshBtn").addEventListener("click", refreshAll);
|
||||||
$("#languageSelect").addEventListener("change", (eventObj) => setLanguage(eventObj.target.value));
|
$("#languageSelect").addEventListener("change", (eventObj) => setLanguage(eventObj.target.value));
|
||||||
|
$("#promoClose").addEventListener("click", () => {
|
||||||
|
$("#promoModal").hidden = true;
|
||||||
|
});
|
||||||
$("#createBackupBtn").addEventListener("click", createBackup);
|
$("#createBackupBtn").addEventListener("click", createBackup);
|
||||||
$("#loadLogsBtn").addEventListener("click", loadLogs);
|
$("#loadLogsBtn").addEventListener("click", loadLogs);
|
||||||
$("#repairStatsBtn").addEventListener("click", repairStats);
|
$("#repairStatsBtn").addEventListener("click", repairStats);
|
||||||
@@ -852,3 +925,4 @@ setTheme(state.theme);
|
|||||||
renderEvents();
|
renderEvents();
|
||||||
refreshAll();
|
refreshAll();
|
||||||
loadLogs();
|
loadLogs();
|
||||||
|
maybeShowPromo();
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>GoTelegram Admin</title>
|
<title>goTelegram Pro Admin</title>
|
||||||
<script>
|
<script>
|
||||||
(function () {
|
(function () {
|
||||||
var stored = localStorage.getItem("gotelegram-theme");
|
var stored = localStorage.getItem("gotelegram-theme");
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
document.documentElement.dataset.theme = theme;
|
document.documentElement.dataset.theme = theme;
|
||||||
}());
|
}());
|
||||||
</script>
|
</script>
|
||||||
<link rel="stylesheet" href="/styles.css?v=2.5.0-admin4">
|
<link rel="stylesheet" href="/styles.css?v=2.5.0-admin5">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="app-shell">
|
<div class="app-shell">
|
||||||
@@ -19,18 +19,18 @@
|
|||||||
<div class="brand">
|
<div class="brand">
|
||||||
<div class="brand-mark">GT</div>
|
<div class="brand-mark">GT</div>
|
||||||
<div>
|
<div>
|
||||||
<strong>GoTelegram</strong>
|
<strong>goTelegram Pro</strong>
|
||||||
<span data-i18n="brandSubtitle">Local Admin</span>
|
<span data-i18n="brandSubtitle">Local Admin</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<nav class="nav-tabs" aria-label="Admin sections">
|
<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 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" data-i18n="navTraffic">Traffic</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" data-i18n="navKeys">Keys</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" data-i18n="navBackups">Backups</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" data-i18n="navLogs">Logs</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" data-i18n="navSettings">Settings</button>
|
<button type="button" class="nav-item" data-nav="settings"><span class="nav-icon">⚙</span><span data-i18n="navSettings">Settings</span></button>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div class="sidebar-foot">
|
<div class="sidebar-foot">
|
||||||
@@ -59,6 +59,19 @@
|
|||||||
|
|
||||||
<main class="content">
|
<main class="content">
|
||||||
<section class="page-panel active" data-page="dashboard">
|
<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">
|
<div class="metric-grid">
|
||||||
<article class="metric-card accent-blue">
|
<article class="metric-card accent-blue">
|
||||||
<span data-i18n="metricMode">Mode</span>
|
<span data-i18n="metricMode">Mode</span>
|
||||||
@@ -169,6 +182,7 @@
|
|||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th data-i18n="tableUser">User</th>
|
<th data-i18n="tableUser">User</th>
|
||||||
|
<th data-i18n="tableStatus">Status</th>
|
||||||
<th data-i18n="tableSecret">Secret</th>
|
<th data-i18n="tableSecret">Secret</th>
|
||||||
<th data-i18n="tableLink">Link</th>
|
<th data-i18n="tableLink">Link</th>
|
||||||
<th data-i18n="tableActions">Actions</th>
|
<th data-i18n="tableActions">Actions</th>
|
||||||
@@ -269,6 +283,27 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="toast" class="toast"></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>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -140,6 +140,10 @@ h2 {
|
|||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.brand strong {
|
||||||
|
letter-spacing: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.brand span {
|
.brand span {
|
||||||
display: block;
|
display: block;
|
||||||
color: var(--sidebar-muted);
|
color: var(--sidebar-muted);
|
||||||
@@ -153,6 +157,9 @@ h2 {
|
|||||||
|
|
||||||
.nav-item {
|
.nav-item {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
@@ -160,6 +167,18 @@ h2 {
|
|||||||
box-shadow: none;
|
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:hover,
|
||||||
.nav-item.active {
|
.nav-item.active {
|
||||||
background: rgba(255, 255, 255, .08);
|
background: rgba(255, 255, 255, .08);
|
||||||
@@ -251,6 +270,79 @@ h2 {
|
|||||||
gap: 18px;
|
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 {
|
.eyebrow {
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
@@ -549,6 +641,10 @@ tr:last-child td {
|
|||||||
border-bottom: 0;
|
border-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.disabled-row {
|
||||||
|
opacity: .72;
|
||||||
|
}
|
||||||
|
|
||||||
td code {
|
td code {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
max-width: 320px;
|
max-width: 320px;
|
||||||
@@ -567,6 +663,65 @@ td small {
|
|||||||
gap: 8px;
|
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 {
|
.empty-cell {
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
text-align: center;
|
text-align: center;
|
||||||
@@ -643,6 +798,64 @@ td small {
|
|||||||
transform: translateY(0);
|
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) {
|
@media (max-width: 1280px) {
|
||||||
.service-grid {
|
.service-grid {
|
||||||
grid-template-columns: repeat(3, minmax(150px, 1fr));
|
grid-template-columns: repeat(3, minmax(150px, 1fr));
|
||||||
@@ -681,6 +894,11 @@ td small {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 720px) {
|
@media (max-width: 720px) {
|
||||||
|
.visual-overview,
|
||||||
|
.promo-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
.topbar {
|
.topbar {
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
|
|||||||
@@ -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.
|
Production-quality Telegram bot for managing MTProxy (telemt engine) on Linux servers.
|
||||||
|
|
||||||
@@ -67,7 +67,7 @@ For systemd service:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
[Unit]
|
[Unit]
|
||||||
Description=GoTelegram Bot
|
Description=goTelegram Pro Bot
|
||||||
After=network.target
|
After=network.target
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
@@ -146,4 +146,4 @@ code, stdout, stderr = await sh("command", "arg1", "arg2")
|
|||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
GoTelegram v2.5.0 - Open source community project
|
goTelegram Pro v2.5.0 - Open source community project
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
#!/usr/bin/env python3
|
#!/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
|
Manages telemt engine via Telegram interface with full CLI feature parity
|
||||||
Uses python-telegram-bot v21+
|
Uses python-telegram-bot v21+
|
||||||
Supports EN/RU UI with per-user language preferences.
|
Supports EN/RU UI with per-user language preferences.
|
||||||
@@ -103,6 +103,7 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
GOTELEGRAM_VERSION = "2.5.0"
|
GOTELEGRAM_VERSION = "2.5.0"
|
||||||
GOTELEGRAM_CONFIG = "/opt/gotelegram/config.json"
|
GOTELEGRAM_CONFIG = "/opt/gotelegram/config.json"
|
||||||
|
DISABLED_USERS_FILE = "/opt/gotelegram/disabled_users.json"
|
||||||
TELEMT_CONFIG = "/etc/telemt/config.toml"
|
TELEMT_CONFIG = "/etc/telemt/config.toml"
|
||||||
TELEMT_SERVICE = "telemt"
|
TELEMT_SERVICE = "telemt"
|
||||||
WEBSITE_ROOT = "/var/www/gotelegram-site"
|
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_1 = "https://vk.cc/ct29NQ"
|
||||||
PROMO_LINK_2 = "https://vk.cc/cUxAhj"
|
PROMO_LINK_2 = "https://vk.cc/cUxAhj"
|
||||||
TIP_LINK = "https://pay.cloudtips.ru/p/7410814f"
|
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"
|
PROMO_STAMP_FILE = "/opt/gotelegram/.promo_bot_last_shown"
|
||||||
|
|
||||||
BOT_TOKEN = os.getenv("BOT_TOKEN")
|
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:
|
def save_telemt_users(users: Dict[str, str]) -> bool:
|
||||||
"""Persist [access.users] while keeping the rest of the TOML structure."""
|
"""Persist [access.users] while keeping the rest of the TOML structure."""
|
||||||
telemt_cfg = load_toml(TELEMT_CONFIG) or {}
|
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 = []
|
rows = []
|
||||||
for name in sorted(users):
|
for name in sorted(users, key=lambda item: (item != "main", item)):
|
||||||
rows.append([InlineKeyboardButton(f"👤 {name}", callback_data=f"user_view_{name}")])
|
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("➕ Добавить ключ", callback_data="user_add")])
|
||||||
rows.append([
|
rows.append([
|
||||||
InlineKeyboardButton(_t(user_id, "btn_refresh"), callback_data="menu_users"),
|
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
|
query = update.callback_query
|
||||||
await query.answer()
|
await query.answer()
|
||||||
user_id = _uid(update)
|
user_id = _uid(update)
|
||||||
users = load_telemt_users()
|
users = load_user_records()
|
||||||
|
|
||||||
if users:
|
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:
|
else:
|
||||||
user_lines = "<i>Ключей пока нет</i>"
|
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")
|
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)
|
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 = ""
|
details = ""
|
||||||
if api:
|
if api:
|
||||||
data = api.get("data", api)
|
data = api.get("data", api)
|
||||||
@@ -1656,12 +1705,16 @@ async def _user_detail_text(name: str, secret: str) -> str:
|
|||||||
else:
|
else:
|
||||||
compact = json.dumps(data, ensure_ascii=False)[:600]
|
compact = json.dumps(data, ensure_ascii=False)[:600]
|
||||||
details = f"\n<pre>{html.escape(compact)}</pre>"
|
details = f"\n<pre>{html.escape(compact)}</pre>"
|
||||||
|
elif enabled:
|
||||||
|
details = "\n<i>Runtime API недоступен. Новые установки goTelegram Pro включают его автоматически.</i>"
|
||||||
else:
|
else:
|
||||||
details = "\n<i>Runtime API недоступен. Новые установки GoTelegram включают его автоматически.</i>"
|
details = "\n<i>Ключ отключён и сейчас не принимается telemt.</i>"
|
||||||
|
|
||||||
link_line = html.escape(link) if link else "link unavailable"
|
link_line = html.escape(link) if link else "link unavailable"
|
||||||
|
status_line = "🟢 enabled" if enabled else "⏸ disabled"
|
||||||
return (
|
return (
|
||||||
f"<b>👤 {html.escape(name)}</b>\n\n"
|
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"Secret: <code>{html.escape(secret)}</code>\n\n"
|
||||||
f"<b>Ссылка:</b>\n<code>{link_line}</code>\n"
|
f"<b>Ссылка:</b>\n<code>{link_line}</code>\n"
|
||||||
f"{details}"
|
f"{details}"
|
||||||
@@ -1673,23 +1726,31 @@ async def cb_user_view(update: Update, context: ContextTypes.DEFAULT_TYPE) -> No
|
|||||||
await query.answer()
|
await query.answer()
|
||||||
user_id = _uid(update)
|
user_id = _uid(update)
|
||||||
name = query.data.removeprefix("user_view_")
|
name = query.data.removeprefix("user_view_")
|
||||||
users = load_telemt_users()
|
users = load_user_records()
|
||||||
secret = users.get(name)
|
record = users.get(name)
|
||||||
if not secret:
|
if not record:
|
||||||
await safe_edit_message(
|
await safe_edit_message(
|
||||||
query,
|
query,
|
||||||
"❌ Пользователь не найден.",
|
"❌ Пользователь не найден.",
|
||||||
reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton(_t(user_id, "btn_back"), callback_data="menu_users")]]),
|
reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton(_t(user_id, "btn_back"), callback_data="menu_users")]]),
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
enabled = bool(record.get("enabled"))
|
||||||
|
secret = str(record.get("secret", ""))
|
||||||
|
|
||||||
buttons = [
|
buttons = [
|
||||||
|
[InlineKeyboardButton("⏸ Отключить" if enabled else "▶️ Включить", callback_data=f"user_toggle_{name}")],
|
||||||
[InlineKeyboardButton("🗑 Удалить", callback_data=f"user_del_{name}")],
|
[InlineKeyboardButton("🗑 Удалить", callback_data=f"user_del_{name}")],
|
||||||
[InlineKeyboardButton(_t(user_id, "btn_back"), callback_data="menu_users")],
|
[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(
|
await safe_edit_message(
|
||||||
query,
|
query,
|
||||||
await _user_detail_text(name, secret),
|
await _user_detail_text(name, secret, enabled),
|
||||||
reply_markup=InlineKeyboardMarkup(buttons),
|
reply_markup=InlineKeyboardMarkup(buttons),
|
||||||
parse_mode="HTML",
|
parse_mode="HTML",
|
||||||
disable_web_page_preview=True,
|
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:
|
async def cb_user_delete(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||||
query = update.callback_query
|
query = update.callback_query
|
||||||
await query.answer()
|
await query.answer()
|
||||||
@@ -1735,12 +1835,15 @@ async def cb_user_delete_confirm(update: Update, context: ContextTypes.DEFAULT_T
|
|||||||
await query.answer()
|
await query.answer()
|
||||||
user_id = _uid(update)
|
user_id = _uid(update)
|
||||||
name = query.data.removeprefix("user_del_yes_")
|
name = query.data.removeprefix("user_del_yes_")
|
||||||
users = load_telemt_users()
|
active = load_telemt_users()
|
||||||
if name == "main" or name not in users:
|
disabled = load_disabled_users()
|
||||||
|
records = load_user_records()
|
||||||
|
if name == "main" or name not in records:
|
||||||
await query.answer("Нельзя удалить этот ключ", show_alert=True)
|
await query.answer("Нельзя удалить этот ключ", show_alert=True)
|
||||||
return
|
return
|
||||||
users.pop(name, None)
|
active.pop(name, None)
|
||||||
if not save_telemt_users(users):
|
disabled.pop(name, None)
|
||||||
|
if not save_telemt_users(active) or not save_disabled_users(disabled):
|
||||||
await safe_edit_message(query, "❌ Не удалось сохранить config.toml")
|
await safe_edit_message(query, "❌ Не удалось сохранить config.toml")
|
||||||
return
|
return
|
||||||
await refresh_telemt_after_user_change()
|
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):
|
if not _USER_NAME_RE.match(name):
|
||||||
await update.message.reply_text("❌ Некорректное имя. Используйте латиницу, цифры, _ . - и до 48 символов.")
|
await update.message.reply_text("❌ Некорректное имя. Используйте латиницу, цифры, _ . - и до 48 символов.")
|
||||||
return
|
return
|
||||||
users = load_telemt_users()
|
records = load_user_records()
|
||||||
if name in users:
|
if name in records:
|
||||||
await update.message.reply_text("❌ Такой пользователь уже есть.")
|
await update.message.reply_text("❌ Такой пользователь уже есть.")
|
||||||
return
|
return
|
||||||
|
users = load_telemt_users()
|
||||||
secret = hashlib.sha256(f"{name}:{time.time()}:{os.urandom(16).hex()}".encode()).hexdigest()[:32]
|
secret = hashlib.sha256(f"{name}:{time.time()}:{os.urandom(16).hex()}".encode()).hexdigest()[:32]
|
||||||
users[name] = secret
|
users[name] = secret
|
||||||
if not save_telemt_users(users):
|
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"Пользователь: <code>{html.escape(name)}</code>\n"
|
||||||
f"Secret: <code>{secret}</code>\n\n"
|
f"Secret: <code>{secret}</code>\n\n"
|
||||||
f"<code>{html.escape(link or '')}</code>",
|
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",
|
parse_mode="HTML",
|
||||||
disable_web_page_preview=True,
|
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:
|
def get_promo_text() -> str:
|
||||||
"""Return promo text with 2 hosters + donate."""
|
"""Return promo text with 2 hosters, optional YouTube link and donate."""
|
||||||
return (
|
text = (
|
||||||
"<b>💰 Хостинг #1 — скидка до 60%</b>\n"
|
"<b>💰 Хостинг #1 — скидка до 60%</b>\n"
|
||||||
f"<a href='{PROMO_LINK_1}'>{PROMO_LINK_1}</a>\n\n"
|
f"<a href='{PROMO_LINK_1}'>{PROMO_LINK_1}</a>\n\n"
|
||||||
"<b>Промокоды:</b>\n"
|
"<b>Промокоды:</b>\n"
|
||||||
@@ -2349,6 +2453,13 @@ def get_promo_text() -> str:
|
|||||||
"<b>☕ Донат / Чаевые</b>\n"
|
"<b>☕ Донат / Чаевые</b>\n"
|
||||||
f"<a href='{TIP_LINK}'>{TIP_LINK}</a>"
|
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:
|
def should_show_promo_bot() -> bool:
|
||||||
@@ -2393,7 +2504,7 @@ async def cb_menu_credits(update: Update, context: ContextTypes.DEFAULT_TYPE) ->
|
|||||||
|
|
||||||
text = (
|
text = (
|
||||||
f"<b>ℹ️ Credits & Acknowledgements</b>\n\n"
|
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"Built with love for the Telegram community\n\n"
|
||||||
f"<b>Special thanks to:</b>\n\n"
|
f"<b>Special thanks to:</b>\n\n"
|
||||||
f"🙏 <b>telemt</b> - MTProxy engine\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"🚀 <b>Start Bootstrap</b> - Bootstrap templates\n"
|
||||||
f" Professional design framework\n\n"
|
f" Professional design framework\n\n"
|
||||||
f"💬 <b>Community</b> - Your feedback & support\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(
|
keyboard = InlineKeyboardMarkup(
|
||||||
@@ -2425,7 +2536,7 @@ async def cb_menu_remove(update: Update, context: ContextTypes.DEFAULT_TYPE) ->
|
|||||||
await query.answer()
|
await query.answer()
|
||||||
|
|
||||||
text = (
|
text = (
|
||||||
"<b>⚠️ Remove GoTelegram</b>\n\n"
|
"<b>⚠️ Remove goTelegram Pro</b>\n\n"
|
||||||
"This will completely remove the installation.\n"
|
"This will completely remove the installation.\n"
|
||||||
"Are you sure?"
|
"Are you sure?"
|
||||||
)
|
)
|
||||||
@@ -2443,7 +2554,7 @@ async def cb_remove_confirm(update: Update, context: ContextTypes.DEFAULT_TYPE)
|
|||||||
query = update.callback_query
|
query = update.callback_query
|
||||||
await query.answer()
|
await query.answer()
|
||||||
|
|
||||||
await safe_edit_message(query,"⏳ Removing GoTelegram...")
|
await safe_edit_message(query,"⏳ Removing goTelegram Pro...")
|
||||||
|
|
||||||
# Stop service
|
# Stop service
|
||||||
await sh("systemctl", "stop", TELEMT_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]:
|
for path in ["/opt/gotelegram", WEBSITE_ROOT]:
|
||||||
await sh("rm", "-rf", path)
|
await sh("rm", "-rf", path)
|
||||||
|
|
||||||
text = "✅ GoTelegram removed successfully"
|
text = "✅ goTelegram Pro removed successfully"
|
||||||
keyboard = InlineKeyboardMarkup(
|
keyboard = InlineKeyboardMarkup(
|
||||||
[[InlineKeyboardButton(_t(_uid(update), "btn_back"), callback_data="menu_main")]]
|
[[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)
|
await cb_user_add(update, context)
|
||||||
elif data.startswith("user_view_"):
|
elif data.startswith("user_view_"):
|
||||||
await cb_user_view(update, context)
|
await cb_user_view(update, context)
|
||||||
|
elif data.startswith("user_toggle_"):
|
||||||
|
await cb_user_toggle(update, context)
|
||||||
elif data.startswith("user_del_yes_"):
|
elif data.startswith("user_del_yes_"):
|
||||||
await cb_user_delete_confirm(update, context)
|
await cb_user_delete_confirm(update, context)
|
||||||
elif data.startswith("user_del_"):
|
elif data.startswith("user_del_"):
|
||||||
@@ -2663,7 +2776,7 @@ async def handle_text_message(update: Update, context: ContextTypes.DEFAULT_TYPE
|
|||||||
if not ok:
|
if not ok:
|
||||||
await update.message.reply_text(_t(user_id, info), parse_mode="HTML")
|
await update.message.reply_text(_t(user_id, info), parse_mode="HTML")
|
||||||
return
|
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).
|
# field name written by install.sh/save_gotelegram_config).
|
||||||
config = load_json(GOTELEGRAM_CONFIG) or {}
|
config = load_json(GOTELEGRAM_CONFIG) or {}
|
||||||
config["template_id"] = tpl_id
|
config["template_id"] = tpl_id
|
||||||
@@ -2734,7 +2847,7 @@ def main() -> None:
|
|||||||
application.add_error_handler(error_handler)
|
application.add_error_handler(error_handler)
|
||||||
|
|
||||||
# Run the bot
|
# 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)
|
application.run_polling(allowed_updates=Update.ALL_TYPES)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
# Copy this to .env and fill in your values
|
||||||
|
|
||||||
# Telegram Bot Token from @BotFather
|
# Telegram Bot Token from @BotFather
|
||||||
|
|||||||
@@ -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.
|
Provides per-user language preferences and a simple t()/tf() API.
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
@@ -40,12 +40,12 @@ def _detect_default_lang() -> str:
|
|||||||
if isinstance(data, dict):
|
if isinstance(data, dict):
|
||||||
candidates.extend([data.get("language"), data.get("lang")])
|
candidates.extend([data.get("language"), data.get("lang")])
|
||||||
except Exception as e:
|
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:
|
try:
|
||||||
if GOTELEGRAM_LANG_MARKER.exists():
|
if GOTELEGRAM_LANG_MARKER.exists():
|
||||||
candidates.append(GOTELEGRAM_LANG_MARKER.read_text(encoding="utf-8").strip()[:2])
|
candidates.append(GOTELEGRAM_LANG_MARKER.read_text(encoding="utf-8").strip()[:2])
|
||||||
except Exception as e:
|
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", ""))
|
candidates.append(os.getenv("BOT_LANG", ""))
|
||||||
for raw in candidates:
|
for raw in candidates:
|
||||||
code = str(raw or "").strip().lower()
|
code = str(raw or "").strip().lower()
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
"lang_saved": "Language saved: %s",
|
"lang_saved": "Language saved: %s",
|
||||||
"lang_choose": "Choose your language:",
|
"lang_choose": "Choose your language:",
|
||||||
|
|
||||||
"welcome_title": "GoTelegram v%s",
|
"welcome_title": "goTelegram Pro v%s",
|
||||||
"welcome_subtitle": "🤖 MTProxy Management Bot",
|
"welcome_subtitle": "🤖 MTProxy Management Bot",
|
||||||
"welcome_powered": "Powered by telemt engine",
|
"welcome_powered": "Powered by telemt engine",
|
||||||
"welcome_prompt": "Select an action from the menu below:",
|
"welcome_prompt": "Select an action from the menu below:",
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
"btn_no": "❌ No",
|
"btn_no": "❌ No",
|
||||||
|
|
||||||
"access_denied": "⛔ Access denied.\nYour ID: <code>%s</code>",
|
"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.",
|
"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",
|
"menu_install": "⚙️ Install",
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
"lang_saved": "Язык сохранён: %s",
|
"lang_saved": "Язык сохранён: %s",
|
||||||
"lang_choose": "Выберите язык:",
|
"lang_choose": "Выберите язык:",
|
||||||
|
|
||||||
"welcome_title": "GoTelegram v%s",
|
"welcome_title": "goTelegram Pro v%s",
|
||||||
"welcome_subtitle": "🤖 Бот управления MTProxy",
|
"welcome_subtitle": "🤖 Бот управления MTProxy",
|
||||||
"welcome_powered": "На базе движка telemt",
|
"welcome_powered": "На базе движка telemt",
|
||||||
"welcome_prompt": "Выберите действие в меню ниже:",
|
"welcome_prompt": "Выберите действие в меню ниже:",
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
"btn_no": "❌ Нет",
|
"btn_no": "❌ Нет",
|
||||||
|
|
||||||
"access_denied": "⛔ Доступ запрещён.\nВаш ID: <code>%s</code>",
|
"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Используйте кнопки меню для остальных операций.",
|
"help_lines": "/start — Главное меню\n/help — Эта справка\n/status — Быстрый статус\n/logs — Последние логи\n/lang — Сменить язык\n/addadmin ID — Добавить админа\n/deladmin ID — Удалить админа\n\nИспользуйте кнопки меню для остальных операций.",
|
||||||
|
|
||||||
"menu_install": "⚙️ Установить",
|
"menu_install": "⚙️ Установить",
|
||||||
|
|||||||
41
install.sh
41
install.sh
@@ -1,6 +1,6 @@
|
|||||||
#!/bin/bash
|
#!/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)
|
# Anti-DPI • Fake TLS • TCP Splice • JA3/JA4 Resistance • i18n (EN/RU)
|
||||||
#
|
#
|
||||||
# Install:
|
# Install:
|
||||||
@@ -45,7 +45,7 @@ show_main_menu() {
|
|||||||
# ── Header (no right border — ANSI breaks alignment) ──
|
# ── Header (no right border — ANSI breaks alignment) ──
|
||||||
echo ""
|
echo ""
|
||||||
echo -e " ${BOLD}${CYAN}━${line}━${NC}"
|
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}"
|
echo -e " ${BOLD}${CYAN}━${line}━${NC}"
|
||||||
|
|
||||||
# ── Service health ──
|
# ── Service health ──
|
||||||
@@ -354,7 +354,7 @@ auto_migrate_legacy_state() {
|
|||||||
|
|
||||||
[ -f "$TELEMT_CONFIG" ] || [ -f "$GOTELEGRAM_CONFIG" ] || [ -d "$WEBSITE_ROOT" ] || return 0
|
[ -f "$TELEMT_CONFIG" ] || [ -f "$GOTELEGRAM_CONFIG" ] || [ -d "$WEBSITE_ROOT" ] || return 0
|
||||||
|
|
||||||
log_step "Миграция состояния GoTelegram"
|
log_step "Миграция состояния goTelegram Pro"
|
||||||
snapshot_preupgrade_state
|
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
|
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 [ -f "$TELEMT_CONFIG" ]; then
|
||||||
if ! grep -q '\[server.api\]' "$TELEMT_CONFIG" 2>/dev/null || \
|
if ! grep -q '\[server.api\]' "$TELEMT_CONFIG" 2>/dev/null || \
|
||||||
! grep -q 'metrics_listen' "$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
|
generate_telemt_toml "$secret" "$port" "$mode" "$mask_host" "$mask_port" "$TELEMT_CONFIG" >&2
|
||||||
replace_telemt_users_block "$users_block" "$TELEMT_CONFIG"
|
replace_telemt_users_block "$users_block" "$TELEMT_CONFIG"
|
||||||
changed=1
|
changed=1
|
||||||
@@ -519,7 +519,7 @@ install_lite_mode() {
|
|||||||
# Start
|
# Start
|
||||||
start_telemt || return
|
start_telemt || return
|
||||||
|
|
||||||
# Save GoTelegram config
|
# Save goTelegram Pro config
|
||||||
save_gotelegram_config "telemt" "lite" "$port" "$secret" "$domain" "" ""
|
save_gotelegram_config "telemt" "lite" "$port" "$secret" "$domain" "" ""
|
||||||
|
|
||||||
# Credits
|
# Credits
|
||||||
@@ -905,7 +905,7 @@ install_admin_web() {
|
|||||||
python_bin=$(command -v python3)
|
python_bin=$(command -v python3)
|
||||||
cat > "/etc/systemd/system/${ADMIN_WEB_SERVICE}.service" << SVCEOF
|
cat > "/etc/systemd/system/${ADMIN_WEB_SERVICE}.service" << SVCEOF
|
||||||
[Unit]
|
[Unit]
|
||||||
Description=GoTelegram v${GOTELEGRAM_VERSION} Local Web Admin
|
Description=goTelegram Pro v${GOTELEGRAM_VERSION} Local Web Admin
|
||||||
After=network.target
|
After=network.target
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
@@ -1190,7 +1190,7 @@ bot_install() {
|
|||||||
# Systemd
|
# Systemd
|
||||||
cat > "/etc/systemd/system/${BOT_SERVICE}.service" << SVCEOF
|
cat > "/etc/systemd/system/${BOT_SERVICE}.service" << SVCEOF
|
||||||
[Unit]
|
[Unit]
|
||||||
Description=GoTelegram v${GOTELEGRAM_VERSION} Telegram Bot
|
Description=goTelegram Pro v${GOTELEGRAM_VERSION} Telegram Bot
|
||||||
After=network.target
|
After=network.target
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
@@ -1380,6 +1380,7 @@ bot_remove() {
|
|||||||
_promo_block() {
|
_promo_block() {
|
||||||
# Print a promo section without width-fragile box borders (i18n safe)
|
# Print a promo section without width-fragile box borders (i18n safe)
|
||||||
local line2; line2=$(printf '─%.0s' {1..54})
|
local line2; line2=$(printf '─%.0s' {1..54})
|
||||||
|
local youtube_link="${GOTELEGRAM_YOUTUBE_LINK:-}"
|
||||||
echo ""
|
echo ""
|
||||||
echo -e " ${DIM}${line2}${NC}"
|
echo -e " ${DIM}${line2}${NC}"
|
||||||
echo -e " ${BOLD}${YELLOW}$(t promo_host1_title)${NC}"
|
echo -e " ${BOLD}${YELLOW}$(t promo_host1_title)${NC}"
|
||||||
@@ -1394,6 +1395,11 @@ _promo_block() {
|
|||||||
echo -e " ${DIM}${line2}${NC}"
|
echo -e " ${DIM}${line2}${NC}"
|
||||||
echo -e " ${BOLD}${YELLOW}$(t promo_tips_title)${NC}"
|
echo -e " ${BOLD}${YELLOW}$(t promo_tips_title)${NC}"
|
||||||
echo -e " ${CYAN}https://pay.cloudtips.ru/p/7410814f${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 -e " ${DIM}${line2}${NC}"
|
||||||
echo ""
|
echo ""
|
||||||
}
|
}
|
||||||
@@ -1423,19 +1429,24 @@ mark_promo_shown() {
|
|||||||
date +%s > "$GOTELEGRAM_DIR/.promo_last_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) ───────────────────
|
# ── Promo with QR + delay (on install + once per day) ───────────────────
|
||||||
# QR показываем ТОЛЬКО для чаевых/донатов. Для хостеров оставлены только
|
|
||||||
# текстовые ссылки и промокоды (см. _promo_block) — QR-коды хостеров
|
|
||||||
# визуально конкурировали с чаевыми и перегружали экран.
|
|
||||||
show_promo_with_qr() {
|
show_promo_with_qr() {
|
||||||
_promo_block
|
_promo_block
|
||||||
|
|
||||||
# QR только для чаевых
|
|
||||||
if command -v qrencode &>/dev/null; then
|
if command -v qrencode &>/dev/null; then
|
||||||
echo -e " ${DIM}$(t promo_qr_tips)${NC}"
|
_promo_qr "$(t promo_qr_host1)" "https://vk.cc/ct29NQ"
|
||||||
qrencode -t UTF8 -m 1 "https://pay.cloudtips.ru/p/7410814f" 2>/dev/null | while IFS= read -r qr_line; do
|
_promo_qr "$(t promo_qr_host2)" "https://vk.cc/cUxAhj"
|
||||||
echo " $qr_line"
|
_promo_qr "$(t promo_qr_tips)" "https://pay.cloudtips.ru/p/7410814f"
|
||||||
done
|
_promo_qr "$(t promo_qr_youtube)" "${GOTELEGRAM_YOUTUBE_LINK:-}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
mark_promo_shown
|
mark_promo_shown
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# GoTelegram v2.5.0 — backup and restore (i18n-aware)
|
# goTelegram Pro v2.5.0 — backup and restore (i18n-aware)
|
||||||
|
|
||||||
# ── Создание бекапа ──────────────────────────────────────────────────────────
|
# ── Создание бекапа ──────────────────────────────────────────────────────────
|
||||||
create_backup() {
|
create_backup() {
|
||||||
@@ -20,10 +20,13 @@ create_backup() {
|
|||||||
cp "$TELEMT_CONFIG" "$tmp_dir/config.toml"
|
cp "$TELEMT_CONFIG" "$tmp_dir/config.toml"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# GoTelegram конфиг
|
# goTelegram Pro конфиг
|
||||||
if [ -f "$GOTELEGRAM_CONFIG" ]; then
|
if [ -f "$GOTELEGRAM_CONFIG" ]; then
|
||||||
cp "$GOTELEGRAM_CONFIG" "$tmp_dir/gotelegram.json"
|
cp "$GOTELEGRAM_CONFIG" "$tmp_dir/gotelegram.json"
|
||||||
fi
|
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)
|
# Language marker (i18n)
|
||||||
if [ -f "$GOTELEGRAM_DIR/.language" ]; then
|
if [ -f "$GOTELEGRAM_DIR/.language" ]; then
|
||||||
@@ -97,7 +100,7 @@ create_backup() {
|
|||||||
|
|
||||||
cat > "$tmp_dir/metadata.json" << EOMETA
|
cat > "$tmp_dir/metadata.json" << EOMETA
|
||||||
{
|
{
|
||||||
"backup_version": "1.3",
|
"backup_version": "1.4",
|
||||||
"gotelegram_version": "$GOTELEGRAM_VERSION",
|
"gotelegram_version": "$GOTELEGRAM_VERSION",
|
||||||
"created_at": "$(date -Iseconds)",
|
"created_at": "$(date -Iseconds)",
|
||||||
"hostname": "$(hostname)",
|
"hostname": "$(hostname)",
|
||||||
@@ -241,12 +244,17 @@ restore_backup() {
|
|||||||
log_success "$(_t_or backup_restored_telemt 'telemt конфиг восстановлен')"
|
log_success "$(_t_or backup_restored_telemt 'telemt конфиг восстановлен')"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Восстанавливаем GoTelegram конфиг
|
# Восстанавливаем goTelegram Pro конфиг
|
||||||
if [ -f "$backup_dir/gotelegram.json" ]; then
|
if [ -f "$backup_dir/gotelegram.json" ]; then
|
||||||
mkdir -p "$GOTELEGRAM_DIR"
|
mkdir -p "$GOTELEGRAM_DIR"
|
||||||
cp "$backup_dir/gotelegram.json" "$GOTELEGRAM_CONFIG"
|
cp "$backup_dir/gotelegram.json" "$GOTELEGRAM_CONFIG"
|
||||||
log_success "$(_t_or backup_restored_gotelegram 'GoTelegram конфиг восстановлен')"
|
log_success "$(_t_or backup_restored_gotelegram 'GoTelegram конфиг восстановлен')"
|
||||||
fi
|
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)
|
# Восстанавливаем language marker (i18n)
|
||||||
if [ -f "$backup_dir/.language" ]; then
|
if [ -f "$backup_dir/.language" ]; then
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# GoTelegram v2.5.0 — common utilities
|
# goTelegram Pro v2.5.0 — common utilities
|
||||||
# Colors, logging, spinner, system helpers, v1 compat, i18n-aware
|
# Colors, logging, spinner, system helpers, v1 compat, i18n-aware
|
||||||
|
|
||||||
# ── Version ───────────────────────────────────────────────────────────────────
|
# ── Version ───────────────────────────────────────────────────────────────────
|
||||||
GOTELEGRAM_VERSION="2.5.0"
|
GOTELEGRAM_VERSION="2.5.0"
|
||||||
GOTELEGRAM_NAME="GoTelegram"
|
GOTELEGRAM_NAME="goTelegram Pro"
|
||||||
|
|
||||||
# ── Пути ──────────────────────────────────────────────────────────────────────
|
# ── Пути ──────────────────────────────────────────────────────────────────────
|
||||||
GOTELEGRAM_DIR="/opt/gotelegram"
|
GOTELEGRAM_DIR="/opt/gotelegram"
|
||||||
@@ -123,7 +123,7 @@ show_banner() {
|
|||||||
echo -e " ${DIM}$(t banner_subtitle)${NC}"
|
echo -e " ${DIM}$(t banner_subtitle)${NC}"
|
||||||
echo -e " ${DIM}$(t banner_features)${NC}"
|
echo -e " ${DIM}$(t banner_features)${NC}"
|
||||||
else
|
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}MTProxy powered by telemt (Rust + Tokio)${NC}"
|
||||||
echo -e " ${DIM}Anti-DPI • Fake TLS • TCP Splice • JA3/JA4${NC}"
|
echo -e " ${DIM}Anti-DPI • Fake TLS • TCP Splice • JA3/JA4${NC}"
|
||||||
fi
|
fi
|
||||||
@@ -483,26 +483,26 @@ detect_3xui_443_listener() {
|
|||||||
warn_3xui_443_conflict() {
|
warn_3xui_443_conflict() {
|
||||||
detect_3xui_443_listener || return 1
|
detect_3xui_443_listener || return 1
|
||||||
log_warning "Обнаружен 3x-ui/Xray, который уже слушает TCP/443."
|
log_warning "Обнаружен 3x-ui/Xray, который уже слушает TCP/443."
|
||||||
log_warning "GoTelegram не будет молча останавливать или переписывать 3x-ui."
|
log_warning "goTelegram Pro не будет молча останавливать или переписывать 3x-ui."
|
||||||
log_dim "Для настоящего shared-443 нужен один фронтовой TLS/SNI-диспетчер и разные SNI-домены для Xray и GoTelegram."
|
log_dim "Для настоящего shared-443 нужен один фронтовой TLS/SNI-диспетчер и разные SNI-домены для Xray и goTelegram Pro."
|
||||||
mkdir -p "$GOTELEGRAM_DIR" 2>/dev/null
|
mkdir -p "$GOTELEGRAM_DIR" 2>/dev/null
|
||||||
cat > "$GOTELEGRAM_DIR/shared-443-3xui.md" <<'EOF' 2>/dev/null || true
|
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
|
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:
|
needs one front TLS/SNI dispatcher on 443 and internal backends, for example:
|
||||||
|
|
||||||
- dispatcher: 0.0.0.0:443
|
- 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
|
- 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
|
The dispatcher must route Xray SNI domains to Xray and route the goTelegram Pro
|
||||||
SNI domain to telemt. If Xray and GoTelegram use the same SNI domain, automatic
|
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.
|
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
|
Xray config without explicit operator confirmation, because 3x-ui can overwrite
|
||||||
manual JSON edits on the next panel change.
|
manual JSON edits on the next panel change.
|
||||||
EOF
|
EOF
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# GoTelegram v2.5.0 — English translations
|
# goTelegram Pro v2.5.0 — English translations
|
||||||
# shellcheck disable=SC2034,SC2148
|
# shellcheck disable=SC2034,SC2148
|
||||||
|
|
||||||
# ── Common words ────────────────────────────────────────────────────────
|
# ── Common words ────────────────────────────────────────────────────────
|
||||||
@@ -25,7 +25,7 @@ I18N[success]="Done"
|
|||||||
I18N[wait]="Please wait..."
|
I18N[wait]="Please wait..."
|
||||||
|
|
||||||
# ── Banner ──────────────────────────────────────────────────────────────
|
# ── 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_subtitle]="MTProxy powered by telemt (Rust + Tokio)"
|
||||||
I18N[banner_features]="Anti-DPI • Fake TLS • TCP Splice • JA3/JA4"
|
I18N[banner_features]="Anti-DPI • Fake TLS • TCP Splice • JA3/JA4"
|
||||||
I18N[credits_title]="Credits / Thanks"
|
I18N[credits_title]="Credits / Thanks"
|
||||||
@@ -75,7 +75,7 @@ I18N[submenu_about_title]="ℹ️ ABOUT"
|
|||||||
I18N[about_version_info]="Version info"
|
I18N[about_version_info]="Version info"
|
||||||
I18N[about_promo]="Promo / Donate"
|
I18N[about_promo]="Promo / Donate"
|
||||||
I18N[version_title]="🔍 Information"
|
I18N[version_title]="🔍 Information"
|
||||||
I18N[version_label]="GoTelegram:"
|
I18N[version_label]="goTelegram Pro:"
|
||||||
I18N[version_engine]="Engine:"
|
I18N[version_engine]="Engine:"
|
||||||
I18N[version_tech]="Technology:"
|
I18N[version_tech]="Technology:"
|
||||||
I18N[version_license]="License:"
|
I18N[version_license]="License:"
|
||||||
@@ -106,7 +106,7 @@ I18N[install_cfg_mode]="Mode:"
|
|||||||
I18N[install_cfg_domain]="Domain:"
|
I18N[install_cfg_domain]="Domain:"
|
||||||
I18N[install_confirm_proxy]="Install proxy?"
|
I18N[install_confirm_proxy]="Install proxy?"
|
||||||
I18N[install_confirm_proxy_site]="Install proxy + website?"
|
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_desc1]="telemt accepts all traffic on 443 (HTTPS masquerade)"
|
||||||
I18N[install_arch_desc2]="nginx serves the site on internal port %s"
|
I18N[install_arch_desc2]="nginx serves the site on internal port %s"
|
||||||
I18N[install_arch_desc3]="ISP only sees HTTPS traffic to %s:443"
|
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 ────────────────────────────────────────────────────────
|
# ── Link / Share ────────────────────────────────────────────────────────
|
||||||
I18N[link_title]="🔗 Connection link:"
|
I18N[link_title]="🔗 Connection link:"
|
||||||
I18N[share_title]="📤 Forward this message:"
|
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_server]="🌍 Server: %s"
|
||||||
I18N[share_port]="🔌 Port: %s"
|
I18N[share_port]="🔌 Port: %s"
|
||||||
I18N[share_connect_cta]="👉 Connect with one tap:"
|
I18N[share_connect_cta]="👉 Connect with one tap:"
|
||||||
@@ -141,7 +141,7 @@ I18N[website_restart_nginx]="Restart nginx"
|
|||||||
I18N[website_change_template]="Change template"
|
I18N[website_change_template]="Change template"
|
||||||
|
|
||||||
# ── Remove ──────────────────────────────────────────────────────────────
|
# ── Remove ──────────────────────────────────────────────────────────────
|
||||||
I18N[remove_title]="🗑 Remove GoTelegram"
|
I18N[remove_title]="🗑 Remove goTelegram Pro"
|
||||||
I18N[remove_proxy_only]="Remove proxy only (telemt)"
|
I18N[remove_proxy_only]="Remove proxy only (telemt)"
|
||||||
I18N[remove_bot_only]="Remove Telegram bot only"
|
I18N[remove_bot_only]="Remove Telegram bot only"
|
||||||
I18N[remove_all]="Remove everything (proxy + bot + settings)"
|
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_warn_all]="This will remove EVERYTHING: proxy, bot, site, settings."
|
||||||
I18N[remove_confirm_all]="Are you absolutely sure?"
|
I18N[remove_confirm_all]="Are you absolutely sure?"
|
||||||
I18N[remove_proxy_done]="Proxy removed"
|
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 ────────────────────────────────────────────────
|
# ── Telegram bot submenu ────────────────────────────────────────────────
|
||||||
I18N[bot_title]="🤖 Telegram bot"
|
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_host1_title]="💰 HOSTING #1 — UP TO 60% OFF"
|
||||||
I18N[promo_host2_title]="💰 HOSTING #2 — UP TO 60% OFF"
|
I18N[promo_host2_title]="💰 HOSTING #2 — UP TO 60% OFF"
|
||||||
I18N[promo_tips_title]="☕ Donate / Tips"
|
I18N[promo_tips_title]="☕ Donate / Tips"
|
||||||
|
I18N[promo_youtube_title]="▶ YouTube Channel"
|
||||||
I18N[promo_link_label]="Link:"
|
I18N[promo_link_label]="Link:"
|
||||||
I18N[promo_off60]="60%% discount on the first month"
|
I18N[promo_off60]="60%% discount on the first month"
|
||||||
I18N[promo_ant20]="20%% + 3%% when paid for 3 months"
|
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_host1]="── QR: Hosting #1 ──"
|
||||||
I18N[promo_qr_host2]="── QR: Hosting #2 ──"
|
I18N[promo_qr_host2]="── QR: Hosting #2 ──"
|
||||||
I18N[promo_qr_tips]="── QR: Donate / Tips ──"
|
I18N[promo_qr_tips]="── QR: Donate / Tips ──"
|
||||||
|
I18N[promo_qr_youtube]="── QR: YouTube Channel ──"
|
||||||
I18N[promo_menu_in]="Menu in %d sec..."
|
I18N[promo_menu_in]="Menu in %d sec..."
|
||||||
|
|
||||||
# ── Stats ───────────────────────────────────────────────────────────────
|
# ── Stats ───────────────────────────────────────────────────────────────
|
||||||
@@ -330,7 +332,7 @@ I18N[backup_lang_label]="Language"
|
|||||||
I18N[backup_date_label]="Date"
|
I18N[backup_date_label]="Date"
|
||||||
I18N[backup_confirm_restore]="Restore configuration? Current settings will be overwritten."
|
I18N[backup_confirm_restore]="Restore configuration? Current settings will be overwritten."
|
||||||
I18N[backup_restored_telemt]="telemt config restored"
|
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_lang]="Interface language restored"
|
||||||
I18N[backup_restored_nginx]="nginx config restored"
|
I18N[backup_restored_nginx]="nginx config restored"
|
||||||
I18N[backup_restored_ssl]="SSL certificates 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"
|
I18N[deps_installing]="Installing dependencies: %s"
|
||||||
|
|
||||||
# ── Migration ───────────────────────────────────────────────────────────
|
# ── 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_container]="Container: %s"
|
||||||
I18N[v1_migration_step]="Migrating from v1 (mtg) to v2 (telemt)"
|
I18N[v1_migration_step]="Migrating from v1 (mtg) to v2 (telemt)"
|
||||||
I18N[v1_found_title]="Found v1 (mtg) installation:"
|
I18N[v1_found_title]="Found v1 (mtg) installation:"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# GoTelegram v2.5.0 — Russian translations
|
# goTelegram Pro v2.5.0 — Russian translations
|
||||||
# shellcheck disable=SC2034,SC2148
|
# shellcheck disable=SC2034,SC2148
|
||||||
|
|
||||||
# ── Common words ────────────────────────────────────────────────────────
|
# ── Common words ────────────────────────────────────────────────────────
|
||||||
@@ -25,7 +25,7 @@ I18N[success]="Готово"
|
|||||||
I18N[wait]="Подождите..."
|
I18N[wait]="Подождите..."
|
||||||
|
|
||||||
# ── Banner ──────────────────────────────────────────────────────────────
|
# ── Banner ──────────────────────────────────────────────────────────────
|
||||||
I18N[banner_title]="GoTelegram v%s"
|
I18N[banner_title]="goTelegram Pro v%s"
|
||||||
I18N[banner_subtitle]="MTProxy на ядре telemt (Rust + Tokio)"
|
I18N[banner_subtitle]="MTProxy на ядре telemt (Rust + Tokio)"
|
||||||
I18N[banner_features]="Anti-DPI • Fake TLS • TCP Splice • JA3/JA4"
|
I18N[banner_features]="Anti-DPI • Fake TLS • TCP Splice • JA3/JA4"
|
||||||
I18N[credits_title]="Благодарности / Credits"
|
I18N[credits_title]="Благодарности / Credits"
|
||||||
@@ -75,7 +75,7 @@ I18N[submenu_about_title]="ℹ️ О ПРОГРАММЕ"
|
|||||||
I18N[about_version_info]="Информация о версии"
|
I18N[about_version_info]="Информация о версии"
|
||||||
I18N[about_promo]="Промо / Донат"
|
I18N[about_promo]="Промо / Донат"
|
||||||
I18N[version_title]="🔍 Информация"
|
I18N[version_title]="🔍 Информация"
|
||||||
I18N[version_label]="GoTelegram:"
|
I18N[version_label]="goTelegram Pro:"
|
||||||
I18N[version_engine]="Ядро:"
|
I18N[version_engine]="Ядро:"
|
||||||
I18N[version_tech]="Технология:"
|
I18N[version_tech]="Технология:"
|
||||||
I18N[version_license]="Лицензия:"
|
I18N[version_license]="Лицензия:"
|
||||||
@@ -106,7 +106,7 @@ I18N[install_cfg_mode]="Режим:"
|
|||||||
I18N[install_cfg_domain]="Домен:"
|
I18N[install_cfg_domain]="Домен:"
|
||||||
I18N[install_confirm_proxy]="Установить прокси?"
|
I18N[install_confirm_proxy]="Установить прокси?"
|
||||||
I18N[install_confirm_proxy_site]="Установить прокси + сайт?"
|
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_desc1]="telemt принимает весь трафик на 443 (маскировка под HTTPS)"
|
||||||
I18N[install_arch_desc2]="nginx обслуживает сайт на внутреннем порту %s"
|
I18N[install_arch_desc2]="nginx обслуживает сайт на внутреннем порту %s"
|
||||||
I18N[install_arch_desc3]="Провайдер видит только HTTPS-трафик к %s:443"
|
I18N[install_arch_desc3]="Провайдер видит только HTTPS-трафик к %s:443"
|
||||||
@@ -125,7 +125,7 @@ I18N[logs_telemt_title]="📋 Логи telemt (последние %s строк)
|
|||||||
# ── Link / Share ────────────────────────────────────────────────────────
|
# ── Link / Share ────────────────────────────────────────────────────────
|
||||||
I18N[link_title]="🔗 Ссылка для подключения:"
|
I18N[link_title]="🔗 Ссылка для подключения:"
|
||||||
I18N[share_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_server]="🌍 Сервер: %s"
|
||||||
I18N[share_port]="🔌 Порт: %s"
|
I18N[share_port]="🔌 Порт: %s"
|
||||||
I18N[share_connect_cta]="👉 Подключиться одним нажатием:"
|
I18N[share_connect_cta]="👉 Подключиться одним нажатием:"
|
||||||
@@ -141,7 +141,7 @@ I18N[website_restart_nginx]="Перезапустить nginx"
|
|||||||
I18N[website_change_template]="Сменить шаблон"
|
I18N[website_change_template]="Сменить шаблон"
|
||||||
|
|
||||||
# ── Remove ──────────────────────────────────────────────────────────────
|
# ── Remove ──────────────────────────────────────────────────────────────
|
||||||
I18N[remove_title]="🗑 Удаление GoTelegram"
|
I18N[remove_title]="🗑 Удаление goTelegram Pro"
|
||||||
I18N[remove_proxy_only]="Удалить только прокси (telemt)"
|
I18N[remove_proxy_only]="Удалить только прокси (telemt)"
|
||||||
I18N[remove_bot_only]="Удалить только Telegram-бота"
|
I18N[remove_bot_only]="Удалить только Telegram-бота"
|
||||||
I18N[remove_all]="Удалить всё (прокси + бот + настройки)"
|
I18N[remove_all]="Удалить всё (прокси + бот + настройки)"
|
||||||
@@ -151,7 +151,7 @@ I18N[remove_backup_before]="Сделать бекап перед удалени
|
|||||||
I18N[remove_warn_all]="Это удалит ВСЁ: прокси, бот, сайт, настройки."
|
I18N[remove_warn_all]="Это удалит ВСЁ: прокси, бот, сайт, настройки."
|
||||||
I18N[remove_confirm_all]="Вы точно уверены?"
|
I18N[remove_confirm_all]="Вы точно уверены?"
|
||||||
I18N[remove_proxy_done]="Прокси удалён"
|
I18N[remove_proxy_done]="Прокси удалён"
|
||||||
I18N[remove_all_done]="GoTelegram полностью удалён (прокси + бот)"
|
I18N[remove_all_done]="goTelegram Pro полностью удалён (прокси + бот)"
|
||||||
|
|
||||||
# ── Telegram bot submenu ────────────────────────────────────────────────
|
# ── Telegram bot submenu ────────────────────────────────────────────────
|
||||||
I18N[bot_title]="🤖 Telegram-бот"
|
I18N[bot_title]="🤖 Telegram-бот"
|
||||||
@@ -218,6 +218,7 @@ I18N[bot_access_ids_fmt]="ID: %s"
|
|||||||
I18N[promo_host1_title]="💰 ХОСТИНГ #1 — СКИДКА ДО 60%"
|
I18N[promo_host1_title]="💰 ХОСТИНГ #1 — СКИДКА ДО 60%"
|
||||||
I18N[promo_host2_title]="💰 ХОСТИНГ #2 — СКИДКА ДО 60%"
|
I18N[promo_host2_title]="💰 ХОСТИНГ #2 — СКИДКА ДО 60%"
|
||||||
I18N[promo_tips_title]="☕ Донат / Чаевые"
|
I18N[promo_tips_title]="☕ Донат / Чаевые"
|
||||||
|
I18N[promo_youtube_title]="▶ YouTube-канал"
|
||||||
I18N[promo_link_label]="Ссылка:"
|
I18N[promo_link_label]="Ссылка:"
|
||||||
I18N[promo_off60]="60%% скидки на первый месяц"
|
I18N[promo_off60]="60%% скидки на первый месяц"
|
||||||
I18N[promo_ant20]="20%% + 3%% при оплате за 3 месяца"
|
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_host1]="── QR: Хостинг #1 ──"
|
||||||
I18N[promo_qr_host2]="── QR: Хостинг #2 ──"
|
I18N[promo_qr_host2]="── QR: Хостинг #2 ──"
|
||||||
I18N[promo_qr_tips]="── QR: Чаевые / Донат ──"
|
I18N[promo_qr_tips]="── QR: Чаевые / Донат ──"
|
||||||
|
I18N[promo_qr_youtube]="── QR: YouTube-канал ──"
|
||||||
I18N[promo_menu_in]="Меню через %d сек..."
|
I18N[promo_menu_in]="Меню через %d сек..."
|
||||||
|
|
||||||
# ── Stats ───────────────────────────────────────────────────────────────
|
# ── Stats ───────────────────────────────────────────────────────────────
|
||||||
@@ -330,7 +332,7 @@ I18N[backup_lang_label]="Язык"
|
|||||||
I18N[backup_date_label]="Дата"
|
I18N[backup_date_label]="Дата"
|
||||||
I18N[backup_confirm_restore]="Восстановить конфигурацию? Текущие настройки будут перезаписаны."
|
I18N[backup_confirm_restore]="Восстановить конфигурацию? Текущие настройки будут перезаписаны."
|
||||||
I18N[backup_restored_telemt]="telemt конфиг восстановлен"
|
I18N[backup_restored_telemt]="telemt конфиг восстановлен"
|
||||||
I18N[backup_restored_gotelegram]="GoTelegram конфиг восстановлен"
|
I18N[backup_restored_gotelegram]="goTelegram Pro конфиг восстановлен"
|
||||||
I18N[backup_restored_lang]="Язык интерфейса восстановлен"
|
I18N[backup_restored_lang]="Язык интерфейса восстановлен"
|
||||||
I18N[backup_restored_nginx]="nginx конфиг восстановлен"
|
I18N[backup_restored_nginx]="nginx конфиг восстановлен"
|
||||||
I18N[backup_restored_ssl]="SSL сертификаты восстановлены"
|
I18N[backup_restored_ssl]="SSL сертификаты восстановлены"
|
||||||
@@ -360,7 +362,7 @@ I18N[auto_refresh]="Обновление через 30 сек"
|
|||||||
I18N[deps_installing]="Установка зависимостей: %s"
|
I18N[deps_installing]="Установка зависимостей: %s"
|
||||||
|
|
||||||
# ── Migration ───────────────────────────────────────────────────────────
|
# ── Migration ───────────────────────────────────────────────────────────
|
||||||
I18N[v1_detected]="⚠️ Обнаружена установка GoTelegram v1 (mtg)"
|
I18N[v1_detected]="⚠️ Обнаружена установка goTelegram Pro v1 (mtg)"
|
||||||
I18N[v1_container]="Контейнер: %s"
|
I18N[v1_container]="Контейнер: %s"
|
||||||
I18N[v1_migration_step]="Миграция с v1 (mtg) на v2 (telemt)"
|
I18N[v1_migration_step]="Миграция с v1 (mtg) на v2 (telemt)"
|
||||||
I18N[v1_found_title]="Найдена установка v1 (mtg):"
|
I18N[v1_found_title]="Найдена установка v1 (mtg):"
|
||||||
|
|||||||
Reference in New Issue
Block a user