mirror of
https://github.com/anten-ka/gotelegram_pro.git
synced 2026-05-19 16:46:03 +00:00
Compare commits
40 Commits
v2.4.10
...
rollback-a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4dc286908e | ||
|
|
7bb88f2c24 | ||
|
|
ffea9f5d77 | ||
|
|
5714714d3c | ||
|
|
6840d309af | ||
|
|
2f3607e1e6 | ||
|
|
5a6bc9f614 | ||
|
|
cae552ec87 | ||
|
|
98e4be8831 | ||
|
|
d62991857f | ||
|
|
7b1a79bba9 | ||
|
|
b58bc9e1ba | ||
|
|
1e27d6ba71 | ||
|
|
a143c01a9a | ||
|
|
507a2979e5 | ||
|
|
bd3fc1af18 | ||
|
|
817ea9ab9f | ||
|
|
d492e5eb69 | ||
|
|
b2ab0dca57 | ||
|
|
c7540a97f7 | ||
|
|
63b564f70f | ||
|
|
c1b5ffc5a7 | ||
|
|
7eaeef8b49 | ||
|
|
5225811b3c | ||
|
|
d74b05ccf8 | ||
|
|
d8ec62eb07 | ||
|
|
e54778c08c | ||
|
|
6b89c3ea81 | ||
|
|
a393bd79ff | ||
|
|
3b075a7ed7 | ||
|
|
9c74a0d00f | ||
|
|
3ab0f9d5c7 | ||
|
|
dc7452930a | ||
|
|
d9e4831e44 | ||
|
|
008143a617 | ||
|
|
8804319e19 | ||
|
|
fe6d91c3a5 | ||
|
|
20103ccac8 | ||
|
|
ed9073f28f | ||
|
|
7afeb59261 |
51
.gitignore
vendored
Normal file
51
.gitignore
vendored
Normal file
@@ -0,0 +1,51 @@
|
||||
# Local secrets and environment
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
*.pem
|
||||
*.key
|
||||
*.p12
|
||||
id_rsa*
|
||||
known_hosts
|
||||
|
||||
# Python/runtime cache
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
.pytest_cache/
|
||||
.mypy_cache/
|
||||
.ruff_cache/
|
||||
.coverage
|
||||
htmlcov/
|
||||
venv/
|
||||
.venv/
|
||||
|
||||
# Node/runtime cache
|
||||
node_modules/
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Logs, temporary files, generated backups
|
||||
*.log
|
||||
*.tmp
|
||||
*.bak
|
||||
*.old
|
||||
*~
|
||||
backup/
|
||||
backups/
|
||||
dumps/
|
||||
*.tar
|
||||
*.tar.gz
|
||||
*.zip
|
||||
|
||||
# OS/editor noise
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
.idea/
|
||||
.vscode/
|
||||
|
||||
# goTelegram local/generated state
|
||||
gotelegram_state/
|
||||
user_stats_history.csv
|
||||
disabled_users.json
|
||||
backup_schedule.json
|
||||
100
DOCS_AI.md
100
DOCS_AI.md
@@ -1,8 +1,8 @@
|
||||
# GoTelegram Pro — техническая документация для ИИ-агентов
|
||||
# goTelegram Pro — техническая документация для ИИ-агентов
|
||||
|
||||
**Версия:** 2.4.3
|
||||
**Версия:** 2.5.0
|
||||
**Репозиторий:** `anten-ka/gotelegram_pro`
|
||||
**Активная ветка:** `alfa-test` (ветка `test` заморожена и содержит stable для конечных пользователей)
|
||||
**Активная ветка:** `alfa` (ветка `test` заморожена и содержит stable для конечных пользователей; `codex` — рабочая ветка разработки)
|
||||
**Целевая ОС:** Ubuntu 20.04+, Debian 11+
|
||||
|
||||
Этот документ описывает устройство проекта с максимальным количеством нюансов — все ловушки, на которые мы уже наступали, все причины «почему именно так», формат всех внешних интерфейсов, и checklist действий для типовых задач. Цель: чтобы новый агент мог продолжить работу без повторения ошибок и без регрессий.
|
||||
@@ -11,7 +11,7 @@
|
||||
|
||||
## 1. Общая картина
|
||||
|
||||
GoTelegram Pro — это менеджер MTProxy для Telegram, собранный вокруг Rust-ядра **telemt** (порт mtproto-proxy с поддержкой fake-TLS, v3.3.x). Проект даёт три вещи:
|
||||
goTelegram Pro — это менеджер MTProxy для Telegram, собранный вокруг Rust-ядра **telemt** (порт mtproto-proxy с поддержкой fake-TLS, v3.3.x). Проект даёт три вещи:
|
||||
|
||||
1. **CLI-меню на bash** (`install.sh` + `lib/*.sh`) — установка, настройка, обновление, бекап, перезапуск, смена режима, управление сайтом-маскировкой, выбор шаблона. Единая точка входа `gotelegram` (symlink → `/opt/gotelegram/install.sh`).
|
||||
2. **Сайт-маскировку** — поднимает рядом nginx с настоящим сайтом на настоящем домене (Let's Encrypt) из каталога 1801 HTML-шаблонов (html5up / startbootstrap / ThemeWagon / dawidolko). В stealth-режиме telemt слушает 443, маскировочный трафик проксирует на локальный nginx (127.0.0.1:8443) через `dns_overrides`.
|
||||
@@ -44,17 +44,18 @@ tls_domain = google.com (или любой из QUICK_DOMAINS)
|
||||
```
|
||||
anten-ka/gotelegram_pro
|
||||
├── test ← frozen stable, пользователи ставятся отсюда через bootstrap.sh
|
||||
└── alfa-test ← активная разработка, пуши туда
|
||||
├── alfa ← актуальная alfa/testing версия для пользователей
|
||||
└── codex ← рабочая ветка разработки, пуши туда и в alfa после проверки
|
||||
```
|
||||
|
||||
**Правило коммитов (из auto-memory):** все новые изменения идут ТОЛЬКО в `alfa-test`. `test` не трогаем без явной команды пользователя. Когда пользователь в диалоге скажет «влей в stable» — тогда мёржим alfa-test → test.
|
||||
**Правило коммитов:** новые изменения сначала проверяются в `codex`, затем пушатся в `alfa`. `test` не трогаем без явной команды пользователя. Когда пользователь в диалоге скажет «влей в stable» — тогда мёржим `alfa` → `test`.
|
||||
|
||||
**Инструменты для коммитов (специфика окружения):**
|
||||
- `git push` через Windows Git CLI **не работает** — credential helper вешает процесс.
|
||||
- Linux sandbox **не имеет доступа к github.com** — прокси возвращает 403 на raw.githubusercontent.com и api.github.com.
|
||||
- **Единственный работающий путь:** Python-скрипт через GitHub REST API, запускаемый через Desktop Commander с `shell="cmd.exe"` (НЕ powershell, иначе не захватывается stdout).
|
||||
- PAT токен — см. CLAUDE.md.
|
||||
- Workflow: `POST git/blobs` для каждого файла → `POST git/trees` (с `base_tree` от текущего HEAD) → `POST git/commits` (parents=[текущий HEAD]) → `PATCH git/refs/heads/alfa-test` (sha=новый commit) → при необходимости `PATCH git/refs/tags/vX.Y.Z`.
|
||||
- Workflow: `POST git/blobs` для каждого файла → `POST git/trees` (с `base_tree` от текущего HEAD) → `POST git/commits` (parents=[текущий HEAD]) → `PATCH git/refs/heads/codex` и/или `PATCH git/refs/heads/alfa` (sha=новый commit) → при необходимости `PATCH git/refs/tags/vX.Y.Z`.
|
||||
- **Важно про `base_tree`:** при частичном обновлении (1-2 файла) ОБЯЗАТЕЛЬНО передавать `base_tree` — иначе дерево получится только из переданных файлов, и все остальные файлы пропадут из коммита. `base_tree` можно опускать только при коммите с полным набором файлов.
|
||||
|
||||
---
|
||||
@@ -99,7 +100,7 @@ gotelegram-bot/locales/*.json 99 ключей i18n для бота с per-user
|
||||
| --- | --- |
|
||||
| Репо-скрипты | `/opt/gotelegram/` |
|
||||
| Symlink запуска | `/usr/local/bin/gotelegram` → `/opt/gotelegram/install.sh` |
|
||||
| Конфиг GoTelegram (JSON) | `/opt/gotelegram/config.json` |
|
||||
| Конфиг goTelegram Pro (JSON) | `/opt/gotelegram/config.json` |
|
||||
| Конфиг telemt | `/etc/telemt/config.toml` |
|
||||
| Бинарник telemt | `/usr/local/bin/telemt` |
|
||||
| Systemd юнит telemt | `/etc/systemd/system/telemt.service` |
|
||||
@@ -109,7 +110,7 @@ gotelegram-bot/locales/*.json 99 ключей i18n для бота с per-user
|
||||
| nginx конфиг | `/etc/nginx/sites-available/gotelegram` |
|
||||
| nginx enabled | `/etc/nginx/sites-enabled/gotelegram` |
|
||||
| Бекапы | `/opt/gotelegram/backups/` |
|
||||
| Лог GoTelegram | `/var/log/gotelegram.log` |
|
||||
| Лог goTelegram Pro | `/var/log/gotelegram.log` |
|
||||
| Логи telemt | `journalctl -u telemt` |
|
||||
| Логи бота | `journalctl -u gotelegram-bot` |
|
||||
|
||||
@@ -172,6 +173,7 @@ dns_overrides = ["anten-ka.com:8443:127.0.0.1"]
|
||||
- `[censorship.tls_fetch]` — параметры получения реального cert от tls_domain (используется только если `tls_emulation=false` И нет dns_overrides).
|
||||
- `[access]` — rate limits, доступ.
|
||||
- `[access.users]` — таблица `name = "secret"` (секрет — 32 hex).
|
||||
- `[access.user_max_unique_ips]` — опциональная таблица `name = число`, ограничивает одновременные уникальные IP на ключ; в goTelegram Pro значение `0` в UI удаляет строку и означает безлимит.
|
||||
|
||||
### Почему `unknown_sni_action = Drop` нас кусал
|
||||
|
||||
@@ -196,7 +198,7 @@ dns_overrides = ["anten-ka.com:8443:127.0.0.1"]
|
||||
Генерируется функцией `install_telemt_service()` в `lib/telemt.sh`:
|
||||
```ini
|
||||
[Unit]
|
||||
Description=GoTelegram MTProxy (telemt engine)
|
||||
Description=goTelegram Pro MTProxy (telemt engine)
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
@@ -324,7 +326,7 @@ Fallback: если по известным правилам index.html не на
|
||||
|
||||
## 10. История багов (не наступать на те же грабли)
|
||||
|
||||
Все решены в текущем HEAD (`alfa-test`). Перечисление для того, чтобы новый агент не «чинил» то, что уже починено, и понимал контекст.
|
||||
Все решены в текущем HEAD (`codex`/`alfa`). Перечисление для того, чтобы новый агент не «чинил» то, что уже починено, и понимал контекст.
|
||||
|
||||
1. `telemt.sh grep` — URL формат `telemt-x86_64-linux-gnu.tar.gz`, arch перед `linux`. Нужен `grep -iE "$arch_pattern"` + цепочка фильтров (`grep -i linux`, `grep -v sha256`, `grep gnu`, `head -1`).
|
||||
2. `bot.py safe_edit_message` — обёртка для `query.edit_message_text`, ловит `BadRequest: message is not modified`.
|
||||
@@ -370,6 +372,7 @@ Fallback: если по известным правилам index.html не на
|
||||
- **Системные действия (v2.4.2+)** — бот реально умеет менять шаблон и домен маскировки. Хелпер `run_bot_action(action, **kwargs)` вызывает `subprocess.run(["/opt/gotelegram/install.sh", "--action=X", "--json", ...])` и парсит JSON. Два коллбэка: `cb_pro_confirm` (change-template) и `cb_lite_domain` (change-lite-domain). Оба обёрнуты в `async with _BOT_ACTION_LOCK` (глобальный `asyncio.Lock()`) — сериализуют параллельные callback'и внутри процесса бота. Входы валидируются ДО subprocess: `_TPL_ID_RE = r"^[A-Za-z0-9_-]{1,64}$"`, `_DOMAIN_RE` (RFC-like). Малформный ввод отвергается с понятным сообщением, не доходя до shell.
|
||||
- **Поле `template_id` в config.json** — канонический ключ для текущего шаблона. Раньше (до v2.4.2) бот читал `config['template']` и писал в него же в `handle_text_message`, но `save_gotelegram_config` всегда писал `template_id` → поле статуса никогда не отображалось. Используй только `template_id`.
|
||||
- **Устанавливается** из меню `install.sh → 12) Telegram-бот → Установить`. Пользователь вводит BotFather token + свой Telegram ID, `.env` пишется в `/opt/gotelegram-bot/.env`.
|
||||
- **Обновляется автоматически (v2.5.0 alfa+)** при повторном bootstrap/update: `auto_update_bot_if_possible` сравнивает новые файлы в `$SCRIPT_DIR/gotelegram-bot/` с установленными `/opt/gotelegram-bot/{bot.py,i18n.py,requirements.txt,lang/*.json}`. Если есть отличия и сервис уже установлен, вызывается `bot_install >/dev/null`, `.env` сохраняется, зависимости обновляются, systemd unit переписывается и `gotelegram-bot` перезапускается. Это закрывает сценарий 2.4.x → 2.5.0, где bootstrap обновлял `/opt/gotelegram/gotelegram-bot/`, но рабочий сервис продолжал запускать старый `/opt/gotelegram-bot/bot.py`.
|
||||
|
||||
### 11.1 Non-interactive action bridge (install.sh ↔ bot)
|
||||
|
||||
@@ -391,7 +394,7 @@ Fallback: если по известным правилам index.html не на
|
||||
|
||||
Коды ошибок: `missing_arg`, `invalid_domain`, `wrong_mode`, `unknown_template`, `download_failed`, `deploy_failed`, `no_secret`, `gen_failed`, `validate_failed`, `restart_failed`, `unknown_action`, `lock_timeout`.
|
||||
|
||||
**Сериализация (v2.4.3):** `bot_action_dispatch` оборачивает вызов в `flock -w 30 9 < /var/lock/gotelegram-bot-action.lock`. Это защищает от гонок при:
|
||||
**Сериализация (v2.4.3+):** `bot_action_dispatch` оборачивает вызов в `flock -w 30 9 < /var/lock/gotelegram-bot-action.lock`. Это защищает от гонок при:
|
||||
1. Одновременных callback'ах внутри бота (asyncio.Lock уже ловит это, но flock — defense-in-depth).
|
||||
2. Параллельных CLI-вызовах (бот + ручной SSH, или два бот-процесса — теоретически).
|
||||
|
||||
@@ -429,13 +432,68 @@ switch_language ru|en
|
||||
`lib/backup.sh`. Собирает в `.tar.gz`:
|
||||
- `/etc/telemt/config.toml`
|
||||
- `/opt/gotelegram/config.json`
|
||||
- `/opt/gotelegram/disabled_users.json`
|
||||
- `/var/www/gotelegram-site/` (если есть)
|
||||
- `/etc/letsencrypt/live/<domain>/` + `/etc/letsencrypt/archive/<domain>/` (если Pro)
|
||||
- `/opt/gotelegram/custom_templates/`, `/opt/gotelegram/templates_catalog.json`
|
||||
- `/opt/gotelegram/stats_history.csv`, `/opt/gotelegram/user_stats_history.csv`, `/opt/gotelegram/shared-443.json`
|
||||
- `/opt/gotelegram-bot/.env`, bot i18n/lang
|
||||
- `/opt/gotelegram-admin/server.py`, `/opt/gotelegram-admin/static/`
|
||||
- `/etc/letsencrypt/live/<domain>/` + `/etc/letsencrypt/archive/<domain>/` + `/etc/letsencrypt/renewal/<domain>.conf` (если Pro)
|
||||
- `/etc/nginx/sites-available/gotelegram` (если есть)
|
||||
|
||||
Складывает в `/opt/gotelegram/backups/backup_YYYY-MM-DD_HH-MM-SS.tar.gz`.
|
||||
Складывает в `/opt/gotelegram/backups/gotelegram_backup_YYYYMMDD_HHMMSS.tar.gz`.
|
||||
|
||||
`restore_backup` разворачивает архив обратно, перезапускает telemt и nginx.
|
||||
`restore_backup <file> [password] [yes]` разворачивает архив обратно, перезапускает telemt, nginx, bot и admin. Третий аргумент `yes` нужен для неинтерактивного restore из web-admin/бота; перед ним они создают свежий safety backup. Restore понимает legacy-архивы раннего бота `backup_*.tar.gz`, где внутри лежали только `opt/gotelegram/config.json` и `etc/telemt/config.toml`.
|
||||
|
||||
Расписания бекапов: `set_backup_schedule off|daily|weekly|monthly` пишет `/etc/systemd/system/gotelegram-backup.{service,timer}` и `/opt/gotelegram/backup_schedule.json`. OnCalendar: daily `*-*-* 03:20:00`, weekly `Sun 03:20:00`, monthly `*-*-01 03:20:00`. Автоматическая чистка держит 30 последних `.tar.gz*` архивов.
|
||||
|
||||
### 13.1 Local Web Admin (v2.5.0)
|
||||
|
||||
Локальная web-админка находится в `admin-web/` и устанавливается в `/opt/gotelegram-admin`:
|
||||
|
||||
- backend: `admin-web/server.py`, Python stdlib only, без pip/npm dependencies;
|
||||
- frontend: `admin-web/static/`, vanilla JS/CSS, SVG-график без CDN;
|
||||
- systemd service: `gotelegram-admin`;
|
||||
- bind: `127.0.0.1:1984`, доступ только через SSH tunnel;
|
||||
- токена нет: после SSH tunnel открывается `http://127.0.0.1:1984/`;
|
||||
- write-запросы дополнительно требуют `X-GoTelegram-Admin: 1`, фронтенд добавляет его автоматически.
|
||||
- язык панели читается из `config.json.language`, затем из `/opt/gotelegram/.language`, fallback `en`; `POST /api/settings/language` сохраняет RU/EN в общий конфиг, marker file и bot `.env`; `gotelegram-bot/i18n.py` использует тот же источник как default до per-user override;
|
||||
- UI построен вкладками (`dashboard`, `traffic`, `keys`, `backups`, `logs`, `settings`), есть иконки меню, полезный overview-блок по реальным TCP/UDP-слушателям 443, light/dark theme в `localStorage` и promo-modal раз в 24 часа через `localStorage`;
|
||||
- `/api/overview` отдаёт `stats_status`, `admin_bind`, `site_status`, `port_443` и `backup_schedule`; `/api/site/check` проверяет `https://config.domain/` и считает OK только HTTP 200; `/api/stats?range=15m|1h|24h|month` отдаёт выбранное окно и `summary_rows`; `/api/users/<name>/traffic?range=15m|1h|24h|month` отдаёт per-user историю по `telemt total_octets`; `/api/users/<name>/qr` отдаёт PNG QR для Telegram proxy link через `qrencode`; `POST /api/users/<name>/max-ips` пишет `[access.user_max_unique_ips]` (`0` удаляет строку и означает безлимит); `/api/stats/collect` делает разовый сбор, `/api/stats/repair` устанавливает/перезапускает `gotelegram-stats`.
|
||||
|
||||
Функции: overview, проверка сайта на HTTP 200, service status/restart, чтение/запись `[access.users]`, enable/disable ключей через `/api/users/<name>/enabled`, per-user лимит одновременных уникальных IP через `[access.user_max_unique_ips]`, генерация proxy links и QR, общий traffic history из `/opt/gotelegram/stats_history.csv`, per-user traffic history из `/opt/gotelegram/user_stats_history.csv` с периодами 15m/1h/24h/month, current stats из `/run/gotelegram/stats_current.json`, список/создание backup, `POST /api/backups/schedule`, `POST /api/backups/restore` (background restore job, только basename из backup dir), структурированные journal logs (`service`, `ok`, `exit_code`, `line_count`, `text`).
|
||||
|
||||
Traffic retention: обе CSV-истории хранятся максимум 365 дней. Последние 31 день остаются с поминутным разрешением, более старые точки автоматически уплотняются до одной последней cumulative-точки в час. Чистка/уплотнение запускается не чаще одного раза в час, а per-user сбор пишет данные не чаще одного раза в минуту, даже если systemd-loop вызывает `stats_collect` каждую секунду. Параметры можно переопределить переменными `STATS_RETENTION_DAYS`, `STATS_MINUTE_RETENTION_DAYS`, `STATS_CLEANUP_INTERVAL`.
|
||||
|
||||
### 13.1.1 Shared TCP/443 with 3x-ui/Xray
|
||||
|
||||
`lib/shared443.sh` добавляет управляемую схему shared-443 через nginx stream `ssl_preread`:
|
||||
|
||||
- публичный вход: `0.0.0.0:443` принадлежит nginx stream dispatcher;
|
||||
- default backend: `127.0.0.1:7443` (`telemt`), а `general.links.public_port` остаётся `443`;
|
||||
- сайт остаётся за telemt через `dns_overrides` на `127.0.0.1:8443`;
|
||||
- Xray/3x-ui должен быть перенесён в панели на внутренний target, например `127.0.0.1:9443`, после чего `shared443_enable <domain> <xray-sni> 127.0.0.1:9443` пишет `/opt/gotelegram/shared-443.json` и `/etc/nginx/stream-conf.d/gotelegram-shared443.conf`.
|
||||
|
||||
Автоматически переписывать SQLite/JSON 3x-ui нельзя: панель может перегенерировать Xray config и потерять ручные правки. Поэтому goTelegram Pro показывает конфликт прямого bind на `443`, даёт маршрут и включает dispatcher только на уровне собственных конфигов/nginx.
|
||||
|
||||
Отключённые ключи хранятся в `/opt/gotelegram/disabled_users.json`: active keys остаются в `/etc/telemt/config.toml` под `[access.users]`, disabled keys удаляются из active block и могут быть возвращены обратно без потери secret. `main` защищён от удаления и отключения. Лимит уникальных IP сохраняется в `[access.user_max_unique_ips]` и не теряется при disable/enable; при удалении ключа строка лимита удаляется. Операции с ключами в web-admin и Telegram-боте берут общий file lock `/run/gotelegram/admin-users.lock`, TOML пишется через temp+replace и quoted keys (`"a.b"`), а telemt restart для add/delete/enable/disable/IP-limit ставится через `systemctl --no-block restart`, чтобы switch в UI не зависал на `wait_tcp_port`.
|
||||
|
||||
`install_admin_web` вызывается при установке Telegram-бота. `auto_install_admin_web_if_possible` подхватывает админку после bootstrap/update, если Python уже установлен и файлы отличаются. `auto_update_bot_if_possible` аналогично подхватывает уже установленный Telegram-бот и обновляет рабочий `/opt/gotelegram-bot` без запроса токена. При установке админки скрипт пытается установить/перезапустить `gotelegram-stats`; если это не удалось, оператор может нажать Restart collector в Traffic. Backup v1.6 сохраняет `admin_web/server.py`, `admin_web/static/`, `disabled_users.json`, `stats_history.csv`, `user_stats_history.csv`, `backup_schedule.json` и `shared-443.json`, restore возвращает их, удаляет legacy `admin_web/token` и пробует перезапустить `gotelegram-admin`.
|
||||
|
||||
### 13.2 Upgrade migration (v2.5.0)
|
||||
|
||||
`install.sh` вызывает `auto_migrate_legacy_state` перед интерактивным меню и перед non-interactive `--action=...` для бота. В интерактивном запуске после миграции также вызываются `auto_update_bot_if_possible` и `auto_install_admin_web_if_possible`, чтобы повторный bootstrap обновлял не только исходники в `/opt/gotelegram`, но и реально запущенные сервисы `/opt/gotelegram-bot` и `/opt/gotelegram/admin-web`. Цель — комфортно обновляться даже с кривой старой логики без переустановки прокси:
|
||||
|
||||
- перед изменениями создаётся `/opt/gotelegram/backups/preupgrade_2.5.0_*.tar.gz`;
|
||||
- из старого `/etc/telemt/config.toml` вытаскиваются все строки `[access.users]`, а не только `main`;
|
||||
- если `main` отсутствует, он добавляется с первым найденным секретом, чтобы старые ссылки не потерялись;
|
||||
- сохраняются порт, `tls_domain`, `mask_port`, режим Lite/Pro, домен, язык, `stats_enabled`;
|
||||
- старый telemt TOML нормализуется под v2.5.0 с `[server.api]` и metrics, затем в него возвращается полный users block;
|
||||
- если сайт уже развёрнут, но template id неизвестен или старый `config.json` врёт, в `/var/www/gotelegram-site/.gotelegram_template_id` пишется `deployed_site`;
|
||||
- `config.json` переписывается в актуальный формат, но с сохранением `installed_at` и пользовательских настроек;
|
||||
- если telemt был активен и TOML был изменён, сервис перезапускается один раз.
|
||||
|
||||
Инвариант: миграция должна быть идемпотентной. Маркер `/opt/gotelegram/.migrated_2.5.0` предотвращает повторную нормализацию без причины.
|
||||
|
||||
---
|
||||
|
||||
@@ -446,9 +504,9 @@ switch_language ru|en
|
||||
3. Напиши `C:\Temp\push_<описание>.py`:
|
||||
```python
|
||||
import os, base64, json, urllib.request, ssl
|
||||
TOKEN = "github_pat_..."
|
||||
TOKEN = os.environ["GOTELEGRAM_PAT"]
|
||||
REPO = "anten-ka/gotelegram_pro"
|
||||
BRANCH = "alfa-test"
|
||||
BRANCH = "alfa"
|
||||
API = f"https://api.github.com/repos/{REPO}"
|
||||
headers = {
|
||||
"Authorization": f"Bearer {TOKEN}",
|
||||
@@ -596,6 +654,8 @@ with socket.create_connection(("95.163.176.222", 443), timeout=5) as s:
|
||||
|
||||
## 17. Changelog
|
||||
|
||||
- **2.5.0 (2026-04-24)** — крупный maintenance pass в ветке `codex`/`alfa`: единая версия `2.5.0` в runtime и документации; удалён дефолтный PAT из `bootstrap.sh` (токен теперь только через `GOTELEGRAM_PAT`); повторный bootstrap/update автоматически обновляет уже установленный `/opt/gotelegram-bot` и перезапускает `gotelegram-bot`, сохраняя `.env`; `generate_telemt_toml` добавляет `[server.api]` на `127.0.0.1:9091` и metrics на `127.0.0.1:9090`, что нужно для управления пользователями и статистики; Telegram-бот получил меню `🔑 Keys` для `[access.users]` (добавить/отключить/включить/удалить/показать ссылку/QR/runtime info/IP-limit); добавлена локальная web-админка goTelegram Pro `gotelegram-admin` на `127.0.0.1:1984` с SSH-tunnel инструкцией в боте без отдельного web-admin токена, вкладочной UI-навигацией, иконками, блоком реальных TCP/UDP-слушателей 443, promo-modal раз в 24 часа, i18n от языка установки, ручным переключателем RU/EN, site check на HTTP 200, structured journal logs, light/dark theme, адаптивом, быстрыми switch-переключателями ключей, настройкой `[access.user_max_unique_ips]`, QR-кодами, traffic history 15m/1h/24h/month с переключением график/строки, per-user traffic history из `telemt total_octets` и stats collector restart endpoint; исправлено чтение traffic CSV в боте (header больше не ломает parsing); бот сам делает `stats_collect` перед показом статистики; `iptables` добавлен в optional deps и stats collector пытается установить его; CLI-смена шаблона теперь обновляет `config.json.template_id`, чтобы бот не показывал первый установленный шаблон; backup/restore версии `1.6` сохраняет bot `.env`, bot lang files, disabled user keys, backup schedule, web-admin server/static, custom templates, templates catalog, stats history, user stats history, shared-443 config и полноценную структуру Let's Encrypt (`live/archive/renewal`) для переезда на новый сервер; добавлен безопасный детект 3x-ui/Xray на 443 и управляемый nginx stream shared-443 dispatcher.
|
||||
- **2.4.6 (2026-04-10)** — universal `apt_lock_wait` helper: ожидание dpkg/apt lock при unattended-upgrades, исправляет установку nginx/certbot/python на свежих VPS.
|
||||
- **2.4.3 (2026-04-10)** — iter3-фикс: `bot_action_dispatch` оборачивается во `flock -w 30` на `/var/lock/gotelegram-bot-action.lock`. Обнаружена гонка: параллельные `change-lite-domain` получали `"no secret in config"`, потому что один процесс читал `config.json`, пока другой делал `jq ... > tmp && mv`. `util-linux` (содержит `flock`) добавлен в `critical` deps, `check_deps_present` и маппинги `apt_pkg_for_cmd`/`dnf_pkg_for_cmd`.
|
||||
- **2.4.2 (2026-04-10)** — реализация non-interactive `bot_action_*` в install.sh (change-template + change-lite-domain с JSON-ответом). bot.py подключает `run_bot_action()` и делает реальную работу вместо stub'ов. Критфиксы: (a) `safe_edit_message` принимает `disable_web_page_preview` (иначе TypeError в success-пути cb_pro_confirm); (b) чтение/запись `config['template_id']` вместо `config['template']` (save_gotelegram_config всегда писал `template_id`, бот смотрел не туда); (c) `bot_update_config_field` использует shell `date -Iseconds` вместо `jq now|todate` (jq 1.5 совместимость для Debian 10); (d) `asyncio.Lock _BOT_ACTION_LOCK` сериализует callback'и в процессе бота; (e) валидация `_TPL_ID_RE`/`_DOMAIN_RE` до subprocess. Полный аудит и автоустановка зависимостей: `ensure_deps` разделяет critical/optional, дедуплицирует пакеты, re-verify после install; `apt_pkg_for_cmd` и `dnf_pkg_for_cmd` мапят команды на пакеты (dig→dnsutils/bind-utils, xxd→xxd/vim-common, flock→util-linux). `check_deps_present` — быстрый чек без `apt-get update`.
|
||||
- **2.4.1 (2026-04-10)** — баг #23: `start_telemt` делает `restart` если сервис активен (иначе stale in-memory config после переустановки Lite поверх Pro). Полная документация проекта — `DOCS_HUMAN.md` и `DOCS_AI.md` (этот файл).
|
||||
@@ -649,7 +709,7 @@ with socket.create_connection(("95.163.176.222", 443), timeout=5) as s:
|
||||
|
||||
## 20. Контрольные точки и инварианты
|
||||
|
||||
Перед любым пушем в `alfa-test`:
|
||||
Перед любым пушем в `codex`/`alfa`:
|
||||
1. `bash -n install.sh lib/*.sh` — синтаксис bash ОК.
|
||||
2. Все новые `$()`-вызываемые функции пишут UI через `>&2`.
|
||||
3. Все пути к lib/ идут через `$SCRIPT_DIR/lib/...`, а `SCRIPT_DIR` — через `readlink -f`.
|
||||
@@ -659,8 +719,8 @@ with socket.create_connection(("95.163.176.222", 443), timeout=5) as s:
|
||||
7. `generate_telemt_toml` не роняет telemt v3 (проверить `telemt run --check config.toml`).
|
||||
|
||||
После пуша:
|
||||
1. Подождать пока `alfa-test` обновится (GitHub API мгновенно, raw кеш ~30 сек).
|
||||
2. На VPS: `bash bootstrap.sh?token=...&ref=alfa-test` (или ручной `git pull` в клоне) + `sed -i 's/\r$//'` + `chmod +x` + `systemctl restart telemt`.
|
||||
1. Подождать пока `alfa` обновится (GitHub API мгновенно, raw кеш ~30 сек).
|
||||
2. На VPS: повторить bootstrap из ветки `alfa` с `GOTELEGRAM_PAT`; скрипт сам скачает файлы, обновит установленный бот/админку и покажет меню.
|
||||
3. `telemt_status` → running. `journalctl -u telemt` → нет ошибок. Ссылка открывается в Telegram-клиенте.
|
||||
|
||||
---
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# GoTelegram Pro — руководство пользователя
|
||||
# goTelegram Pro — руководство пользователя
|
||||
|
||||
**Версия:** 2.4.3
|
||||
**Версия:** 2.5.0
|
||||
**Репозиторий:** `anten-ka/gotelegram_pro`
|
||||
**Для кого:** владельцы VPS, которым нужен надёжный MTProxy для Telegram с маскировкой под обычный HTTPS-сайт.
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
## 1. Что это такое
|
||||
|
||||
GoTelegram Pro — это готовый менеджер прокси-сервера MTProxy для Telegram. Он делает три вещи, которые иначе пришлось бы собирать вручную:
|
||||
goTelegram Pro — это готовый менеджер прокси-сервера MTProxy для Telegram. Он делает три вещи, которые иначе пришлось бы собирать вручную:
|
||||
|
||||
1. Ставит и настраивает ядро **telemt** (это современный Rust-порт mtproto-proxy с fake-TLS маскировкой).
|
||||
2. Запускает рядом обычный HTTPS-сайт на настоящем домене, так что провайдеру со стороны всё выглядит как посещение безобидного лендинга — а на самом деле в том же соединении ходит Telegram-трафик. Это называется «stealth» или «Pro-режим».
|
||||
@@ -23,7 +23,13 @@ GoTelegram Pro — это готовый менеджер прокси-серв
|
||||
На чистом Ubuntu/Debian VPS под root:
|
||||
|
||||
```bash
|
||||
bash <(curl -sL "https://raw.githubusercontent.com/anten-ka/gotelegram_pro/test/bootstrap.sh?token=YOUR_PAT")
|
||||
export GOTELEGRAM_PAT="YOUR_PAT"; bash <(curl -sL -H "Authorization: token $GOTELEGRAM_PAT" "https://raw.githubusercontent.com/anten-ka/gotelegram_pro/alfa/bootstrap.sh")
|
||||
```
|
||||
|
||||
Если нужно поставить строго зафиксированный prerelease, укажи ветку/тег явно:
|
||||
|
||||
```bash
|
||||
export GOTELEGRAM_PAT="YOUR_PAT" GOTELEGRAM_BRANCH="v2.5.0-alfa"; bash <(curl -sL -H "Authorization: token $GOTELEGRAM_PAT" "https://raw.githubusercontent.com/anten-ka/gotelegram_pro/v2.5.0-alfa/bootstrap.sh")
|
||||
```
|
||||
|
||||
`bootstrap.sh` скачает все файлы из приватного репозитория, создаст симлинк `/usr/local/bin/gotelegram` и запустит главное меню. Через минуту команда `gotelegram` уже будет работать откуда угодно.
|
||||
@@ -104,6 +110,8 @@ bash <(curl -sL "https://raw.githubusercontent.com/anten-ka/gotelegram_pro/test/
|
||||
|
||||
Чтобы запустить бота, нужны только два параметра — токен от `@BotFather` и твой Telegram ID (чтобы никто кроме тебя бота не использовал). Меню подсказывает где что ввести.
|
||||
|
||||
При обновлении goTelegram Pro установленный бот обновляется автоматически: новые `bot.py`, `i18n.py`, языковые файлы и `requirements.txt` копируются в `/opt/gotelegram-bot/`, зависимости проверяются, сервис `gotelegram-bot` перезапускается. Файл `/opt/gotelegram-bot/.env` с токеном и администраторами сохраняется.
|
||||
|
||||
---
|
||||
|
||||
## 6. Язык интерфейса
|
||||
@@ -119,26 +127,63 @@ CLI и бот переведены на русский и английский.
|
||||
|
||||
## 7. Бекап и восстановление
|
||||
|
||||
Пункт 8 делает один файл `.tar.gz` со всем, что нужно: `/etc/telemt/config.toml`, `/opt/gotelegram/config.json`, данные nginx-сайта и сертификаты. По умолчанию в `/opt/gotelegram/backups/`.
|
||||
Пункт 8 делает один файл `.tar.gz` со всем, что нужно: `/etc/telemt/config.toml`, `/opt/gotelegram/config.json`, `/opt/gotelegram/disabled_users.json`, данные nginx-сайта, сертификаты Let's Encrypt (`live/archive/renewal`), пользовательские шаблоны, каталог шаблонов, историю трафика, состояние Telegram-бота и локальной web-админки. По умолчанию архивы лежат в `/opt/gotelegram/backups/`.
|
||||
|
||||
Пункт 9 принимает такой архив и восстанавливает всё обратно (конфиг, сервис, ссылка — всё то же, что было).
|
||||
Пункт 9 принимает такой архив и восстанавливает всё обратно (конфиг, сервис, ссылка — всё то же, что было). Перед восстановлением из админки или Telegram-бота автоматически создаётся свежий safety-бекап текущего состояния.
|
||||
|
||||
Хорошая практика: делать бекап каждый раз перед пунктами 1 (переустановка) и 10 (обновление telemt).
|
||||
В web-админке и Telegram-боте есть сценарии автобэкапов: выключено, каждый день, каждую неделю или каждый месяц. Это systemd timer `gotelegram-backup.timer`; автоматическая чистка оставляет последние 30 архивов, чтобы каталог не рос бесконечно. Старые простые архивы `backup_*.tar.gz`, которые создавал ранний Telegram-бот только из двух конфигов, тоже распознаются при восстановлении как legacy-формат.
|
||||
|
||||
---
|
||||
|
||||
## 8. Обновление
|
||||
## 8. Локальная web-админка
|
||||
|
||||
Начиная с **2.5.0** вместе с ботом ставится локальная web-админка:
|
||||
|
||||
- systemd service: `gotelegram-admin`;
|
||||
- слушает только `127.0.0.1:1984`;
|
||||
- наружу не публикуется и рассчитана на доступ через SSH tunnel;
|
||||
- после туннеля открывается обычным URL `http://127.0.0.1:1984/`;
|
||||
- язык берётся из `config.json.language` / `/opt/gotelegram/.language`, как в CLI и Telegram-боте; в верхней панели можно переключить RU/EN, выбор сохраняется в общий конфиг;
|
||||
- есть светлая/тёмная тема, вкладки и адаптивная вёрстка под desktop/mobile;
|
||||
- Telegram-бот показывает инструкцию для Termius и обычную команду `ssh -L 1984:127.0.0.1:1984 root@SERVER`.
|
||||
|
||||
В админке есть dashboard, проверка сайта `https://домен/` на HTTP 200, статус сервисов, полезный блок «кто слушает порт 443» по данным `ss`, управление ключами `[access.users]` с добавлением, удалением и быстрым отключением через switch, лимит одновременных уникальных IP на ключ через `[access.user_max_unique_ips]` (`0` — безлимит), генерация ссылок и QR-кодов для импорта в Telegram, traffic history по периодам 15 минут / 1 час / 24 часа / месяц с переключателем график/строки, такая же статистика по каждому ключу, кнопка разового обновления статистики, кнопка перезапуска сборщика, список/создание/восстановление бекапов, расписание автобэкапов и просмотр логов с количеством строк и статусом `journalctl`.
|
||||
|
||||
История трафика хранится максимум 1 год. Чтобы файлы не разрастались, последние 31 день пишутся поминутно, а более старая история автоматически уплотняется до одной точки в час. Для обычного просмотра 15 минут / 1 час / 24 часа / месяц детализация остаётся полной.
|
||||
|
||||
## 8.1 3x-ui / VLESS на том же 443
|
||||
|
||||
Один порт `443` не могут одновременно слушать `telemt` и Xray напрямую. Для совместной работы используется схема shared-443: публичный `443` занимает nginx stream-диспетчер, goTelegram `telemt` переносится на `127.0.0.1:7443`, сайт остаётся на `127.0.0.1:8443`, а inbound 3x-ui/Xray нужно в панели перенести на внутренний адрес, например `127.0.0.1:9443`.
|
||||
|
||||
После переноса Xray-входа можно включить маршрут:
|
||||
|
||||
```bash
|
||||
source /opt/gotelegram/lib/shared443.sh
|
||||
shared443_enable my-domain.com xray-domain.com 127.0.0.1:9443
|
||||
```
|
||||
|
||||
goTelegram Pro не переписывает базу 3x-ui автоматически, потому что панель может перегенерировать Xray-конфиг. Админка показывает карту `443`: публичный edge, telemt, сайт и Xray-маршруты.
|
||||
|
||||
Отключённые ключи убираются из активного telemt-конфига и сохраняются в `/opt/gotelegram/disabled_users.json`, поэтому их можно включить обратно без потери secret. Основной ключ `main` защищён от удаления и отключения.
|
||||
|
||||
Лимит IP хранится в `/etc/telemt/config.toml` в секции `[access.user_max_unique_ips]`. Значение `0` в UI означает отсутствие строки в этой секции и безлимит. Значение `1`, `2` и выше ограничивает количество одновременно активных уникальных IP для выбранного ключа; если лимит уже занят, новый IP не проходит, пока один из старых не отключится.
|
||||
|
||||
---
|
||||
|
||||
## 9. Обновление
|
||||
|
||||
Два типа обновлений:
|
||||
|
||||
- **Обновление ядра telemt** (пункт 10) — тянет свежий бинарник с GitHub Releases `telemt/telemt`, сохраняет старый в `.bak`, перезапускает сервис. Конфиг остаётся как был.
|
||||
- **Обновление самого GoTelegram** (пункт 1) — переустанавливает скрипты и lib/. Файл `config.json` не трогается, ключ и домен не меняются.
|
||||
- **Обновление самого goTelegram Pro** (повторный запуск bootstrap или пункт 1 после скачивания новых файлов) — переустанавливает скрипты и `lib/`, обновляет локальную web-админку и уже установленный Telegram-бот. `config.json`, ключи, домен и `/opt/gotelegram-bot/.env` не трогаются.
|
||||
|
||||
Bootstrap.sh умеет сам обновлять всё, если запустить его повторно.
|
||||
|
||||
Начиная с **2.5.0** первый запуск новой версии делает автоматическую миграцию старого состояния: создаёт pre-upgrade архив в `/opt/gotelegram/backups/`, вытаскивает все ключи из `[access.users]`, сохраняет домен, порт, режим, язык, историю статистики и фактически развёрнутый сайт. Если старый `template_id` был неправильным или отсутствовал, сайт помечается как `deployed_site`, чтобы Telegram-бот не показывал первый установленный шаблон вместо текущего.
|
||||
|
||||
---
|
||||
|
||||
## 9. Удаление
|
||||
## 10. Удаление
|
||||
|
||||
Пункт **13) Удалить всё** даёт выбор:
|
||||
|
||||
@@ -150,18 +195,18 @@ Bootstrap.sh умеет сам обновлять всё, если запуст
|
||||
|
||||
---
|
||||
|
||||
## 10. Требования к VPS
|
||||
## 11. Требования к VPS
|
||||
|
||||
- **ОС:** Ubuntu 20.04+ или Debian 11+ (протестировано на Ubuntu 22.04).
|
||||
- **RAM:** 512 МБ минимум, 1 ГБ комфортно (telemt сам по себе ест мало, но рядом nginx + бот).
|
||||
- **Диск:** 2 ГБ (в основном под каталог шаблонов и бекапы).
|
||||
- **Права:** root или sudo.
|
||||
- **Порты:** 443 должен быть свободен (ни apache, ни nginx, ни ничего другого не должно на нём висеть). Если занят — скрипт предупредит.
|
||||
- **Порты:** в обычной схеме 443 должен быть свободен. Для совместной работы с 3x-ui/Xray используйте shared-443 и переносите Xray inbound на внутренний порт, например `127.0.0.1:9443`.
|
||||
- **Для Pro-режима:** домен с настроенным A-record на IP VPS. DNS должен отвечать ДО установки, иначе Let's Encrypt не выдаст сертификат.
|
||||
|
||||
---
|
||||
|
||||
## 11. Частые вопросы
|
||||
## 12. Частые вопросы
|
||||
|
||||
**Q: Ключ перестал работать, telemt живой.**
|
||||
A: 95% случаев — это когда после переустановки telemt не перечитал свежий конфиг (было исправлено в 2.4.1, см. changelog). Перезапусти вручную: `systemctl restart telemt`. Если не помогло — смотри логи (пункт 6 меню) и проверь `/etc/telemt/config.toml` на предмет правильного `tls_domain`.
|
||||
@@ -176,7 +221,7 @@ A: Пункт 7 → сменить режим/шаблон. Можно такж
|
||||
A: Посмотри логи бота в пункте 12 → «Логи бота». Чаще всего — неверный токен или неверный admin ID в `.env`.
|
||||
|
||||
**Q: Могу ли я поставить несколько прокси на одном VPS?**
|
||||
A: На одном IP на порту 443 — нет, telemt один. На разных портах — можно, но скрипт этого не поддерживает из коробки, нужно руками.
|
||||
A: Да, через shared-443: nginx stream слушает публичный `443`, goTelegram `telemt` работает на `127.0.0.1:7443`, а Xray/3x-ui — на внутреннем порту вроде `127.0.0.1:9443`. Напрямую два процесса на `0.0.0.0:443` работать не будут.
|
||||
|
||||
**Q: Это легально?**
|
||||
A: Сам MTProxy — да, это публичная технология из исходников Telegram. Запуск прокси, чтобы твои друзья могли пользоваться Telegram там, где он заблокирован — в большинстве юрисдикций легально. Проверь локальные законы.
|
||||
@@ -190,7 +235,7 @@ A: Сам MTProxy — да, это публичная технология из
|
||||
| Скрипты (`install.sh`, `lib/`) | `/opt/gotelegram/` |
|
||||
| Симлинк запуска | `/usr/local/bin/gotelegram` |
|
||||
| Конфиг telemt | `/etc/telemt/config.toml` |
|
||||
| Конфиг GoTelegram (JSON) | `/opt/gotelegram/config.json` |
|
||||
| Конфиг goTelegram Pro (JSON) | `/opt/gotelegram/config.json` |
|
||||
| Бинарник telemt | `/usr/local/bin/telemt` |
|
||||
| Systemd юнит | `/etc/systemd/system/telemt.service` |
|
||||
| Бот | `/opt/gotelegram-bot/` |
|
||||
@@ -198,7 +243,7 @@ A: Сам MTProxy — да, это публичная технология из
|
||||
| Сайт (Pro-режим) | `/var/www/gotelegram-site/` |
|
||||
| nginx site | `/etc/nginx/sites-available/gotelegram` |
|
||||
| Бекапы | `/opt/gotelegram/backups/` |
|
||||
| Лог GoTelegram | `/var/log/gotelegram.log` |
|
||||
| Лог goTelegram Pro | `/var/log/gotelegram.log` |
|
||||
| Логи telemt | `journalctl -u telemt` |
|
||||
| Логи бота | `journalctl -u gotelegram-bot` |
|
||||
|
||||
@@ -208,12 +253,14 @@ A: Сам MTProxy — да, это публичная технология из
|
||||
|
||||
- Баги и пожелания — issues в репозитории `anten-ka/gotelegram_pro`.
|
||||
- Владелец: Vitalii (`anten-ka`).
|
||||
- Ветки: `test` (заморожена, stable для пользователей), `alfa-test` (активная разработка).
|
||||
- Ветки: `test` (заморожена, stable для пользователей), `alfa` (актуальная тестовая версия), `codex` (рабочая ветка разработки).
|
||||
|
||||
---
|
||||
|
||||
## Changelog (коротко)
|
||||
|
||||
- **2.5.0** — единая версия по коду и документации; удалён дефолтный PAT из `bootstrap.sh`; исправлена статистика в боте (CSV header больше не ломает чтение истории, бот сам обновляет snapshot); установленный Telegram-бот теперь автоматически обновляется при повторном bootstrap/update без потери `.env`; CLI-смена шаблона теперь обновляет `config.json.template_id`, поэтому бот показывает текущий шаблон; telemt TOML включает локальный API `127.0.0.1:9091` и metrics на `127.0.0.1:9090`; добавлено меню Telegram-бота для отдельных ключей пользователей (`[access.users]`): список, добавление, отключение/включение, удаление, ссылка, QR-код, текущий runtime, лимит уникальных IP через `[access.user_max_unique_ips]` и история трафика по ключу; добавлена локальная web-админка goTelegram Pro на `127.0.0.1:1984` под SSH tunnel без отдельного токена, с вкладками, иконками, promo-разделом раз в 24 часа, i18n от языка установки, ручным переключателем RU/EN, проверкой сайта на HTTP 200, тёмной темой, адаптивом, быстрыми switch-переключателями ключей, настройкой IP-лимита, QR-кодами, блоком реальных TCP/UDP-слушателей 443, подсказками к техническим терминам, traffic history по периодам 15 минут / 1 час / 24 часа / месяц и per-user traffic history; backup/restore сохраняет bot `.env`, языки бота, отключённые ключи, web-admin server/static, custom templates, stats history, user stats history, shared-443 config и структуру Let's Encrypt для переезда на новый VPS; добавлены ручные/ежедневные/еженедельные/ежемесячные бэкапы, восстановление из админки/бота с safety-бекапом и legacy-restore для старых `backup_*.tar.gz`; добавлен безопасный детект 3x-ui/Xray на 443 и управляемый nginx stream shared-443 dispatcher.
|
||||
- **2.4.6** — ожидание apt/dpkg lock на свежих Ubuntu/Debian, чтобы установка nginx/certbot/Python не падала во время unattended-upgrades.
|
||||
- **2.4.3** — фикс гонки в `bot_action_dispatch`: параллельные вызовы `change-lite-domain`/`change-template` (например, два пользователя бота одновременно) могли получить ошибку «no secret in config», если один процесс читал `config.json` в момент, когда другой его перезаписывал через `jq`. Теперь диспетчер оборачивается в `flock(1)` с таймаутом 30 с; `util-linux` (содержит `flock`) добавлен в критические зависимости.
|
||||
- **2.4.2** — смена шаблона и домена маскировки **прямо из Telegram-бота** без SSH. Раньше эти пункты меню показывали сообщение «сделай через CLI», теперь бот вызывает `install.sh --action=change-template --json` / `--action=change-lite-domain --json` и разбирает ответ. Плюс: безопасный `safe_edit_message` принимает `disable_web_page_preview`; поле статуса шаблона наконец-то отображается (раньше читалось не из того ключа JSON); полный аудит и автоустановка системных зависимостей при первом запуске (`curl jq openssl git xxd tar dig flock` + опциональные `qrencode bc`); `asyncio.Lock` в боте сериализует параллельные callback'и; валидация tpl\_id (`[A-Za-z0-9_-]{1,64}`) и домена до subprocess.
|
||||
- **2.4.1** — фикс: `start_telemt` теперь делает `restart` если сервис уже запущен. Раньше переустановка Lite поверх Pro оставляла в памяти старый конфиг, и клиенты получали «Unknown TLS SNI drop». Плюс полная документация проекта (этот файл и `DOCS_AI.md`).
|
||||
|
||||
1665
admin-web/server.py
Normal file
1665
admin-web/server.py
Normal file
File diff suppressed because it is too large
Load Diff
1755
admin-web/static/app.js
Normal file
1755
admin-web/static/app.js
Normal file
File diff suppressed because it is too large
Load Diff
398
admin-web/static/index.html
Normal file
398
admin-web/static/index.html
Normal file
@@ -0,0 +1,398 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>goTelegram Pro Admin</title>
|
||||
<script>
|
||||
(function () {
|
||||
var stored = localStorage.getItem("gotelegram-theme");
|
||||
var theme = stored || (matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light");
|
||||
document.documentElement.dataset.theme = theme;
|
||||
}());
|
||||
</script>
|
||||
<link rel="stylesheet" href="/styles.css?v=2.5.0-admin18">
|
||||
</head>
|
||||
<body>
|
||||
<div class="app-shell">
|
||||
<aside class="sidebar" id="sidebar">
|
||||
<div class="brand">
|
||||
<div class="brand-mark">GT</div>
|
||||
<div>
|
||||
<strong>goTelegram Pro</strong>
|
||||
<span data-i18n="brandSubtitle">Local Admin</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<nav class="nav-tabs" aria-label="Admin sections" data-i18n-aria-label="ariaAdminSections">
|
||||
<button type="button" class="nav-item active" data-nav="dashboard"><span class="nav-icon">⌁</span><span data-i18n="navDashboard">Dashboard</span></button>
|
||||
<button type="button" class="nav-item" data-nav="traffic"><span class="nav-icon">⇅</span><span data-i18n="navTraffic">Traffic</span></button>
|
||||
<button type="button" class="nav-item" data-nav="keys"><span class="nav-icon">⚿</span><span data-i18n="navKeys">Keys</span></button>
|
||||
<button type="button" class="nav-item" data-nav="backups"><span class="nav-icon">▣</span><span data-i18n="navBackups">Backups</span></button>
|
||||
<button type="button" class="nav-item" data-nav="logs"><span class="nav-icon">☰</span><span data-i18n="navLogs">Logs</span></button>
|
||||
<button type="button" class="nav-item" data-nav="settings"><span class="nav-icon">⚙</span><span data-i18n="navSettings">Settings</span></button>
|
||||
</nav>
|
||||
|
||||
<div class="sidebar-foot">
|
||||
<span id="sidebarVersion">v--</span>
|
||||
<span id="sidebarBind">127.0.0.1:1984</span>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<div class="workspace">
|
||||
<header class="topbar">
|
||||
<button id="menuBtn" class="icon-btn mobile-only" type="button" aria-label="Menu" data-i18n-aria-label="ariaMenu">☰</button>
|
||||
<div class="title-block">
|
||||
<p class="eyebrow" id="pageKicker">Local Admin</p>
|
||||
<h1 id="pageTitle">Dashboard</h1>
|
||||
<small id="lastRefresh">--</small>
|
||||
</div>
|
||||
<div class="top-actions">
|
||||
<select id="languageSelect" class="language-select" aria-label="Language" data-i18n-aria-label="ariaLanguage">
|
||||
<option value="en">EN</option>
|
||||
<option value="ru">RU</option>
|
||||
</select>
|
||||
<button id="themeToggle" class="ghost" type="button">Theme</button>
|
||||
<button id="refreshBtn" type="button" data-i18n="refresh">Refresh</button>
|
||||
<button id="autoRefreshToggle" class="auto-refresh-toggle" type="button" aria-pressed="true" data-i18n-title="autoRefresh" title="Auto refresh">
|
||||
<span class="auto-refresh-icon" aria-hidden="true">↻</span>
|
||||
<span class="auto-refresh-track" aria-hidden="true"><span></span></span>
|
||||
<span class="auto-refresh-state">5s</span>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="content">
|
||||
<section class="page-panel active" data-page="dashboard">
|
||||
<section class="visual-overview">
|
||||
<div>
|
||||
<p class="eyebrow">goTelegram Pro</p>
|
||||
<h2 id="visualTitle">Port 443</h2>
|
||||
<p id="visualText">Website, MTProxy and local admin status in one operational view.</p>
|
||||
</div>
|
||||
<div class="port-map" id="port443Map">
|
||||
<div class="port-map-head">
|
||||
<div class="port-badge">
|
||||
<span id="port443Number">443</span>
|
||||
<small id="port443Configured">public</small>
|
||||
</div>
|
||||
<strong id="port443Summary">--</strong>
|
||||
</div>
|
||||
<div id="port443List" class="port-list"></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="metric-grid">
|
||||
<article class="metric-card accent-blue">
|
||||
<span data-i18n="metricMode">Mode</span>
|
||||
<strong id="metricMode">--</strong>
|
||||
<small id="metricDomain">--</small>
|
||||
<small id="siteStatus" class="metric-status">--</small>
|
||||
</article>
|
||||
<article class="metric-card accent-green">
|
||||
<span data-i18n="metricKeys">Keys</span>
|
||||
<strong id="metricUsers">0</strong>
|
||||
<small data-i18n="configuredUsers">configured users</small>
|
||||
</article>
|
||||
<article class="metric-card accent-violet">
|
||||
<span data-i18n="metricProxyTraffic">Proxy Traffic</span>
|
||||
<strong id="metricProxyTraffic">0 B</strong>
|
||||
<small id="metricProxyPackets">0 packets</small>
|
||||
</article>
|
||||
<article class="metric-card accent-amber">
|
||||
<span data-i18n="metricSiteTraffic">Site Traffic</span>
|
||||
<strong id="metricSiteTraffic">0 B</strong>
|
||||
<small id="metricSitePackets">0 packets</small>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<div class="grid-two">
|
||||
<section class="panel">
|
||||
<div class="panel-head">
|
||||
<div>
|
||||
<p class="eyebrow" data-i18n="servicesEyebrow">Services</p>
|
||||
<h2 class="with-help"><span data-i18n="servicesTitle">Service health</span><span class="info-hint" tabindex="0" data-i18n-title="servicesHelp" title="Service health">?</span></h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="service-grid" id="services"></div>
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
<div class="panel-head">
|
||||
<div>
|
||||
<p class="eyebrow" data-i18n="runtimeEyebrow">Runtime</p>
|
||||
<h2 class="with-help"><span data-i18n="runtimeTitle">telemt summary</span><span class="info-hint" tabindex="0" data-i18n-title="runtimeHelp" title="Runtime data">?</span></h2>
|
||||
</div>
|
||||
</div>
|
||||
<div id="runtimeCards" class="runtime-grid"></div>
|
||||
<div id="runtimeIssues" class="issue-list"></div>
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="page-panel" data-page="traffic">
|
||||
<div class="panel">
|
||||
<div class="panel-head">
|
||||
<div>
|
||||
<p class="eyebrow" data-i18n="trafficEyebrow">Traffic</p>
|
||||
<h2 data-i18n="trafficTitle">History</h2>
|
||||
</div>
|
||||
<div class="panel-actions">
|
||||
<span id="statsHealth" class="status-pill">--</span>
|
||||
<button id="collectStatsBtn" class="ghost" type="button" data-i18n="collectStats" data-i18n-title="collectStatsHelp">Collect</button>
|
||||
<button id="repairStatsBtn" type="button" data-i18n="repairStats" data-i18n-title="repairStatsHelp">Restart collector</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="traffic-summary">
|
||||
<article>
|
||||
<span data-i18n="collector">Collector</span>
|
||||
<strong id="collectorState">--</strong>
|
||||
</article>
|
||||
<article>
|
||||
<span data-i18n="lastPoint">Last point</span>
|
||||
<strong id="lastStatsPoint">--</strong>
|
||||
</article>
|
||||
<article>
|
||||
<span data-i18n="historyRows">History rows</span>
|
||||
<strong id="historyRows">0</strong>
|
||||
</article>
|
||||
</div>
|
||||
<div class="traffic-controls">
|
||||
<div class="segmented" id="trafficRange" aria-label="Traffic range" data-i18n-aria-label="ariaTrafficRange">
|
||||
<button type="button" data-traffic-range="15m" data-i18n="range15m">15 min</button>
|
||||
<button type="button" data-traffic-range="1h" data-i18n="range1h">1 hour</button>
|
||||
<button type="button" data-traffic-range="24h" data-i18n="range24h">24 hours</button>
|
||||
<button type="button" data-traffic-range="month" data-i18n="rangeMonth">Month</button>
|
||||
</div>
|
||||
<div class="segmented" id="trafficView" aria-label="Traffic view" data-i18n-aria-label="ariaTrafficView">
|
||||
<button type="button" data-traffic-view="chart" data-i18n="viewChart">Chart</button>
|
||||
<button type="button" data-traffic-view="table" data-i18n="viewRows">Rows</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="trafficChart" class="traffic-chart"></div>
|
||||
<div class="table-wrap" id="trafficTableWrap">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th data-i18n="tablePeriod">Period</th>
|
||||
<th data-i18n="tableProxyDelta">Proxy delta</th>
|
||||
<th data-i18n="tableSiteDelta">Site delta</th>
|
||||
<th data-i18n="tableProxyTotal">Proxy total</th>
|
||||
<th data-i18n="tableSiteTotal">Site total</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="historyTable"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="page-panel" data-page="keys">
|
||||
<div class="panel">
|
||||
<div class="panel-head">
|
||||
<div>
|
||||
<p class="eyebrow" data-i18n="keysEyebrow">Access</p>
|
||||
<h2 data-i18n="keysTitle">User keys</h2>
|
||||
</div>
|
||||
<form id="addUserForm" class="inline-form">
|
||||
<input id="userName" autocomplete="off" placeholder="client-name" data-i18n-placeholder="userPlaceholder">
|
||||
<button type="submit" data-i18n="addKey">Add key</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="keys-wrap">
|
||||
<div class="keys-list" id="usersTable" aria-live="polite"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel user-traffic-panel" id="userTrafficPanel">
|
||||
<div class="panel-head">
|
||||
<div>
|
||||
<p class="eyebrow" data-i18n="userTrafficEyebrow">Per user</p>
|
||||
<h2 id="userTrafficTitle" data-i18n="userTrafficTitle">User traffic</h2>
|
||||
</div>
|
||||
<div class="panel-actions">
|
||||
<span id="userTrafficHealth" class="status-pill">--</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="traffic-summary compact">
|
||||
<article>
|
||||
<span data-i18n="trafficTotal">Total</span>
|
||||
<strong id="userTrafficTotal">--</strong>
|
||||
</article>
|
||||
<article>
|
||||
<span data-i18n="currentConnections">Connections</span>
|
||||
<strong id="userTrafficConnections">--</strong>
|
||||
</article>
|
||||
<article>
|
||||
<span data-i18n="activeIps">Active IPs</span>
|
||||
<strong id="userTrafficIps">--</strong>
|
||||
</article>
|
||||
</div>
|
||||
<div class="traffic-controls">
|
||||
<div class="segmented" id="userTrafficRange" aria-label="User traffic range" data-i18n-aria-label="ariaTrafficRange">
|
||||
<button type="button" data-user-traffic-range="15m" data-i18n="range15m">15 min</button>
|
||||
<button type="button" data-user-traffic-range="1h" data-i18n="range1h">1 hour</button>
|
||||
<button type="button" data-user-traffic-range="24h" data-i18n="range24h">24 hours</button>
|
||||
<button type="button" data-user-traffic-range="month" data-i18n="rangeMonth">Month</button>
|
||||
</div>
|
||||
<div class="segmented" id="userTrafficView" aria-label="User traffic view" data-i18n-aria-label="ariaTrafficView">
|
||||
<button type="button" data-user-traffic-view="chart" data-i18n="viewChart">Chart</button>
|
||||
<button type="button" data-user-traffic-view="table" data-i18n="viewRows">Rows</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="userTrafficChart" class="traffic-chart"></div>
|
||||
<div class="table-wrap" id="userTrafficTableWrap">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th data-i18n="tablePeriod">Period</th>
|
||||
<th data-i18n="tableTrafficDelta">Traffic delta</th>
|
||||
<th data-i18n="tableTrafficTotal">Total</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="userTrafficTable"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="page-panel" data-page="backups">
|
||||
<div class="grid-two">
|
||||
<section class="panel">
|
||||
<div class="panel-head">
|
||||
<div>
|
||||
<p class="eyebrow" data-i18n="backupsEyebrow">Snapshots</p>
|
||||
<h2 data-i18n="backupsTitle">Backups</h2>
|
||||
</div>
|
||||
<button id="createBackupBtn" type="button" data-i18n="createBackup">Create backup</button>
|
||||
</div>
|
||||
<div class="backup-schedule">
|
||||
<div>
|
||||
<strong data-i18n="backupScheduleTitle">Automatic backups</strong>
|
||||
<span id="backupScheduleMeta" data-i18n="backupScheduleLoading">Loading schedule...</span>
|
||||
</div>
|
||||
<div class="segmented compact" role="group" aria-label="Backup schedule">
|
||||
<button type="button" data-backup-schedule="off" data-i18n="scheduleOff">Off</button>
|
||||
<button type="button" data-backup-schedule="daily" data-i18n="scheduleDaily">Daily</button>
|
||||
<button type="button" data-backup-schedule="weekly" data-i18n="scheduleWeekly">Weekly</button>
|
||||
<button type="button" data-backup-schedule="monthly" data-i18n="scheduleMonthly">Monthly</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="backup-includes">
|
||||
<strong data-i18n="backupIncludesTitle">Backup contents</strong>
|
||||
<span data-i18n="backupIncludesText">telemt config, goTelegram settings, keys, disabled keys, site, templates, SSL certificates, bot, admin panel and traffic history.</span>
|
||||
</div>
|
||||
<div id="backupsList" class="backup-list"></div>
|
||||
</section>
|
||||
|
||||
<aside class="panel">
|
||||
<div class="panel-head">
|
||||
<div>
|
||||
<p class="eyebrow" data-i18n="eventsEyebrow">Events</p>
|
||||
<h2 data-i18n="eventsTitle">Activity</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div id="events" class="events-list"></div>
|
||||
</aside>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="page-panel" data-page="logs">
|
||||
<div class="panel">
|
||||
<div class="panel-head">
|
||||
<div>
|
||||
<p class="eyebrow" data-i18n="logsEyebrow">Journal</p>
|
||||
<h2 data-i18n="logsTitle">Logs</h2>
|
||||
</div>
|
||||
<div class="inline-form">
|
||||
<select id="logService">
|
||||
<option value="telemt">telemt</option>
|
||||
<option value="nginx">nginx</option>
|
||||
<option value="gotelegram-bot">bot</option>
|
||||
<option value="gotelegram-stats">stats</option>
|
||||
<option value="gotelegram-admin">admin</option>
|
||||
</select>
|
||||
<button id="loadLogsBtn" type="button" data-i18n="loadLogs">Load</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="logsMeta" class="logs-meta"></div>
|
||||
<pre id="logsBox" class="logs"></pre>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="page-panel" data-page="settings">
|
||||
<div class="grid-two">
|
||||
<section class="panel">
|
||||
<div class="panel-head">
|
||||
<div>
|
||||
<p class="eyebrow" data-i18n="settingsEyebrow">Settings</p>
|
||||
<h2 data-i18n="settingsTitle">Panel preferences</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-list">
|
||||
<div>
|
||||
<span data-i18n="panelLanguage">Panel language</span>
|
||||
<strong id="settingsLanguage">--</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span data-i18n="theme">Theme</span>
|
||||
<strong id="settingsTheme">--</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span data-i18n="bindAddress">Bind address</span>
|
||||
<strong id="settingsBind">127.0.0.1:1984</strong>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
<div class="panel-head">
|
||||
<div>
|
||||
<p class="eyebrow" data-i18n="configEyebrow">Config</p>
|
||||
<h2 data-i18n="configTitle">Installation state</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div id="configList" class="settings-list"></div>
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="toast" class="toast"></div>
|
||||
<div id="qrModal" class="promo-modal" hidden>
|
||||
<div class="promo-card qr-card" role="dialog" aria-modal="true" aria-labelledby="qrTitle">
|
||||
<button id="qrClose" class="icon-btn ghost" type="button" aria-label="Close" data-i18n-aria-label="ariaClose">×</button>
|
||||
<p class="eyebrow" data-i18n="qrEyebrow">QR import</p>
|
||||
<h2 id="qrTitle" data-i18n="qrTitle">Scan Telegram proxy</h2>
|
||||
<div class="qr-frame">
|
||||
<img id="qrImage" alt="Telegram proxy QR">
|
||||
</div>
|
||||
<p id="qrMeta" class="modal-note"></p>
|
||||
<button id="qrCopyBtn" type="button" class="soft" data-i18n="copyLink">Copy link</button>
|
||||
</div>
|
||||
</div>
|
||||
<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" data-i18n-aria-label="ariaClose">×</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-admin18" type="module"></script>
|
||||
</body>
|
||||
</html>
|
||||
1708
admin-web/static/styles.css
Normal file
1708
admin-web/static/styles.css
Normal file
File diff suppressed because it is too large
Load Diff
18
bootstrap.sh
18
bootstrap.sh
@@ -5,8 +5,8 @@
|
||||
set -euo pipefail
|
||||
|
||||
REPO="anten-ka/gotelegram_pro"
|
||||
BRANCH="${GOTELEGRAM_BRANCH:-test}"
|
||||
PAT="${GOTELEGRAM_PAT:-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}"
|
||||
BRANCH="${GOTELEGRAM_BRANCH:-alfa}"
|
||||
PAT="${GOTELEGRAM_PAT:-}"
|
||||
INSTALL_DIR="/opt/gotelegram"
|
||||
# Use raw.githubusercontent.com (CDN) — faster and avoids Contents API caching
|
||||
# issues that occasionally return 404 for recently added files on non-default branches.
|
||||
@@ -34,6 +34,12 @@ if [ "$(id -u)" -ne 0 ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "$PAT" ]; then
|
||||
echo -e " ${RED}✗${NC} Не задан GitHub token."
|
||||
echo -e " ${YELLOW}Запустите так:${NC} ${CYAN}GOTELEGRAM_PAT=YOUR_PAT sudo -E bash bootstrap.sh${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check dependencies
|
||||
for cmd in curl jq; do
|
||||
if ! command -v "$cmd" &>/dev/null; then
|
||||
@@ -87,10 +93,14 @@ FILES=(
|
||||
"gotelegram-bot/config.example.env"
|
||||
"gotelegram-bot/requirements.txt"
|
||||
"gotelegram-bot/README.md"
|
||||
"admin-web/server.py"
|
||||
"admin-web/static/index.html"
|
||||
"admin-web/static/styles.css"
|
||||
"admin-web/static/app.js"
|
||||
)
|
||||
|
||||
echo -e " ${CYAN}↻${NC} Загрузка файлов в ${INSTALL_DIR}..."
|
||||
mkdir -p "${INSTALL_DIR}/lib/lang" "${INSTALL_DIR}/gotelegram-bot/lang"
|
||||
mkdir -p "${INSTALL_DIR}/lib/lang" "${INSTALL_DIR}/gotelegram-bot/lang" "${INSTALL_DIR}/admin-web/static"
|
||||
|
||||
failed=0
|
||||
for f in "${FILES[@]}"; do
|
||||
@@ -112,8 +122,10 @@ fi
|
||||
echo -e " ${CYAN}↻${NC} Настройка прав..."
|
||||
chmod +x "${INSTALL_DIR}/install.sh" "${INSTALL_DIR}/install_gotelegram_bot.sh"
|
||||
chmod +x "${INSTALL_DIR}"/lib/*.sh
|
||||
chmod +x "${INSTALL_DIR}/admin-web/server.py" 2>/dev/null || true
|
||||
sed -i 's/\r$//' "${INSTALL_DIR}/install.sh" "${INSTALL_DIR}/install_gotelegram_bot.sh" "${INSTALL_DIR}"/lib/*.sh "${INSTALL_DIR}"/lib/lang/*.sh 2>/dev/null || true
|
||||
sed -i 's/\r$//' "${INSTALL_DIR}"/gotelegram-bot/*.py "${INSTALL_DIR}"/gotelegram-bot/lang/*.json 2>/dev/null || true
|
||||
sed -i 's/\r$//' "${INSTALL_DIR}"/admin-web/*.py "${INSTALL_DIR}"/admin-web/static/* 2>/dev/null || true
|
||||
|
||||
# Create symlink
|
||||
ln -sf "${INSTALL_DIR}/install.sh" /usr/local/bin/gotelegram
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# GoTelegram v2.2 Bot
|
||||
# goTelegram Pro v2.5.0 Bot
|
||||
|
||||
Production-quality Telegram bot for managing MTProxy (telemt engine) on Linux servers.
|
||||
|
||||
@@ -19,6 +19,9 @@ Production-quality Telegram bot for managing MTProxy (telemt engine) on Linux se
|
||||
- Promotional links
|
||||
|
||||
- **Template Browsing** - Browse categories → templates → preview → install
|
||||
- **Per-user MTProxy Keys** - Manage telemt `[access.users]` from inline bot menus
|
||||
- **Per-user QR Import** - Show QR codes for every Telegram proxy key
|
||||
- **Local Web Admin** - Shows SSH tunnel instructions for the 127.0.0.1:1984 dashboard
|
||||
- **V1 Migration** - Detects old mtg Docker container and offers migration
|
||||
- **Access Control** - ALLOWED_IDS from .env
|
||||
- **Async/Await** - Full async support via python-telegram-bot v21+
|
||||
@@ -28,6 +31,14 @@ Production-quality Telegram bot for managing MTProxy (telemt engine) on Linux se
|
||||
|
||||
## Installation
|
||||
|
||||
Recommended installation path is the main CLI menu:
|
||||
|
||||
```bash
|
||||
gotelegram
|
||||
```
|
||||
|
||||
Then choose `12) Telegram-bot` → install/update. On repeat goTelegram Pro bootstrap/update, an already installed bot is refreshed automatically: code, i18n files and requirements are copied to `/opt/gotelegram-bot`, `.env` is preserved, dependencies are checked and `gotelegram-bot` is restarted.
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Python 3.8+
|
||||
@@ -65,16 +76,16 @@ For systemd service:
|
||||
|
||||
```bash
|
||||
[Unit]
|
||||
Description=GoTelegram Bot
|
||||
Description=goTelegram Pro Bot
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=gotelegram
|
||||
WorkingDirectory=/opt/gotelegram/bot
|
||||
ExecStart=/usr/bin/python3 /opt/gotelegram/bot/bot.py
|
||||
WorkingDirectory=/opt/gotelegram-bot
|
||||
ExecStart=/opt/gotelegram-bot/venv/bin/python /opt/gotelegram-bot/bot.py
|
||||
Restart=always
|
||||
RestartSec=10
|
||||
RestartSec=5
|
||||
Environment=PATH=/opt/gotelegram-bot/venv/bin:/usr/bin
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -86,6 +97,7 @@ WantedBy=multi-user.target
|
||||
|
||||
- `BOT_TOKEN` - Telegram bot token (required)
|
||||
- `ALLOWED_IDS` - Comma-separated user IDs (optional, all users allowed if empty)
|
||||
- `BOT_LANG` - Default language inherited from goTelegram Pro install language
|
||||
|
||||
### System Paths
|
||||
|
||||
@@ -94,6 +106,7 @@ WantedBy=multi-user.target
|
||||
- `TELEMT_SERVICE` - `telemt` (systemd service name)
|
||||
- `WEBSITE_ROOT` - `/var/www/gotelegram-site`
|
||||
- `BACKUP_DIR` - `/opt/gotelegram/backups`
|
||||
- `BACKUP_SCHEDULE_FILE` - `/opt/gotelegram/backup_schedule.json`
|
||||
- `TEMPLATES_CATALOG` - `/opt/gotelegram/templates_catalog.json`
|
||||
|
||||
## Architecture
|
||||
@@ -112,6 +125,7 @@ Organized by feature:
|
||||
- Installation (quick/stealth modes)
|
||||
- Status monitoring
|
||||
- Backup/restore
|
||||
- Backup schedules: off, daily, weekly, monthly
|
||||
- SSL management
|
||||
- Updates
|
||||
- Removal
|
||||
@@ -144,4 +158,4 @@ code, stdout, stderr = await sh("command", "arg1", "arg2")
|
||||
|
||||
## License
|
||||
|
||||
GoTelegram v2.2 - Open source community project
|
||||
goTelegram Pro v2.5.0 - Open source community project
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,4 @@
|
||||
# GoTelegram v2.2 Bot Configuration
|
||||
# goTelegram Pro v2.5.0 Bot Configuration
|
||||
# Copy this to .env and fill in your values
|
||||
|
||||
# Telegram Bot Token from @BotFather
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"""
|
||||
GoTelegram v2.4 Bot — i18n module
|
||||
goTelegram Pro v2.5.0 Bot — i18n module
|
||||
Provides per-user language preferences and a simple t()/tf() API.
|
||||
|
||||
Usage:
|
||||
@@ -24,12 +24,37 @@ logger = logging.getLogger(__name__)
|
||||
_MODULE_DIR = Path(__file__).resolve().parent
|
||||
LANG_DIR = _MODULE_DIR / "lang"
|
||||
USER_LANG_FILE = Path("/opt/gotelegram-bot/user_langs.json")
|
||||
GOTELEGRAM_CONFIG = Path("/opt/gotelegram/config.json")
|
||||
GOTELEGRAM_LANG_MARKER = Path("/opt/gotelegram/.language")
|
||||
|
||||
# Supported codes; keep in sync with lang/*.json
|
||||
SUPPORTED_LANGS = ("en", "ru")
|
||||
DEFAULT_LANG = os.getenv("BOT_LANG", "en").strip().lower() or "en"
|
||||
if DEFAULT_LANG not in SUPPORTED_LANGS:
|
||||
DEFAULT_LANG = "en"
|
||||
|
||||
|
||||
def _detect_default_lang() -> str:
|
||||
candidates = []
|
||||
try:
|
||||
if GOTELEGRAM_CONFIG.exists():
|
||||
with open(GOTELEGRAM_CONFIG, "r", encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
if isinstance(data, dict):
|
||||
candidates.extend([data.get("language"), data.get("lang")])
|
||||
except Exception as e:
|
||||
logger.warning("failed to read goTelegram Pro language config: %s", e)
|
||||
try:
|
||||
if GOTELEGRAM_LANG_MARKER.exists():
|
||||
candidates.append(GOTELEGRAM_LANG_MARKER.read_text(encoding="utf-8").strip()[:2])
|
||||
except Exception as e:
|
||||
logger.warning("failed to read goTelegram Pro language marker: %s", e)
|
||||
candidates.append(os.getenv("BOT_LANG", ""))
|
||||
for raw in candidates:
|
||||
code = str(raw or "").strip().lower()
|
||||
if code in SUPPORTED_LANGS:
|
||||
return code
|
||||
return "en"
|
||||
|
||||
|
||||
DEFAULT_LANG = _detect_default_lang()
|
||||
|
||||
LANG_NAMES = {
|
||||
"en": "English",
|
||||
@@ -100,8 +125,8 @@ def get_user_lang(user_id: Optional[int]) -> str:
|
||||
if not _USER_LANGS_LOADED:
|
||||
_load_user_langs()
|
||||
if user_id is None:
|
||||
return DEFAULT_LANG
|
||||
return _USER_LANGS.get(int(user_id), DEFAULT_LANG)
|
||||
return _detect_default_lang()
|
||||
return _USER_LANGS.get(int(user_id), _detect_default_lang())
|
||||
|
||||
|
||||
def set_user_lang(user_id: int, code: str) -> bool:
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"lang_saved": "Language saved: %s",
|
||||
"lang_choose": "Choose your language:",
|
||||
|
||||
"welcome_title": "GoTelegram v%s",
|
||||
"welcome_title": "goTelegram Pro v%s",
|
||||
"welcome_subtitle": "🤖 MTProxy Management Bot",
|
||||
"welcome_powered": "Powered by telemt engine",
|
||||
"welcome_prompt": "Select an action from the menu below:",
|
||||
@@ -17,7 +17,7 @@
|
||||
"btn_no": "❌ No",
|
||||
|
||||
"access_denied": "⛔ Access denied.\nYour ID: <code>%s</code>",
|
||||
"help_title": "GoTelegram Bot — Commands",
|
||||
"help_title": "goTelegram Pro Bot — Commands",
|
||||
"help_lines": "/start — Main menu\n/help — This help\n/status — Quick status\n/logs — Latest logs\n/lang — Change language\n/addadmin ID — Add admin\n/deladmin ID — Remove admin\n\nUse the menu buttons for other operations.",
|
||||
|
||||
"menu_install": "⚙️ Install",
|
||||
@@ -33,6 +33,8 @@
|
||||
"menu_website": "🌐 Website/SSL",
|
||||
"menu_promo": "🎁 Promo",
|
||||
"menu_stats": "📊 Traffic Stats",
|
||||
"menu_users": "🔑 Keys",
|
||||
"menu_admin_web": "🖥 Web Admin",
|
||||
"menu_remove": "🗑️ Remove",
|
||||
"menu_admins": "👤 Admins",
|
||||
"menu_credits": "ℹ️ Credits",
|
||||
@@ -112,21 +114,5 @@
|
||||
"cg_timeout": "❌ Clone timeout (repository too large or slow)",
|
||||
"cg_too_big": "❌ Repository too large (>100MB)",
|
||||
"cg_no_index": "❌ No index.html found in repository",
|
||||
"cg_ok_fmt": "✅ Custom template downloaded: %s",
|
||||
|
||||
"stats_title": "Statistics",
|
||||
"stats_unavailable": "Data unavailable. Make sure stats module is enabled.",
|
||||
"stats_traffic_title": "Traffic statistics",
|
||||
"stats_proxy_label": "Proxy (telemt)",
|
||||
"stats_site_label": "Site (nginx)",
|
||||
"stats_hdr_period": "Period",
|
||||
"stats_hdr_traffic": "Traffic",
|
||||
"stats_hdr_rate": "Rate",
|
||||
"stats_1min": "1 min",
|
||||
"stats_5min": "5 min",
|
||||
"stats_60min": "60 min",
|
||||
"stats_1day": "1 day",
|
||||
"stats_7days": "7 days",
|
||||
"stats_30days": "30 days",
|
||||
"stats_365days": "365 days"
|
||||
"cg_ok_fmt": "✅ Custom template downloaded: %s"
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"lang_saved": "Язык сохранён: %s",
|
||||
"lang_choose": "Выберите язык:",
|
||||
|
||||
"welcome_title": "GoTelegram v%s",
|
||||
"welcome_title": "goTelegram Pro v%s",
|
||||
"welcome_subtitle": "🤖 Бот управления MTProxy",
|
||||
"welcome_powered": "На базе движка telemt",
|
||||
"welcome_prompt": "Выберите действие в меню ниже:",
|
||||
@@ -17,7 +17,7 @@
|
||||
"btn_no": "❌ Нет",
|
||||
|
||||
"access_denied": "⛔ Доступ запрещён.\nВаш ID: <code>%s</code>",
|
||||
"help_title": "GoTelegram Bot — Команды",
|
||||
"help_title": "goTelegram Pro Bot — Команды",
|
||||
"help_lines": "/start — Главное меню\n/help — Эта справка\n/status — Быстрый статус\n/logs — Последние логи\n/lang — Сменить язык\n/addadmin ID — Добавить админа\n/deladmin ID — Удалить админа\n\nИспользуйте кнопки меню для остальных операций.",
|
||||
|
||||
"menu_install": "⚙️ Установить",
|
||||
@@ -33,6 +33,8 @@
|
||||
"menu_website": "🌐 Сайт/SSL",
|
||||
"menu_promo": "🎁 Промо",
|
||||
"menu_stats": "📊 Трафик",
|
||||
"menu_users": "🔑 Ключи",
|
||||
"menu_admin_web": "🖥 Веб-админка",
|
||||
"menu_remove": "🗑️ Удалить",
|
||||
"menu_admins": "👤 Админы",
|
||||
"menu_credits": "ℹ️ О проекте",
|
||||
@@ -70,7 +72,7 @@
|
||||
"install_title": "⚙️ Установка / Обновление",
|
||||
"install_pick_mode": "Выберите режим установки:",
|
||||
"install_mode_lite": "🚀 Lite (быстро, без сайта)",
|
||||
"install_mode_pro": "🎨 Pro (stealth + сайт)",
|
||||
"install_mode_pro": "🎨 Pro (маскировка + сайт)",
|
||||
|
||||
"backup_title": "💾 Бекап",
|
||||
"backup_creating": "⏳ Создаю бекап...",
|
||||
@@ -112,21 +114,5 @@
|
||||
"cg_timeout": "❌ Таймаут клонирования (репозиторий слишком большой или медленный)",
|
||||
"cg_too_big": "❌ Репозиторий слишком большой (>100МБ)",
|
||||
"cg_no_index": "❌ В репозитории не найден index.html",
|
||||
"cg_ok_fmt": "✅ Свой шаблон загружен: %s",
|
||||
|
||||
"stats_title": "Статистика",
|
||||
"stats_unavailable": "Данные недоступны. Убедитесь что модуль статистики включён.",
|
||||
"stats_traffic_title": "Статистика трафика",
|
||||
"stats_proxy_label": "Proxy (telemt)",
|
||||
"stats_site_label": "Сайт (nginx)",
|
||||
"stats_hdr_period": "Период",
|
||||
"stats_hdr_traffic": "Трафик",
|
||||
"stats_hdr_rate": "Скорость",
|
||||
"stats_1min": "1 мин",
|
||||
"stats_5min": "5 мин",
|
||||
"stats_60min": "60 мин",
|
||||
"stats_1day": "1 день",
|
||||
"stats_7days": "7 дней",
|
||||
"stats_30days": "30 дней",
|
||||
"stats_365days": "365 дней"
|
||||
"cg_ok_fmt": "✅ Свой шаблон загружен: %s"
|
||||
}
|
||||
|
||||
470
install.sh
Normal file → Executable file
470
install.sh
Normal file → Executable file
@@ -1,6 +1,6 @@
|
||||
#!/bin/bash
|
||||
# ══════════════════════════════════════════════════════════════════════════════
|
||||
# GoTelegram v2.4.0 — MTProxy powered by telemt (Rust + Tokio)
|
||||
# goTelegram Pro v2.5.0 — MTProxy powered by telemt (Rust + Tokio)
|
||||
# Anti-DPI • Fake TLS • TCP Splice • JA3/JA4 Resistance • i18n (EN/RU)
|
||||
#
|
||||
# Install:
|
||||
@@ -22,6 +22,7 @@ source "$LIB_DIR/website.sh"
|
||||
source "$LIB_DIR/templates_catalog.sh"
|
||||
source "$LIB_DIR/backup.sh"
|
||||
[ -f "$LIB_DIR/stats.sh" ] && source "$LIB_DIR/stats.sh"
|
||||
[ -f "$LIB_DIR/shared443.sh" ] && source "$LIB_DIR/shared443.sh"
|
||||
|
||||
# Load language (from config.json or marker file, default en)
|
||||
load_language "$(detect_language)"
|
||||
@@ -45,7 +46,7 @@ show_main_menu() {
|
||||
# ── Header (no right border — ANSI breaks alignment) ──
|
||||
echo ""
|
||||
echo -e " ${BOLD}${CYAN}━${line}━${NC}"
|
||||
echo -e " ${BOLD}${WHITE} GoTelegram v${GOTELEGRAM_VERSION}${NC} ${DIM}— $(t dashboard_title)${NC}"
|
||||
echo -e " ${BOLD}${WHITE} goTelegram Pro v${GOTELEGRAM_VERSION}${NC} ${DIM}— $(t dashboard_title)${NC}"
|
||||
echo -e " ${BOLD}${CYAN}━${line}━${NC}"
|
||||
|
||||
# ── Service health ──
|
||||
@@ -245,6 +246,195 @@ menu_version() {
|
||||
echo -e " ${DIM}$(printf '─%.0s' {1..54})${NC}"
|
||||
}
|
||||
|
||||
# ── Upgrade migration ────────────────────────────────────────────────────────
|
||||
snapshot_preupgrade_state() {
|
||||
local marker="$GOTELEGRAM_DIR/.preupgrade_${GOTELEGRAM_VERSION}_done"
|
||||
[ -f "$marker" ] && return 0
|
||||
mkdir -p "$BACKUP_DIR"
|
||||
|
||||
local ts tmp archive
|
||||
ts=$(date +%Y%m%d_%H%M%S)
|
||||
tmp="/tmp/gotelegram_preupgrade_${ts}"
|
||||
archive="$BACKUP_DIR/preupgrade_${GOTELEGRAM_VERSION}_${ts}.tar.gz"
|
||||
mkdir -p "$tmp"
|
||||
|
||||
[ -f "$GOTELEGRAM_CONFIG" ] && mkdir -p "$tmp/opt/gotelegram" && cp "$GOTELEGRAM_CONFIG" "$tmp/opt/gotelegram/config.json" 2>/dev/null
|
||||
[ -f "$TELEMT_CONFIG" ] && mkdir -p "$tmp/etc/telemt" && cp "$TELEMT_CONFIG" "$tmp/etc/telemt/config.toml" 2>/dev/null
|
||||
[ -f "$NGINX_SITE_CONF" ] && mkdir -p "$tmp/etc/nginx/sites-available" && cp "$NGINX_SITE_CONF" "$tmp/etc/nginx/sites-available/gotelegram" 2>/dev/null
|
||||
[ -d "$WEBSITE_ROOT" ] && mkdir -p "$tmp/var/www/gotelegram-site" && cp -a "$WEBSITE_ROOT/." "$tmp/var/www/gotelegram-site/" 2>/dev/null
|
||||
[ -f "$BOT_DIR/.env" ] && mkdir -p "$tmp/opt/gotelegram-bot" && cp "$BOT_DIR/.env" "$tmp/opt/gotelegram-bot/.env" 2>/dev/null
|
||||
|
||||
if tar czf "$archive" -C "$tmp" . 2>/dev/null; then
|
||||
log_dim "Pre-upgrade snapshot: $archive"
|
||||
touch "$marker" 2>/dev/null || true
|
||||
fi
|
||||
rm -rf "$tmp"
|
||||
}
|
||||
|
||||
read_config_or_default() {
|
||||
local key="$1" fallback="$2"
|
||||
config_get "$key" 2>/dev/null || echo "$fallback"
|
||||
}
|
||||
|
||||
detect_deployed_template_id() {
|
||||
local tpl=""
|
||||
if [ -f "$WEBSITE_ROOT/.gotelegram_template_id" ]; then
|
||||
tpl=$(head -1 "$WEBSITE_ROOT/.gotelegram_template_id" 2>/dev/null || echo "")
|
||||
[ -n "$tpl" ] && { echo "$tpl"; return 0; }
|
||||
fi
|
||||
if [ -d "$WEBSITE_ROOT" ] && [ -f "$WEBSITE_ROOT/index.html" ]; then
|
||||
echo "deployed_site"
|
||||
return 0
|
||||
fi
|
||||
tpl=$(read_config_or_default template_id "")
|
||||
[ -n "$tpl" ] && { echo "$tpl"; return 0; }
|
||||
echo ""
|
||||
}
|
||||
|
||||
detect_template_source() {
|
||||
local src
|
||||
if [ -f "$WEBSITE_ROOT/.gotelegram_template_source" ]; then
|
||||
src=$(head -1 "$WEBSITE_ROOT/.gotelegram_template_source" 2>/dev/null || echo "")
|
||||
[ -n "$src" ] && { echo "$src"; return 0; }
|
||||
fi
|
||||
[ -d "$WEBSITE_ROOT" ] && [ -f "$WEBSITE_ROOT/index.html" ] && return 0
|
||||
read_config_or_default template_source ""
|
||||
}
|
||||
|
||||
write_normalized_gotelegram_config() {
|
||||
local mode="$1" port="$2" secret="$3" mask_host="$4" domain="$5" tpl_id="$6" tpl_source="$7"
|
||||
local lang installed_at stats_enabled tmp
|
||||
lang=$(read_config_or_default language "$(get_language 2>/dev/null || echo en)")
|
||||
installed_at=$(read_config_or_default installed_at "$(date -Iseconds)")
|
||||
stats_enabled=$(read_config_or_default stats_enabled "")
|
||||
tmp=$(mktemp) || return 1
|
||||
|
||||
jq -n \
|
||||
--arg version "$GOTELEGRAM_VERSION" \
|
||||
--arg engine "telemt" \
|
||||
--arg mode "$mode" \
|
||||
--argjson port "$port" \
|
||||
--arg secret "$secret" \
|
||||
--arg mask_host "$mask_host" \
|
||||
--arg domain "$domain" \
|
||||
--arg template_id "$tpl_id" \
|
||||
--arg template_source "$tpl_source" \
|
||||
--arg language "$lang" \
|
||||
--arg installed_at "$installed_at" \
|
||||
--arg updated_at "$(date -Iseconds)" \
|
||||
--arg stats_enabled "$stats_enabled" \
|
||||
'{
|
||||
version: $version,
|
||||
engine: $engine,
|
||||
mode: $mode,
|
||||
port: $port,
|
||||
secret: $secret,
|
||||
mask_host: $mask_host,
|
||||
domain: $domain,
|
||||
template_id: $template_id,
|
||||
language: $language,
|
||||
installed_at: $installed_at,
|
||||
updated_at: $updated_at
|
||||
}
|
||||
+ (if $template_source != "" then {template_source: $template_source} else {} end)
|
||||
+ (if $stats_enabled == "true" then {stats_enabled: true} elif $stats_enabled == "false" then {stats_enabled: false} else {} end)' \
|
||||
> "$tmp" || { rm -f "$tmp"; return 1; }
|
||||
|
||||
mkdir -p "$(dirname "$GOTELEGRAM_CONFIG")"
|
||||
mv "$tmp" "$GOTELEGRAM_CONFIG"
|
||||
chmod 600 "$GOTELEGRAM_CONFIG"
|
||||
}
|
||||
|
||||
auto_migrate_legacy_state() {
|
||||
local marker="$GOTELEGRAM_DIR/.migrated_${GOTELEGRAM_VERSION}"
|
||||
local current_version
|
||||
current_version=$(read_config_or_default version "")
|
||||
if [ -f "$marker" ] && [ "$current_version" = "$GOTELEGRAM_VERSION" ]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
[ -f "$TELEMT_CONFIG" ] || [ -f "$GOTELEGRAM_CONFIG" ] || [ -d "$WEBSITE_ROOT" ] || return 0
|
||||
|
||||
log_step "Миграция состояния goTelegram Pro"
|
||||
snapshot_preupgrade_state
|
||||
|
||||
local mode port secret mask_host domain mask_port tpl_id tpl_source users_block tls_emulation changed=0 users_block_needs_write=0
|
||||
users_block=$(get_telemt_users_block "$TELEMT_CONFIG" 2>/dev/null || true)
|
||||
secret=$(get_config_value secret "$TELEMT_CONFIG" 2>/dev/null || echo "")
|
||||
[ -z "$secret" ] && secret=$(read_config_or_default secret "")
|
||||
[ -z "$secret" ] && secret=$(first_telemt_user_secret "$TELEMT_CONFIG" 2>/dev/null || echo "")
|
||||
[ -z "$secret" ] && secret=$(generate_hex 32)
|
||||
|
||||
if [ -n "$users_block" ] && ! telemt_users_block_has_main "$users_block"; then
|
||||
users_block=$(printf 'main = "%s"\n%s\n' "$secret" "$users_block")
|
||||
users_block_needs_write=1
|
||||
fi
|
||||
if [ -z "$users_block" ]; then
|
||||
users_block="main = \"$secret\""
|
||||
users_block_needs_write=1
|
||||
fi
|
||||
|
||||
port=$(get_config_value port "$TELEMT_CONFIG" 2>/dev/null || echo "")
|
||||
[ -z "$port" ] && port=$(read_config_or_default port "443")
|
||||
[[ "$port" =~ ^[0-9]+$ ]] || port=443
|
||||
|
||||
mask_host=$(get_config_value mask_host "$TELEMT_CONFIG" 2>/dev/null || echo "")
|
||||
[ -z "$mask_host" ] && mask_host=$(read_config_or_default mask_host "google.com")
|
||||
domain=$(read_config_or_default domain "")
|
||||
mask_port=$(get_config_value mask_port "$TELEMT_CONFIG" 2>/dev/null || echo "")
|
||||
[ -z "$mask_port" ] && mask_port="443"
|
||||
tls_emulation=$(toml_bool_value censorship tls_emulation "$TELEMT_CONFIG" 2>/dev/null || echo "")
|
||||
|
||||
mode=$(read_config_or_default mode "")
|
||||
if [ -z "$mode" ]; then
|
||||
if [ -n "$domain" ] || [ "$tls_emulation" = "false" ] || grep -q 'dns_overrides' "$TELEMT_CONFIG" 2>/dev/null; then
|
||||
mode="pro"
|
||||
else
|
||||
mode="lite"
|
||||
fi
|
||||
fi
|
||||
if [ "$mode" = "pro" ]; then
|
||||
[ -z "$domain" ] && domain="$mask_host"
|
||||
[ -n "$domain" ] && mask_host="$domain"
|
||||
[ "$mask_port" = "443" ] && mask_port="8443"
|
||||
else
|
||||
domain=""
|
||||
mask_port="443"
|
||||
fi
|
||||
|
||||
tpl_id=$(detect_deployed_template_id)
|
||||
tpl_source=$(detect_template_source || echo "")
|
||||
if [ -d "$WEBSITE_ROOT" ] && [ -f "$WEBSITE_ROOT/index.html" ] && [ -n "$tpl_id" ]; then
|
||||
echo "$tpl_id" > "$WEBSITE_ROOT/.gotelegram_template_id" 2>/dev/null || true
|
||||
[ -n "$tpl_source" ] && echo "$tpl_source" > "$WEBSITE_ROOT/.gotelegram_template_source" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
if [ -f "$TELEMT_CONFIG" ]; then
|
||||
if ! grep -q '\[server.api\]' "$TELEMT_CONFIG" 2>/dev/null || \
|
||||
! grep -q 'metrics_listen' "$TELEMT_CONFIG" 2>/dev/null || \
|
||||
! grep -q "goTelegram Pro v${GOTELEGRAM_VERSION}" "$TELEMT_CONFIG" 2>/dev/null; then
|
||||
generate_telemt_toml "$secret" "$port" "$mode" "$mask_host" "$mask_port" "$TELEMT_CONFIG" >&2
|
||||
replace_telemt_users_block "$users_block" "$TELEMT_CONFIG"
|
||||
changed=1
|
||||
users_block_needs_write=0
|
||||
elif [ "$users_block_needs_write" = "1" ]; then
|
||||
replace_telemt_users_block "$users_block" "$TELEMT_CONFIG"
|
||||
changed=1
|
||||
fi
|
||||
fi
|
||||
|
||||
write_normalized_gotelegram_config "$mode" "$port" "$secret" "$mask_host" "$domain" "$tpl_id" "$tpl_source" || \
|
||||
log_warning "Не удалось нормализовать config.json"
|
||||
|
||||
if [ "$changed" = "1" ] && systemctl is-active --quiet "$TELEMT_SERVICE" 2>/dev/null; then
|
||||
log_info "Перезапускаю telemt, чтобы применить нормализованный конфиг..."
|
||||
restart_telemt || log_warning "telemt не перезапустился после миграции; проверьте journalctl -u telemt"
|
||||
fi
|
||||
|
||||
touch "$marker" 2>/dev/null || true
|
||||
log_success "Миграция завершена: ключи, режим, домен и сайт сохранены"
|
||||
}
|
||||
|
||||
# ── Install: mode selection ─────────────────────────────────────────────────
|
||||
menu_install() {
|
||||
# Check for v1
|
||||
@@ -258,49 +448,6 @@ menu_install() {
|
||||
fi
|
||||
fi
|
||||
|
||||
# Always start from a clean slate — any leftover env vars from a previous
|
||||
# manual-key entry must not leak into a "new install" flow.
|
||||
unset GOTELEGRAM_EXISTING_SECRET GOTELEGRAM_EXISTING_DOMAIN GOTELEGRAM_EXISTING_PORT
|
||||
|
||||
# ── Step 1: install source picker ────────────────────────────────────────
|
||||
echo ""
|
||||
echo -e " ${BOLD}${WHITE}$(_t_or install_source_title 'Источник установки')${NC}"
|
||||
echo -e " ${DIM}$(printf '─%.0s' {1..55})${NC}"
|
||||
echo -e " ${CYAN}1)${NC} ${GREEN}$(_t_or install_menu_new 'Новая установка')${NC}"
|
||||
echo -e " ${DIM}$(_t_or install_menu_new_desc 'Сгенерировать новый ключ и настроить с нуля')${NC}"
|
||||
echo ""
|
||||
echo -e " ${CYAN}2)${NC} ${BLUE}$(_t_or install_menu_restore 'Восстановить из бекапа')${NC}"
|
||||
echo -e " ${DIM}$(_t_or install_menu_restore_desc 'Полное восстановление из файла .tar.gz[.enc]')${NC}"
|
||||
echo ""
|
||||
echo -e " ${CYAN}3)${NC} ${YELLOW}$(_t_or install_menu_existing_key 'Использовать существующий ключ')${NC}"
|
||||
echo -e " ${DIM}$(_t_or install_menu_existing_key_desc 'Ввести ссылку tg://proxy или ключ вручную')${NC}"
|
||||
echo -e " ${DIM}$(printf '─%.0s' {1..55})${NC}"
|
||||
echo -ne " ${WHITE}$(_t_or install_source_choice 'Выберите источник')${NC} "
|
||||
read -r src_choice
|
||||
src_choice="${src_choice:-}"
|
||||
|
||||
case "$src_choice" in
|
||||
1) : ;; # fall through to mode picker
|
||||
2)
|
||||
if type interactive_restore &>/dev/null; then
|
||||
interactive_restore
|
||||
else
|
||||
log_error "backup.sh not loaded"
|
||||
fi
|
||||
return
|
||||
;;
|
||||
3)
|
||||
if type manual_secret_input &>/dev/null; then
|
||||
manual_secret_input || return
|
||||
else
|
||||
log_error "backup.sh not loaded"
|
||||
return
|
||||
fi
|
||||
;;
|
||||
*) log_error "$(tf install_bad_choice "${src_choice:-<empty>}")" ; return ;;
|
||||
esac
|
||||
|
||||
# ── Step 2: lite/pro mode picker ─────────────────────────────────────────
|
||||
echo ""
|
||||
echo -e " ${BOLD}${WHITE}$(t install_select_mode)${NC}"
|
||||
echo -e " ${DIM}$(printf '─%.0s' {1..55})${NC}"
|
||||
@@ -317,19 +464,11 @@ menu_install() {
|
||||
read -r mode_choice
|
||||
mode_choice="${mode_choice:-}"
|
||||
|
||||
# If user provided an ee-prefixed key with a domain, hint at pro mode
|
||||
if [ -n "${GOTELEGRAM_EXISTING_DOMAIN:-}" ] && [ "$mode_choice" = "1" ]; then
|
||||
log_warning "$(_t_or install_hint_pro_mode 'Ключ содержит домен — обычно это Pro режим')"
|
||||
fi
|
||||
|
||||
case "$mode_choice" in
|
||||
1) install_lite_mode ;;
|
||||
2) install_pro_mode ;;
|
||||
*) log_error "$(tf install_bad_choice "${mode_choice:-<empty>}")" ;;
|
||||
esac
|
||||
|
||||
# Clean up env vars after install, just in case
|
||||
unset GOTELEGRAM_EXISTING_SECRET GOTELEGRAM_EXISTING_DOMAIN GOTELEGRAM_EXISTING_PORT
|
||||
}
|
||||
|
||||
# ── Lite mode ───────────────────────────────────────────────────────────────
|
||||
@@ -341,30 +480,17 @@ install_lite_mode() {
|
||||
domain=$(select_quick_domain)
|
||||
[ $? -ne 0 ] && return
|
||||
|
||||
# Port selection — if user provided a port via existing-key flow, reuse it
|
||||
# Port selection
|
||||
local port
|
||||
if [ -n "${GOTELEGRAM_EXISTING_PORT:-}" ] && [[ "$GOTELEGRAM_EXISTING_PORT" =~ ^[0-9]+$ ]]; then
|
||||
port="$GOTELEGRAM_EXISTING_PORT"
|
||||
log_info "$(_t_or install_reuse_port 'Используется порт из ключа'): ${port}"
|
||||
else
|
||||
port=$(select_port)
|
||||
[ $? -ne 0 ] && return
|
||||
port=$(select_port)
|
||||
[ $? -ne 0 ] && return
|
||||
if [ "$port" = "443" ]; then
|
||||
warn_3xui_443_conflict || true
|
||||
fi
|
||||
|
||||
# Preflight: port conflict check (checks the external port only for lite)
|
||||
if ! preflight_check "lite" "$port"; then
|
||||
show_promo_with_qr 15
|
||||
return
|
||||
fi
|
||||
|
||||
# Secret: reuse if provided via manual_secret_input, otherwise generate new
|
||||
# Generate secret
|
||||
local secret
|
||||
if [ -n "${GOTELEGRAM_EXISTING_SECRET:-}" ]; then
|
||||
secret="$GOTELEGRAM_EXISTING_SECRET"
|
||||
log_info "$(_t_or install_reuse_secret 'Используется переданный ключ'): ${secret:0:8}...${secret: -4}"
|
||||
else
|
||||
secret=$(generate_hex 32)
|
||||
fi
|
||||
secret=$(generate_hex 32)
|
||||
|
||||
# Confirm
|
||||
local ip
|
||||
@@ -394,14 +520,9 @@ install_lite_mode() {
|
||||
# Start
|
||||
start_telemt || return
|
||||
|
||||
# Save GoTelegram config
|
||||
# Save goTelegram Pro config
|
||||
save_gotelegram_config "telemt" "lite" "$port" "$secret" "$domain" "" ""
|
||||
|
||||
# Auto-install stats collector so stats work from the start
|
||||
if type install_stats_collector &>/dev/null; then
|
||||
install_stats_collector 2>/dev/null
|
||||
fi
|
||||
|
||||
# Credits
|
||||
show_credits
|
||||
|
||||
@@ -414,22 +535,12 @@ install_lite_mode() {
|
||||
install_pro_mode() {
|
||||
log_step "$(t install_pro_step)"
|
||||
|
||||
# Preflight: pro mode needs 443, 80 and 8443 (internal nginx mask)
|
||||
if ! preflight_check "pro"; then
|
||||
show_promo_with_qr 15
|
||||
return
|
||||
fi
|
||||
warn_3xui_443_conflict || true
|
||||
|
||||
# Enter domain — if provided via existing-key flow, reuse it
|
||||
local user_domain=""
|
||||
if [ -n "${GOTELEGRAM_EXISTING_DOMAIN:-}" ]; then
|
||||
user_domain="$GOTELEGRAM_EXISTING_DOMAIN"
|
||||
log_info "$(_t_or install_reuse_domain 'Используется домен из ключа'): ${user_domain}"
|
||||
else
|
||||
echo ""
|
||||
echo -ne " ${WHITE}$(t install_enter_domain)${NC} "
|
||||
read -r user_domain
|
||||
fi
|
||||
# Enter domain
|
||||
echo ""
|
||||
echo -ne " ${WHITE}$(t install_enter_domain)${NC} "
|
||||
read -r user_domain
|
||||
|
||||
if [ -z "$user_domain" ] || ! validate_domain "$user_domain"; then
|
||||
log_error "$(tf install_bad_domain "${user_domain:-<empty>}")"
|
||||
@@ -471,14 +582,8 @@ install_pro_mode() {
|
||||
|
||||
# Generate fake-TLS secret (ee + secret + hex domain)
|
||||
# ee prefix tells Telegram client to masquerade traffic as TLS to domain
|
||||
# Reuse existing secret if manual_secret_input provided it
|
||||
local raw_secret
|
||||
if [ -n "${GOTELEGRAM_EXISTING_SECRET:-}" ]; then
|
||||
raw_secret="$GOTELEGRAM_EXISTING_SECRET"
|
||||
log_info "$(_t_or install_reuse_secret 'Используется переданный ключ'): ${raw_secret:0:8}...${raw_secret: -4}"
|
||||
else
|
||||
raw_secret=$(generate_hex 32)
|
||||
fi
|
||||
raw_secret=$(generate_hex 32)
|
||||
local domain_hex
|
||||
domain_hex=$(printf '%s' "$user_domain" | xxd -p | tr -d '\n')
|
||||
local faketls_secret="ee${raw_secret}${domain_hex}"
|
||||
@@ -517,11 +622,6 @@ install_pro_mode() {
|
||||
tpl_id=$(basename "$template_dir")
|
||||
save_gotelegram_config "telemt" "pro" "443" "$raw_secret" "$user_domain" "$user_domain" "$tpl_id"
|
||||
|
||||
# Auto-install stats collector so stats work from the start
|
||||
if type install_stats_collector &>/dev/null; then
|
||||
install_stats_collector 2>/dev/null
|
||||
fi
|
||||
|
||||
# Result — use domain and fake-TLS link
|
||||
show_proxy_info_pro "$user_domain" "$faketls_secret"
|
||||
echo -e " ${WHITE}$(t svc_site):${NC} ${GREEN}https://${user_domain}${NC}"
|
||||
@@ -631,6 +731,21 @@ menu_logs() {
|
||||
}
|
||||
|
||||
# ── Change mode / template ──────────────────────────────────────────────────
|
||||
update_current_template_id() {
|
||||
local template_dir="$1"
|
||||
local tpl_id
|
||||
tpl_id=$(basename "$template_dir")
|
||||
[ -z "$tpl_id" ] && return 0
|
||||
|
||||
if [ -f "$template_dir/.custom_git_source" ]; then
|
||||
local source_url
|
||||
source_url=$(head -1 "$template_dir/.custom_git_source" 2>/dev/null || echo "")
|
||||
bot_update_config_field "template_source" "$source_url" || true
|
||||
fi
|
||||
bot_update_config_field "template_id" "$tpl_id" || \
|
||||
log_warning "Не удалось обновить template_id в config.json"
|
||||
}
|
||||
|
||||
menu_change_mode() {
|
||||
local current_mode
|
||||
current_mode=$(config_get mode 2>/dev/null)
|
||||
@@ -653,6 +768,7 @@ menu_change_mode() {
|
||||
template_dir=$(interactive_template_selection)
|
||||
[ $? -ne 0 ] && return
|
||||
switch_template "$template_dir"
|
||||
update_current_template_id "$template_dir"
|
||||
;;
|
||||
2)
|
||||
log_warning "$(t change_requires_reinstall)"
|
||||
@@ -696,6 +812,7 @@ menu_website() {
|
||||
template_dir=$(interactive_template_selection)
|
||||
[ $? -ne 0 ] && return
|
||||
switch_template "$template_dir"
|
||||
update_current_template_id "$template_dir"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
@@ -753,6 +870,7 @@ menu_remove() {
|
||||
systemctl daemon-reload
|
||||
rm -rf "$BOT_DIR"
|
||||
fi
|
||||
remove_admin_web
|
||||
log_success "$(t remove_all_done)"
|
||||
;;
|
||||
esac
|
||||
@@ -762,6 +880,79 @@ menu_remove() {
|
||||
BOT_DIR="/opt/gotelegram-bot"
|
||||
BOT_SERVICE="gotelegram-bot"
|
||||
|
||||
admin_web_service_status() {
|
||||
if ! systemctl list-unit-files "$ADMIN_WEB_SERVICE.service" &>/dev/null 2>&1; then
|
||||
echo "not_installed"
|
||||
elif systemctl is-active "$ADMIN_WEB_SERVICE" &>/dev/null 2>&1; then
|
||||
echo "running"
|
||||
else
|
||||
echo "stopped"
|
||||
fi
|
||||
}
|
||||
|
||||
install_admin_web() {
|
||||
local src_dir="$SCRIPT_DIR/admin-web"
|
||||
[ -d "$src_dir" ] || { log_warning "admin-web files not found: $src_dir"; return 1; }
|
||||
command -v python3 &>/dev/null || { log_warning "python3 not found; web admin skipped"; return 1; }
|
||||
|
||||
mkdir -p "$ADMIN_WEB_DIR/static"
|
||||
cp "$src_dir/server.py" "$ADMIN_WEB_DIR/server.py"
|
||||
cp -a "$src_dir/static/." "$ADMIN_WEB_DIR/static/"
|
||||
chmod 700 "$ADMIN_WEB_DIR"
|
||||
chmod 755 "$ADMIN_WEB_DIR/server.py" "$ADMIN_WEB_DIR/static"
|
||||
rm -f "$ADMIN_WEB_DIR/token" 2>/dev/null || true
|
||||
|
||||
local python_bin
|
||||
python_bin=$(command -v python3)
|
||||
cat > "/etc/systemd/system/${ADMIN_WEB_SERVICE}.service" << SVCEOF
|
||||
[Unit]
|
||||
Description=goTelegram Pro v${GOTELEGRAM_VERSION} Local Web Admin
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
WorkingDirectory=$ADMIN_WEB_DIR
|
||||
ExecStart=$python_bin $ADMIN_WEB_DIR/server.py
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
Environment=GOTELEGRAM_ADMIN_HOST=$ADMIN_WEB_HOST
|
||||
Environment=GOTELEGRAM_ADMIN_PORT=$ADMIN_WEB_PORT
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
SVCEOF
|
||||
|
||||
systemctl daemon-reload
|
||||
systemctl enable "$ADMIN_WEB_SERVICE" &>/dev/null
|
||||
systemctl restart "$ADMIN_WEB_SERVICE" 2>/dev/null || systemctl start "$ADMIN_WEB_SERVICE"
|
||||
if type install_stats_collector &>/dev/null; then
|
||||
install_stats_collector >/dev/null 2>&1 || log_warning "stats collector was not started; open Web Admin traffic page and use Repair"
|
||||
fi
|
||||
log_success "Web admin installed: ${ADMIN_WEB_HOST}:${ADMIN_WEB_PORT}"
|
||||
}
|
||||
|
||||
auto_install_admin_web_if_possible() {
|
||||
[ -d "$SCRIPT_DIR/admin-web" ] || return 0
|
||||
command -v python3 &>/dev/null || return 0
|
||||
if [ "$(admin_web_service_status)" != "not_installed" ] && \
|
||||
[ -f "$ADMIN_WEB_DIR/server.py" ] && \
|
||||
cmp -s "$SCRIPT_DIR/admin-web/server.py" "$ADMIN_WEB_DIR/server.py" && \
|
||||
cmp -s "$SCRIPT_DIR/admin-web/static/index.html" "$ADMIN_WEB_DIR/static/index.html" && \
|
||||
cmp -s "$SCRIPT_DIR/admin-web/static/app.js" "$ADMIN_WEB_DIR/static/app.js" && \
|
||||
cmp -s "$SCRIPT_DIR/admin-web/static/styles.css" "$ADMIN_WEB_DIR/static/styles.css"; then
|
||||
return 0
|
||||
fi
|
||||
install_admin_web >/dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
remove_admin_web() {
|
||||
systemctl stop "$ADMIN_WEB_SERVICE" 2>/dev/null
|
||||
systemctl disable "$ADMIN_WEB_SERVICE" 2>/dev/null
|
||||
rm -f "/etc/systemd/system/${ADMIN_WEB_SERVICE}.service"
|
||||
systemctl daemon-reload 2>/dev/null
|
||||
rm -rf "$ADMIN_WEB_DIR"
|
||||
}
|
||||
|
||||
bot_service_status() {
|
||||
if ! systemctl list-unit-files "$BOT_SERVICE.service" &>/dev/null 2>&1; then
|
||||
echo "not_installed"
|
||||
@@ -772,6 +963,31 @@ bot_service_status() {
|
||||
fi
|
||||
}
|
||||
|
||||
auto_update_bot_if_possible() {
|
||||
[ -d "$SCRIPT_DIR/gotelegram-bot" ] || return 0
|
||||
[ "$(bot_service_status)" = "not_installed" ] && return 0
|
||||
[ -f "$BOT_DIR/.env" ] || return 0
|
||||
|
||||
local needs_update=0
|
||||
[ -f "$SCRIPT_DIR/gotelegram-bot/bot.py" ] && \
|
||||
! cmp -s "$SCRIPT_DIR/gotelegram-bot/bot.py" "$BOT_DIR/bot.py" && needs_update=1
|
||||
[ -f "$SCRIPT_DIR/gotelegram-bot/i18n.py" ] && \
|
||||
! cmp -s "$SCRIPT_DIR/gotelegram-bot/i18n.py" "$BOT_DIR/i18n.py" && needs_update=1
|
||||
[ -f "$SCRIPT_DIR/gotelegram-bot/requirements.txt" ] && \
|
||||
! cmp -s "$SCRIPT_DIR/gotelegram-bot/requirements.txt" "$BOT_DIR/requirements.txt" && needs_update=1
|
||||
|
||||
local lang_file lang_name
|
||||
for lang_file in "$SCRIPT_DIR"/gotelegram-bot/lang/*.json; do
|
||||
[ -e "$lang_file" ] || continue
|
||||
lang_name=$(basename "$lang_file")
|
||||
! cmp -s "$lang_file" "$BOT_DIR/lang/$lang_name" && needs_update=1
|
||||
done
|
||||
|
||||
[ "$needs_update" = "1" ] || return 0
|
||||
bot_install >/dev/null 2>&1 || \
|
||||
log_warning "Telegram bot auto-update failed; run menu 12 → Telegram-bot → Install/update"
|
||||
}
|
||||
|
||||
menu_bot() {
|
||||
local st
|
||||
st=$(bot_service_status)
|
||||
@@ -1001,7 +1217,7 @@ bot_install() {
|
||||
# Systemd
|
||||
cat > "/etc/systemd/system/${BOT_SERVICE}.service" << SVCEOF
|
||||
[Unit]
|
||||
Description=GoTelegram v${GOTELEGRAM_VERSION} Telegram Bot
|
||||
Description=goTelegram Pro v${GOTELEGRAM_VERSION} Telegram Bot
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
@@ -1020,6 +1236,8 @@ SVCEOF
|
||||
systemctl enable "$BOT_SERVICE" &>/dev/null
|
||||
systemctl restart "$BOT_SERVICE" 2>/dev/null || systemctl start "$BOT_SERVICE"
|
||||
|
||||
install_admin_web || log_warning "Web admin could not be installed"
|
||||
|
||||
# If auto mode — wait until bot captures first admin
|
||||
local has_ids
|
||||
has_ids=$(grep "^ALLOWED_IDS=" "$BOT_DIR/.env" 2>/dev/null | cut -d= -f2)
|
||||
@@ -1189,6 +1407,7 @@ bot_remove() {
|
||||
_promo_block() {
|
||||
# Print a promo section without width-fragile box borders (i18n safe)
|
||||
local line2; line2=$(printf '─%.0s' {1..54})
|
||||
local youtube_link="${GOTELEGRAM_YOUTUBE_LINK:-}"
|
||||
echo ""
|
||||
echo -e " ${DIM}${line2}${NC}"
|
||||
echo -e " ${BOLD}${YELLOW}$(t promo_host1_title)${NC}"
|
||||
@@ -1203,6 +1422,11 @@ _promo_block() {
|
||||
echo -e " ${DIM}${line2}${NC}"
|
||||
echo -e " ${BOLD}${YELLOW}$(t promo_tips_title)${NC}"
|
||||
echo -e " ${CYAN}https://pay.cloudtips.ru/p/7410814f${NC}"
|
||||
if [ -n "$youtube_link" ]; then
|
||||
echo -e " ${DIM}${line2}${NC}"
|
||||
echo -e " ${BOLD}${YELLOW}$(t promo_youtube_title)${NC}"
|
||||
echo -e " $(t promo_link_label) ${CYAN}${youtube_link}${NC}"
|
||||
fi
|
||||
echo -e " ${DIM}${line2}${NC}"
|
||||
echo ""
|
||||
}
|
||||
@@ -1232,27 +1456,30 @@ mark_promo_shown() {
|
||||
date +%s > "$GOTELEGRAM_DIR/.promo_last_shown"
|
||||
}
|
||||
|
||||
_promo_qr() {
|
||||
local label="$1" url="$2"
|
||||
[ -n "$url" ] || return 0
|
||||
echo -e " ${DIM}${label}${NC}"
|
||||
qrencode -t UTF8 -m 1 "$url" 2>/dev/null | while IFS= read -r qr_line; do
|
||||
echo " $qr_line"
|
||||
done
|
||||
}
|
||||
|
||||
# ── Promo with QR + delay (on install + once per day) ───────────────────
|
||||
# QR показываем ТОЛЬКО для чаевых/донатов. Для хостеров оставлены только
|
||||
# текстовые ссылки и промокоды (см. _promo_block) — QR-коды хостеров
|
||||
# визуально конкурировали с чаевыми и перегружали экран.
|
||||
show_promo_with_qr() {
|
||||
local countdown="${1:-5}"
|
||||
_promo_block
|
||||
|
||||
# QR только для чаевых
|
||||
if command -v qrencode &>/dev/null; then
|
||||
echo -e " ${DIM}$(t promo_qr_tips)${NC}"
|
||||
qrencode -t UTF8 -m 1 "https://pay.cloudtips.ru/p/7410814f" 2>/dev/null | while IFS= read -r qr_line; do
|
||||
echo " $qr_line"
|
||||
done
|
||||
_promo_qr "$(t promo_qr_host1)" "https://vk.cc/ct29NQ"
|
||||
_promo_qr "$(t promo_qr_host2)" "https://vk.cc/cUxAhj"
|
||||
_promo_qr "$(t promo_qr_tips)" "https://pay.cloudtips.ru/p/7410814f"
|
||||
_promo_qr "$(t promo_qr_youtube)" "${GOTELEGRAM_YOUTUBE_LINK:-}"
|
||||
fi
|
||||
|
||||
mark_promo_shown
|
||||
|
||||
# Countdown (default 5s, caller may pass longer for preflight abort)
|
||||
local i
|
||||
for ((i=countdown; i>0; i--)); do
|
||||
# 5-second countdown
|
||||
for i in 5 4 3 2 1; do
|
||||
echo -ne "\r ${DIM}$(tf promo_menu_in "$i")${NC} "
|
||||
sleep 1
|
||||
done
|
||||
@@ -1566,6 +1793,7 @@ main() {
|
||||
if ! check_deps_present; then
|
||||
ensure_deps >&2 || exit 1
|
||||
fi
|
||||
auto_migrate_legacy_state >&2 || true
|
||||
bot_action_dispatch "$@"
|
||||
exit $?
|
||||
fi
|
||||
@@ -1584,6 +1812,10 @@ main() {
|
||||
}
|
||||
fi
|
||||
|
||||
auto_migrate_legacy_state || true
|
||||
auto_update_bot_if_possible || true
|
||||
auto_install_admin_web_if_possible || true
|
||||
|
||||
# First-run language picker (before banner so banner appears in chosen lang)
|
||||
first_run_language_picker
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/bin/bash
|
||||
# GoTelegram v2.2.1 — Установка Telegram-бота
|
||||
# GoTelegram v2.5.0 — Установка Telegram-бота
|
||||
# Создаёт venv, ставит зависимости, настраивает systemd
|
||||
|
||||
set -e
|
||||
@@ -12,6 +12,9 @@ NC='\033[0m'
|
||||
BOT_DIR="/opt/gotelegram-bot"
|
||||
SERVICE_NAME="gotelegram-bot"
|
||||
GOTELEGRAM_DIR="/opt/gotelegram"
|
||||
ADMIN_WEB_DIR="/opt/gotelegram-admin"
|
||||
ADMIN_WEB_SERVICE="gotelegram-admin"
|
||||
ADMIN_WEB_PORT="1984"
|
||||
|
||||
if [ "$EUID" -ne 0 ]; then
|
||||
echo -e "${RED}Запустите с sudo.${NC}"
|
||||
@@ -19,7 +22,7 @@ if [ "$EUID" -ne 0 ]; then
|
||||
fi
|
||||
|
||||
echo -e "${CYAN}╔═══════════════════════════════════════════╗${NC}"
|
||||
echo -e "${CYAN}║${NC} ${GREEN}GoTelegram v2.2.1 — Установка бота${NC} ${CYAN}║${NC}"
|
||||
echo -e "${CYAN}║${NC} ${GREEN}GoTelegram v2.5.0 — Установка бота${NC} ${CYAN}║${NC}"
|
||||
echo -e "${CYAN}╚═══════════════════════════════════════════╝${NC}"
|
||||
echo ""
|
||||
|
||||
@@ -38,6 +41,8 @@ fi
|
||||
# ── Каталог бота ─────────────────────────────────────────────────────────────
|
||||
mkdir -p "$BOT_DIR"
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
[ -f "$SCRIPT_DIR/lib/common.sh" ] && source "$SCRIPT_DIR/lib/common.sh" || true
|
||||
[ -f "$SCRIPT_DIR/lib/stats.sh" ] && source "$SCRIPT_DIR/lib/stats.sh" || true
|
||||
|
||||
if [ -f "$SCRIPT_DIR/gotelegram-bot/bot.py" ]; then
|
||||
echo -e "${GREEN}[*] Копирование файлов бота...${NC}"
|
||||
@@ -93,7 +98,7 @@ fi
|
||||
# ── Systemd ──────────────────────────────────────────────────────────────────
|
||||
cat > "/etc/systemd/system/${SERVICE_NAME}.service" << EOF
|
||||
[Unit]
|
||||
Description=GoTelegram v2.2.1 Telegram Bot
|
||||
Description=GoTelegram v2.5.0 Telegram Bot
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
@@ -112,6 +117,42 @@ systemctl daemon-reload
|
||||
systemctl enable "$SERVICE_NAME"
|
||||
systemctl restart "$SERVICE_NAME" 2>/dev/null || systemctl start "$SERVICE_NAME"
|
||||
|
||||
# ── Local Web Admin ──────────────────────────────────────────────────────────
|
||||
if [ -f "$SCRIPT_DIR/admin-web/server.py" ]; then
|
||||
echo -e "${GREEN}[*] Установка локальной web-админки...${NC}"
|
||||
mkdir -p "$ADMIN_WEB_DIR/static"
|
||||
cp "$SCRIPT_DIR/admin-web/server.py" "$ADMIN_WEB_DIR/server.py"
|
||||
cp -a "$SCRIPT_DIR/admin-web/static/." "$ADMIN_WEB_DIR/static/"
|
||||
chmod 700 "$ADMIN_WEB_DIR"
|
||||
chmod 755 "$ADMIN_WEB_DIR/server.py" "$ADMIN_WEB_DIR/static"
|
||||
rm -f "$ADMIN_WEB_DIR/token" 2>/dev/null || true
|
||||
|
||||
PYTHON_BIN=$(command -v python3)
|
||||
cat > "/etc/systemd/system/${ADMIN_WEB_SERVICE}.service" << EOF
|
||||
[Unit]
|
||||
Description=GoTelegram v2.5.0 Local Web Admin
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
WorkingDirectory=$ADMIN_WEB_DIR
|
||||
ExecStart=$PYTHON_BIN $ADMIN_WEB_DIR/server.py
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
Environment=GOTELEGRAM_ADMIN_HOST=127.0.0.1
|
||||
Environment=GOTELEGRAM_ADMIN_PORT=$ADMIN_WEB_PORT
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
systemctl daemon-reload
|
||||
systemctl enable "$ADMIN_WEB_SERVICE"
|
||||
systemctl restart "$ADMIN_WEB_SERVICE" 2>/dev/null || systemctl start "$ADMIN_WEB_SERVICE"
|
||||
if type install_stats_collector &>/dev/null; then
|
||||
install_stats_collector >/dev/null 2>&1 || echo -e "${YELLOW}[!] Сборщик статистики не запущен; откройте Traffic в Web Admin и нажмите Repair.${NC}"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo -e "${GREEN}╔═══════════════════════════════════════════╗${NC}"
|
||||
echo -e "${GREEN}║ ✅ Бот установлен и запущен! ║${NC}"
|
||||
|
||||
715
lib/backup.sh
Normal file → Executable file
715
lib/backup.sh
Normal file → Executable file
@@ -1,179 +1,129 @@
|
||||
#!/bin/bash
|
||||
# GoTelegram v2.4.9 — Unified Backup Format (UBF) v2.0
|
||||
#
|
||||
# UBF v2.0 layout (inside the tarball):
|
||||
# gotelegram_backup_YYYYMMDD_HHMMSS_<shortid>/
|
||||
# ├── metadata.json # backup_id, versions, fingerprint, ...
|
||||
# ├── secrets.json # raw_secret, faketls_secret, proxy_link, bot_token
|
||||
# ├── telemt/config.toml
|
||||
# ├── gotelegram/config.json
|
||||
# ├── gotelegram/.language
|
||||
# ├── nginx/site.conf
|
||||
# ├── letsencrypt/
|
||||
# │ ├── live/<domain>/{fullchain,privkey,chain,cert}.pem
|
||||
# │ └── renewal/<domain>.conf
|
||||
# ├── site/ (nginx document root)
|
||||
# └── bot/.env
|
||||
#
|
||||
# Backup ID format: GT-YYMMDD-<last6hex-of-raw-secret>
|
||||
# Archive name: gotelegram_backup_YYYYMMDD_HHMMSS_<shortid>.tar.gz[.enc]
|
||||
#
|
||||
# Encryption: AES-256-CBC + PBKDF2 (optional, password-based)
|
||||
# Integrity: SHA-256 sidecar file (<archive>.sha256)
|
||||
#
|
||||
# Restore path automatically detects v1.1 (legacy) vs v2.0 layouts by reading
|
||||
# metadata.json.backup_version. When restoring a v1.1 archive the script
|
||||
# immediately writes a fresh v2.0 backup alongside the old one, so subsequent
|
||||
# reinstalls can benefit from the new format.
|
||||
# goTelegram Pro v2.5.0 — backup and restore (i18n-aware)
|
||||
|
||||
# ── Utility: generate a backup ID from a raw secret ─────────────────────────
|
||||
# Format: GT-YYMMDD-<last6hex>. Deterministic per-day per-key; easy to read.
|
||||
generate_backup_id() {
|
||||
local raw_secret="$1"
|
||||
local date_part
|
||||
date_part=$(date +%y%m%d)
|
||||
local last6="000000"
|
||||
if [ -n "$raw_secret" ] && [ ${#raw_secret} -ge 6 ]; then
|
||||
last6="${raw_secret: -6}"
|
||||
last6=$(echo "$last6" | tr 'A-F' 'a-f')
|
||||
fi
|
||||
echo "GT-${date_part}-${last6}"
|
||||
}
|
||||
|
||||
# ── Utility: SHA-256 fingerprint of a raw secret ────────────────────────────
|
||||
secret_fingerprint() {
|
||||
local raw_secret="$1"
|
||||
[ -z "$raw_secret" ] && { echo ""; return; }
|
||||
printf '%s' "$raw_secret" | openssl dgst -sha256 2>/dev/null | awk '{print $NF}'
|
||||
}
|
||||
|
||||
# ── Utility: hex-encode an ASCII string (for fake-TLS secret) ───────────────
|
||||
_hex_encode() {
|
||||
printf '%s' "$1" | xxd -p | tr -d '\n'
|
||||
}
|
||||
|
||||
# ── Создание бекапа (UBF v2.0) ──────────────────────────────────────────────
|
||||
# ── Создание бекапа ──────────────────────────────────────────────────────────
|
||||
create_backup() {
|
||||
local password="${1:-}"
|
||||
local password="$1"
|
||||
local output_dir="${2:-$BACKUP_DIR}"
|
||||
|
||||
# Pull current config (so backup_id can include the real secret)
|
||||
local raw_secret domain mode engine port lang tpl_id mask_host
|
||||
raw_secret=$(config_get secret 2>/dev/null || echo "")
|
||||
domain=$(config_get domain 2>/dev/null || echo "")
|
||||
mode=$(config_get mode 2>/dev/null || echo "unknown")
|
||||
engine=$(config_get engine 2>/dev/null || echo "telemt")
|
||||
port=$(config_get port 2>/dev/null || echo "443")
|
||||
lang=$(type get_language &>/dev/null && get_language 2>/dev/null || echo "en")
|
||||
tpl_id=$(config_get template_id 2>/dev/null || echo "")
|
||||
mask_host=$(config_get mask_host 2>/dev/null || echo "")
|
||||
|
||||
# Sanitise port
|
||||
[[ "$port" =~ ^[0-9]+$ ]] || port=443
|
||||
|
||||
# Backup id / short id (last 6 of secret, or random if unknown)
|
||||
local backup_id short_id
|
||||
backup_id=$(generate_backup_id "$raw_secret")
|
||||
short_id="${backup_id##*-}"
|
||||
|
||||
local timestamp
|
||||
timestamp=$(date +%Y%m%d_%H%M%S)
|
||||
local backup_name="gotelegram_backup_${timestamp}_${short_id}"
|
||||
local tmp_dir="/tmp/${backup_name}"
|
||||
local backup_name tmp_dir suffix=0
|
||||
|
||||
mkdir -p "$tmp_dir" "$output_dir"
|
||||
mkdir -p "$output_dir"
|
||||
while true; do
|
||||
if [ "$suffix" -eq 0 ]; then
|
||||
backup_name="gotelegram_backup_${timestamp}"
|
||||
else
|
||||
backup_name="gotelegram_backup_${timestamp}_${suffix}"
|
||||
fi
|
||||
tmp_dir="/tmp/${backup_name}"
|
||||
if [ ! -e "$tmp_dir" ] && \
|
||||
[ ! -e "${output_dir}/${backup_name}.tar.gz" ] && \
|
||||
[ ! -e "${output_dir}/${backup_name}.tar.gz.enc" ]; then
|
||||
break
|
||||
fi
|
||||
suffix=$((suffix + 1))
|
||||
done
|
||||
mkdir -p "$tmp_dir"
|
||||
|
||||
# Собираем файлы
|
||||
log_info "$(_t_or backup_collecting 'Собираю конфигурацию...')"
|
||||
|
||||
# ── telemt ──
|
||||
# telemt конфиг
|
||||
if [ -f "$TELEMT_CONFIG" ]; then
|
||||
mkdir -p "$tmp_dir/telemt"
|
||||
cp "$TELEMT_CONFIG" "$tmp_dir/telemt/config.toml"
|
||||
cp "$TELEMT_CONFIG" "$tmp_dir/config.toml"
|
||||
fi
|
||||
|
||||
# ── gotelegram ──
|
||||
mkdir -p "$tmp_dir/gotelegram"
|
||||
# goTelegram Pro конфиг
|
||||
if [ -f "$GOTELEGRAM_CONFIG" ]; then
|
||||
cp "$GOTELEGRAM_CONFIG" "$tmp_dir/gotelegram/config.json"
|
||||
cp "$GOTELEGRAM_CONFIG" "$tmp_dir/gotelegram.json"
|
||||
fi
|
||||
if [ -f "$GOTELEGRAM_DIR/disabled_users.json" ]; then
|
||||
cp "$GOTELEGRAM_DIR/disabled_users.json" "$tmp_dir/disabled_users.json" 2>/dev/null
|
||||
fi
|
||||
if [ -f "$GOTELEGRAM_DIR/backup_schedule.json" ]; then
|
||||
cp "$GOTELEGRAM_DIR/backup_schedule.json" "$tmp_dir/backup_schedule.json" 2>/dev/null
|
||||
fi
|
||||
|
||||
# Language marker (i18n)
|
||||
if [ -f "$GOTELEGRAM_DIR/.language" ]; then
|
||||
cp "$GOTELEGRAM_DIR/.language" "$tmp_dir/gotelegram/.language"
|
||||
cp "$GOTELEGRAM_DIR/.language" "$tmp_dir/.language"
|
||||
fi
|
||||
|
||||
# ── nginx ──
|
||||
# nginx конфиг (stealth mode)
|
||||
if [ -f "$NGINX_SITE_CONF" ]; then
|
||||
mkdir -p "$tmp_dir/nginx"
|
||||
cp "$NGINX_SITE_CONF" "$tmp_dir/nginx/site.conf"
|
||||
cp "$NGINX_SITE_CONF" "$tmp_dir/nginx.conf"
|
||||
fi
|
||||
|
||||
# ── Let's Encrypt (full tree: live/<d>/*.pem + renewal/<d>.conf) ──
|
||||
# SSL сертификаты и renewal metadata для переносов между VPS
|
||||
local domain
|
||||
domain=$(config_get domain 2>/dev/null)
|
||||
if [ -n "$domain" ] && [ -d "/etc/letsencrypt/live/$domain" ]; then
|
||||
mkdir -p "$tmp_dir/letsencrypt/live/$domain"
|
||||
# Follow symlinks — letsencrypt's live/ tree is symlinks into archive/
|
||||
cp -L "/etc/letsencrypt/live/$domain/"*.pem "$tmp_dir/letsencrypt/live/$domain/" 2>/dev/null
|
||||
if [ -f "/etc/letsencrypt/renewal/${domain}.conf" ]; then
|
||||
mkdir -p "$tmp_dir/letsencrypt/renewal"
|
||||
cp "/etc/letsencrypt/renewal/${domain}.conf" "$tmp_dir/letsencrypt/renewal/"
|
||||
fi
|
||||
log_dim "$(_t_or backup_ssl_included 'SSL-сертификаты включены (+ chain + renewal)')"
|
||||
mkdir -p "$tmp_dir/letsencrypt/live" "$tmp_dir/letsencrypt/archive" "$tmp_dir/letsencrypt/renewal"
|
||||
cp -a "/etc/letsencrypt/live/$domain" "$tmp_dir/letsencrypt/live/" 2>/dev/null
|
||||
[ -d "/etc/letsencrypt/archive/$domain" ] && \
|
||||
cp -a "/etc/letsencrypt/archive/$domain" "$tmp_dir/letsencrypt/archive/" 2>/dev/null
|
||||
[ -f "/etc/letsencrypt/renewal/$domain.conf" ] && \
|
||||
cp -a "/etc/letsencrypt/renewal/$domain.conf" "$tmp_dir/letsencrypt/renewal/" 2>/dev/null
|
||||
log_dim "SSL сертификаты включены"
|
||||
fi
|
||||
|
||||
# ── Website template ──
|
||||
# Шаблон сайта (если есть)
|
||||
if [ -d "$WEBSITE_ROOT" ] && [ -f "$WEBSITE_ROOT/index.html" ]; then
|
||||
mkdir -p "$tmp_dir/site"
|
||||
cp -r "$WEBSITE_ROOT"/* "$tmp_dir/site/" 2>/dev/null
|
||||
cp -a "$WEBSITE_ROOT/." "$tmp_dir/site/"
|
||||
log_dim "$(_t_or backup_site_included 'Шаблон сайта включён')"
|
||||
fi
|
||||
|
||||
# ── Telegram bot ──
|
||||
if [ -f "$BOT_DIR/.env" ]; then
|
||||
# Custom templates and catalog
|
||||
if [ -d "$GOTELEGRAM_DIR/custom_templates" ]; then
|
||||
mkdir -p "$tmp_dir/custom_templates"
|
||||
cp -a "$GOTELEGRAM_DIR/custom_templates/." "$tmp_dir/custom_templates/" 2>/dev/null
|
||||
fi
|
||||
if [ -f "$GOTELEGRAM_DIR/templates_catalog.json" ]; then
|
||||
cp "$GOTELEGRAM_DIR/templates_catalog.json" "$tmp_dir/templates_catalog.json" 2>/dev/null
|
||||
fi
|
||||
|
||||
# Bot state (.env has BotFather token, so encrypted backups are strongly recommended)
|
||||
if [ -d "$BOT_DIR" ]; then
|
||||
mkdir -p "$tmp_dir/bot"
|
||||
cp "$BOT_DIR/.env" "$tmp_dir/bot/.env"
|
||||
chmod 600 "$tmp_dir/bot/.env" 2>/dev/null
|
||||
log_dim "$(_t_or backup_bot_included 'Конфиг Telegram-бота включён')"
|
||||
[ -f "$BOT_DIR/.env" ] && cp "$BOT_DIR/.env" "$tmp_dir/bot/.env" 2>/dev/null
|
||||
[ -f "$BOT_DIR/i18n.py" ] && cp "$BOT_DIR/i18n.py" "$tmp_dir/bot/i18n.py" 2>/dev/null
|
||||
[ -d "$BOT_DIR/lang" ] && cp -a "$BOT_DIR/lang" "$tmp_dir/bot/" 2>/dev/null
|
||||
fi
|
||||
|
||||
# ── secrets.json ──
|
||||
local faketls_secret="" proxy_link="" bot_token=""
|
||||
if [ -n "$raw_secret" ] && [ "$mode" = "pro" ] && [ -n "$domain" ]; then
|
||||
faketls_secret="ee${raw_secret}$(_hex_encode "$domain")"
|
||||
fi
|
||||
if type generate_proxy_link &>/dev/null; then
|
||||
if [ "$mode" = "pro" ] && [ -n "$domain" ]; then
|
||||
proxy_link=$(generate_proxy_link "$domain" "$port" "$raw_secret" "$domain" 2>/dev/null || echo "")
|
||||
elif [ -n "$raw_secret" ]; then
|
||||
local ip
|
||||
ip=$(get_server_ip)
|
||||
proxy_link=$(generate_proxy_link "$ip" "$port" "$raw_secret" "$mask_host" 2>/dev/null || echo "")
|
||||
fi
|
||||
fi
|
||||
if [ -f "$BOT_DIR/.env" ]; then
|
||||
bot_token=$(grep -E '^BOT_TOKEN=' "$BOT_DIR/.env" 2>/dev/null | head -1 | cut -d= -f2-)
|
||||
bot_token="${bot_token%\"}"
|
||||
bot_token="${bot_token#\"}"
|
||||
# Local web admin state
|
||||
if [ -d "$ADMIN_WEB_DIR" ]; then
|
||||
mkdir -p "$tmp_dir/admin_web"
|
||||
[ -f "$ADMIN_WEB_DIR/server.py" ] && cp "$ADMIN_WEB_DIR/server.py" "$tmp_dir/admin_web/server.py" 2>/dev/null
|
||||
[ -d "$ADMIN_WEB_DIR/static" ] && cp -a "$ADMIN_WEB_DIR/static" "$tmp_dir/admin_web/" 2>/dev/null
|
||||
fi
|
||||
|
||||
cat > "$tmp_dir/secrets.json" << EOSEC
|
||||
{
|
||||
"version": "1",
|
||||
"raw_secret": "${raw_secret}",
|
||||
"faketls_secret": "${faketls_secret}",
|
||||
"proxy_link": "${proxy_link}",
|
||||
"bot_token": "${bot_token}",
|
||||
"exported_at": "$(date -Iseconds)"
|
||||
}
|
||||
EOSEC
|
||||
chmod 600 "$tmp_dir/secrets.json"
|
||||
# Traffic history
|
||||
if [ -f "$GOTELEGRAM_DIR/stats_history.csv" ]; then
|
||||
cp "$GOTELEGRAM_DIR/stats_history.csv" "$tmp_dir/stats_history.csv" 2>/dev/null
|
||||
fi
|
||||
if [ -f "$GOTELEGRAM_DIR/user_stats_history.csv" ]; then
|
||||
cp "$GOTELEGRAM_DIR/user_stats_history.csv" "$tmp_dir/user_stats_history.csv" 2>/dev/null
|
||||
fi
|
||||
if [ -f "$GOTELEGRAM_DIR/shared-443.json" ]; then
|
||||
cp "$GOTELEGRAM_DIR/shared-443.json" "$tmp_dir/shared-443.json" 2>/dev/null
|
||||
fi
|
||||
|
||||
# ── metadata.json v2.0 ──
|
||||
local ip fingerprint
|
||||
# Метаданные
|
||||
local ip mode engine lang port domain
|
||||
ip=$(get_server_ip)
|
||||
fingerprint=$(secret_fingerprint "$raw_secret")
|
||||
mode=$(config_get mode 2>/dev/null || echo "unknown")
|
||||
engine=$(config_get engine 2>/dev/null || echo "telemt")
|
||||
lang=$(type get_language &>/dev/null && get_language 2>/dev/null || echo "en")
|
||||
port=$(config_get port 2>/dev/null || echo "443")
|
||||
# Ensure port is numeric; fall back to 443 if garbage
|
||||
[[ "$port" =~ ^[0-9]+$ ]] || port=443
|
||||
domain=$(config_get domain 2>/dev/null || echo "")
|
||||
|
||||
cat > "$tmp_dir/metadata.json" << EOMETA
|
||||
{
|
||||
"backup_version": "2.0",
|
||||
"backup_id": "${backup_id}",
|
||||
"backup_version": "1.6",
|
||||
"gotelegram_version": "$GOTELEGRAM_VERSION",
|
||||
"created_at": "$(date -Iseconds)",
|
||||
"hostname": "$(hostname)",
|
||||
@@ -182,37 +132,34 @@ EOSEC
|
||||
"mode": "$mode",
|
||||
"language": "$lang",
|
||||
"port": $port,
|
||||
"domain": "$domain",
|
||||
"template_id": "$tpl_id",
|
||||
"mask_host": "$mask_host",
|
||||
"secret_fingerprint_sha256": "$fingerprint",
|
||||
"has_secrets": true,
|
||||
"has_letsencrypt": $([ -d "$tmp_dir/letsencrypt" ] && echo true || echo false),
|
||||
"has_site": $([ -d "$tmp_dir/site" ] && echo true || echo false),
|
||||
"has_bot": $([ -d "$tmp_dir/bot" ] && echo true || echo false)
|
||||
"domain": "$domain"
|
||||
}
|
||||
EOMETA
|
||||
|
||||
# ── Archive ──
|
||||
# Архивируем
|
||||
local tar_file="/tmp/${backup_name}.tar.gz"
|
||||
if ! tar czf "$tar_file" -C /tmp "$backup_name" 2>/dev/null; then
|
||||
log_error "$(_t_or backup_archive_err 'Ошибка создания архива')"
|
||||
rm -rf "$tmp_dir"; rm -f "$tar_file"
|
||||
rm -rf "$tmp_dir"
|
||||
rm -f "$tar_file"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [ ! -f "$tar_file" ]; then
|
||||
log_error "$(_t_or backup_archive_missing 'Архив не создан')"
|
||||
rm -rf "$tmp_dir"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# ── Encrypt (optional) ──
|
||||
# Шифруем если задан пароль
|
||||
local final_file=""
|
||||
if [ -n "$password" ]; then
|
||||
final_file="${output_dir}/${backup_name}.tar.gz.enc"
|
||||
if ! openssl enc -aes-256-cbc -salt -pbkdf2 -in "$tar_file" -out "$final_file" -pass "pass:${password}" 2>/dev/null; then
|
||||
openssl enc -aes-256-cbc -salt -pbkdf2 -in "$tar_file" -out "$final_file" -pass "pass:${password}" 2>/dev/null
|
||||
if [ $? -ne 0 ]; then
|
||||
log_error "$(_t_or backup_encrypt_err 'Ошибка шифрования')"
|
||||
rm -f "$tar_file"; rm -rf "$tmp_dir"
|
||||
rm -f "$tar_file"
|
||||
rm -rf "$tmp_dir"
|
||||
return 1
|
||||
fi
|
||||
rm -f "$tar_file"
|
||||
@@ -222,36 +169,28 @@ EOMETA
|
||||
mv "$tar_file" "$final_file"
|
||||
fi
|
||||
|
||||
# SHA-256 sidecar
|
||||
# SHA256 подпись
|
||||
sha256sum "$final_file" > "${final_file}.sha256" 2>/dev/null
|
||||
|
||||
# Cleanup
|
||||
# Очистка
|
||||
rm -rf "$tmp_dir"
|
||||
|
||||
local size
|
||||
size=$(du -h "$final_file" | cut -f1)
|
||||
|
||||
echo "" >&2
|
||||
echo -e " ${BOLD}${GREEN}✓ $(_t_or backup_created 'Бекап создан')${NC}" >&2
|
||||
echo -e " ${DIM}$(printf '─%.0s' {1..60})${NC}" >&2
|
||||
echo -e " ${WHITE}$(_t_or backup_id_label 'Backup ID'):${NC} ${BOLD}${CYAN}${backup_id}${NC}" >&2
|
||||
echo -e " ${WHITE}$(_t_or backup_file_label 'Файл'):${NC} ${final_file}" >&2
|
||||
echo -e " ${WHITE}$(_t_or backup_size_label 'Размер'):${NC} ${size}" >&2
|
||||
if [ -n "$raw_secret" ]; then
|
||||
echo -e " ${WHITE}$(_t_or backup_key_label 'Ключ в бекапе (fingerprint)'):${NC} ${DIM}${fingerprint:0:32}...${NC}" >&2
|
||||
if type tf &>/dev/null; then
|
||||
log_success "$(tf backup_created_fmt "$final_file" "$size")"
|
||||
else
|
||||
log_success "Бекап создан: $final_file ($size)"
|
||||
fi
|
||||
echo -e " ${DIM}$(printf '─%.0s' {1..60})${NC}" >&2
|
||||
echo "" >&2
|
||||
|
||||
# Only the final file path on stdout (callers capture it)
|
||||
echo "$final_file"
|
||||
return 0
|
||||
}
|
||||
|
||||
# ── Восстановление из бекапа (auto-detect v1.1 vs v2.0) ─────────────────────
|
||||
# ── Восстановление из бекапа ────────────────────────────────────────────────
|
||||
restore_backup() {
|
||||
local backup_file="$1"
|
||||
local password="${2:-}"
|
||||
local password="$2"
|
||||
local assume_yes="$3"
|
||||
|
||||
if [ ! -f "$backup_file" ]; then
|
||||
if type tf &>/dev/null; then
|
||||
@@ -265,16 +204,17 @@ restore_backup() {
|
||||
local tmp_dir="/tmp/gotelegram_restore_$$"
|
||||
mkdir -p "$tmp_dir"
|
||||
|
||||
# ── Decrypt if needed ──
|
||||
# Расшифровываем если нужно
|
||||
local tar_file=""
|
||||
if echo "$backup_file" | grep -q '\.enc$'; then
|
||||
if [ -z "$password" ]; then
|
||||
echo -ne " $(_t_or backup_enter_pass 'Введите пароль от бекапа'): " >&2
|
||||
echo -ne " $(_t_or backup_enter_pass 'Введите пароль от бекапа'): "
|
||||
read -rs password
|
||||
echo "" >&2
|
||||
echo ""
|
||||
fi
|
||||
tar_file="/tmp/gotelegram_restore_$$.tar.gz"
|
||||
if ! openssl enc -aes-256-cbc -d -pbkdf2 -in "$backup_file" -out "$tar_file" -pass "pass:${password}" 2>/dev/null; then
|
||||
openssl enc -aes-256-cbc -d -pbkdf2 -in "$backup_file" -out "$tar_file" -pass "pass:${password}" 2>/dev/null
|
||||
if [ $? -ne 0 ]; then
|
||||
log_error "$(_t_or backup_bad_pass 'Неверный пароль или повреждённый файл')"
|
||||
rm -rf "$tmp_dir" "$tar_file"
|
||||
return 1
|
||||
@@ -283,155 +223,184 @@ restore_backup() {
|
||||
tar_file="$backup_file"
|
||||
fi
|
||||
|
||||
# ── Extract ──
|
||||
if ! tar xzf "$tar_file" -C "$tmp_dir" 2>/dev/null; then
|
||||
# Распаковываем
|
||||
tar xzf "$tar_file" -C "$tmp_dir" 2>/dev/null
|
||||
if [ $? -ne 0 ]; then
|
||||
log_error "$(_t_or backup_extract_err 'Ошибка распаковки архива')"
|
||||
rm -rf "$tmp_dir"
|
||||
[ "$tar_file" != "$backup_file" ] && rm -f "$tar_file"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Find the single top-level dir inside the archive
|
||||
# Находим папку бекапа
|
||||
local backup_dir
|
||||
backup_dir=$(find "$tmp_dir" -maxdepth 1 -mindepth 1 -type d -name "gotelegram_backup_*" | head -1)
|
||||
backup_dir=$(find "$tmp_dir" -maxdepth 1 -type d -name "gotelegram_backup_*" | head -1)
|
||||
[ -z "$backup_dir" ] && backup_dir="$tmp_dir"
|
||||
|
||||
# ── Parse metadata.json ──
|
||||
local bk_version="1.1" bk_id="" bk_mode="" bk_domain="" bk_ip="" bk_lang="" bk_date=""
|
||||
# Legacy bot backups before v2.5.0 stored absolute paths directly in tar:
|
||||
# opt/gotelegram/config.json and etc/telemt/config.toml.
|
||||
if [ ! -f "$backup_dir/config.toml" ] && [ -f "$tmp_dir/etc/telemt/config.toml" ]; then
|
||||
cp "$tmp_dir/etc/telemt/config.toml" "$backup_dir/config.toml" 2>/dev/null || true
|
||||
fi
|
||||
if [ ! -f "$backup_dir/gotelegram.json" ] && [ -f "$tmp_dir/opt/gotelegram/config.json" ]; then
|
||||
cp "$tmp_dir/opt/gotelegram/config.json" "$backup_dir/gotelegram.json" 2>/dev/null || true
|
||||
fi
|
||||
if [ ! -f "$backup_dir/disabled_users.json" ] && [ -f "$tmp_dir/opt/gotelegram/disabled_users.json" ]; then
|
||||
cp "$tmp_dir/opt/gotelegram/disabled_users.json" "$backup_dir/disabled_users.json" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# Проверяем метаданные
|
||||
if [ -f "$backup_dir/metadata.json" ]; then
|
||||
bk_version=$(jq -r '.backup_version // "1.1"' "$backup_dir/metadata.json")
|
||||
bk_id=$(jq -r '.backup_id // empty' "$backup_dir/metadata.json")
|
||||
local bk_version bk_mode bk_ip bk_lang bk_date
|
||||
bk_version=$(jq -r '.gotelegram_version // "unknown"' "$backup_dir/metadata.json")
|
||||
bk_mode=$(jq -r '.mode // "unknown"' "$backup_dir/metadata.json")
|
||||
bk_domain=$(jq -r '.domain // empty' "$backup_dir/metadata.json")
|
||||
bk_ip=$(jq -r '.ip // "-"' "$backup_dir/metadata.json")
|
||||
bk_ip=$(jq -r '.ip // "unknown"' "$backup_dir/metadata.json")
|
||||
bk_lang=$(jq -r '.language // "-"' "$backup_dir/metadata.json")
|
||||
bk_date=$(jq -r '.created_at // "-"' "$backup_dir/metadata.json")
|
||||
echo ""
|
||||
echo -e " ${BOLD}${WHITE}📦 $(_t_or backup_label 'Бекап'):${NC}"
|
||||
echo -e " $(_t_or backup_version_label 'Версия'): $bk_version | $(_t_or backup_mode_label 'Режим'): $bk_mode | IP: $bk_ip | $(_t_or backup_lang_label 'Язык'): $bk_lang"
|
||||
echo -e " $(_t_or backup_date_label 'Дата'): $bk_date"
|
||||
echo ""
|
||||
fi
|
||||
|
||||
echo "" >&2
|
||||
echo -e " ${BOLD}${WHITE}📦 $(_t_or backup_label 'Бекап'):${NC}" >&2
|
||||
echo -e " ${DIM}$(printf '─%.0s' {1..60})${NC}" >&2
|
||||
if [ -n "$bk_id" ]; then
|
||||
echo -e " ${WHITE}$(_t_or backup_id_label 'Backup ID'):${NC} ${BOLD}${CYAN}${bk_id}${NC}" >&2
|
||||
fi
|
||||
echo -e " ${WHITE}$(_t_or backup_format_label 'Формат'):${NC} UBF ${bk_version}" >&2
|
||||
echo -e " ${WHITE}$(_t_or backup_mode_label 'Режим'):${NC} ${bk_mode}${bk_domain:+ | $bk_domain}" >&2
|
||||
echo -e " ${WHITE}$(_t_or backup_lang_label 'Язык'):${NC} ${bk_lang} | IP: ${bk_ip}" >&2
|
||||
echo -e " ${WHITE}$(_t_or backup_date_label 'Дата'):${NC} ${bk_date}" >&2
|
||||
echo -e " ${DIM}$(printf '─%.0s' {1..60})${NC}" >&2
|
||||
echo "" >&2
|
||||
|
||||
if ! confirm "$(_t_or backup_confirm_restore 'Восстановить конфигурацию? Текущие настройки будут перезаписаны.')"; then
|
||||
if [ "$assume_yes" != "yes" ] && ! confirm "$(_t_or backup_confirm_restore 'Восстановить конфигурацию? Текущие настройки будут перезаписаны.')"; then
|
||||
rm -rf "$tmp_dir"
|
||||
[ "$tar_file" != "$backup_file" ] && rm -f "$tar_file"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Останавливаем сервисы
|
||||
stop_telemt 2>/dev/null
|
||||
systemctl stop nginx 2>/dev/null
|
||||
|
||||
# ── Detect layout ──
|
||||
# v2.0 paths: telemt/config.toml, gotelegram/config.json, nginx/site.conf, letsencrypt/live/<d>/
|
||||
# v1.1 paths: config.toml, gotelegram.json, nginx.conf, certs/
|
||||
local src_telemt src_gt src_lang src_nginx src_le_live src_le_renewal src_site src_bot
|
||||
if [ "$bk_version" = "2.0" ] || [ -d "$backup_dir/telemt" ]; then
|
||||
src_telemt="$backup_dir/telemt/config.toml"
|
||||
src_gt="$backup_dir/gotelegram/config.json"
|
||||
src_lang="$backup_dir/gotelegram/.language"
|
||||
src_nginx="$backup_dir/nginx/site.conf"
|
||||
src_le_live=$(find "$backup_dir/letsencrypt/live" -maxdepth 1 -mindepth 1 -type d 2>/dev/null | head -1)
|
||||
src_le_renewal="$backup_dir/letsencrypt/renewal"
|
||||
src_site="$backup_dir/site"
|
||||
src_bot="$backup_dir/bot/.env"
|
||||
else
|
||||
src_telemt="$backup_dir/config.toml"
|
||||
src_gt="$backup_dir/gotelegram.json"
|
||||
src_lang="$backup_dir/.language"
|
||||
src_nginx="$backup_dir/nginx.conf"
|
||||
src_le_live="$backup_dir/certs" # v1.1 dumps certs flat
|
||||
src_le_renewal=""
|
||||
src_site="$backup_dir/site"
|
||||
src_bot="" # v1.1 never backed up bot
|
||||
fi
|
||||
|
||||
# ── telemt config ──
|
||||
if [ -f "$src_telemt" ]; then
|
||||
# Восстанавливаем telemt конфиг
|
||||
if [ -f "$backup_dir/config.toml" ]; then
|
||||
mkdir -p /etc/telemt
|
||||
cp "$src_telemt" "$TELEMT_CONFIG"
|
||||
cp "$backup_dir/config.toml" "$TELEMT_CONFIG"
|
||||
chmod 600 "$TELEMT_CONFIG"
|
||||
log_success "$(_t_or backup_restored_telemt 'telemt конфиг восстановлен')"
|
||||
fi
|
||||
|
||||
# ── GoTelegram config ──
|
||||
if [ -f "$src_gt" ]; then
|
||||
# Восстанавливаем goTelegram Pro конфиг
|
||||
if [ -f "$backup_dir/gotelegram.json" ]; then
|
||||
mkdir -p "$GOTELEGRAM_DIR"
|
||||
cp "$src_gt" "$GOTELEGRAM_CONFIG"
|
||||
cp "$backup_dir/gotelegram.json" "$GOTELEGRAM_CONFIG"
|
||||
log_success "$(_t_or backup_restored_gotelegram 'GoTelegram конфиг восстановлен')"
|
||||
fi
|
||||
|
||||
# ── Language ──
|
||||
if [ -f "$src_lang" ]; then
|
||||
if [ -f "$backup_dir/disabled_users.json" ]; then
|
||||
mkdir -p "$GOTELEGRAM_DIR"
|
||||
cp "$src_lang" "$GOTELEGRAM_DIR/.language"
|
||||
cp "$backup_dir/disabled_users.json" "$GOTELEGRAM_DIR/disabled_users.json"
|
||||
chmod 600 "$GOTELEGRAM_DIR/disabled_users.json" 2>/dev/null || true
|
||||
fi
|
||||
if [ -f "$backup_dir/backup_schedule.json" ]; then
|
||||
mkdir -p "$GOTELEGRAM_DIR"
|
||||
cp "$backup_dir/backup_schedule.json" "$GOTELEGRAM_DIR/backup_schedule.json" 2>/dev/null
|
||||
chmod 600 "$GOTELEGRAM_DIR/backup_schedule.json" 2>/dev/null || true
|
||||
if command -v jq >/dev/null 2>&1; then
|
||||
local restored_schedule
|
||||
restored_schedule=$(jq -r '.frequency // "off"' "$GOTELEGRAM_DIR/backup_schedule.json" 2>/dev/null || echo "off")
|
||||
case "$restored_schedule" in
|
||||
off|daily|weekly|monthly) set_backup_schedule "$restored_schedule" >/dev/null 2>&1 || true ;;
|
||||
esac
|
||||
fi
|
||||
fi
|
||||
|
||||
# Восстанавливаем language marker (i18n)
|
||||
if [ -f "$backup_dir/.language" ]; then
|
||||
mkdir -p "$GOTELEGRAM_DIR"
|
||||
cp "$backup_dir/.language" "$GOTELEGRAM_DIR/.language"
|
||||
log_success "$(_t_or backup_restored_lang 'Язык интерфейса восстановлен')"
|
||||
fi
|
||||
|
||||
# ── nginx ──
|
||||
if [ -f "$src_nginx" ]; then
|
||||
# Восстанавливаем nginx конфиг
|
||||
if [ -f "$backup_dir/nginx.conf" ]; then
|
||||
mkdir -p /etc/nginx/sites-available /etc/nginx/sites-enabled
|
||||
cp "$src_nginx" "$NGINX_SITE_CONF"
|
||||
cp "$backup_dir/nginx.conf" "$NGINX_SITE_CONF"
|
||||
ln -sf "$NGINX_SITE_CONF" "$NGINX_SITE_LINK"
|
||||
log_success "$(_t_or backup_restored_nginx 'nginx конфиг восстановлен')"
|
||||
fi
|
||||
|
||||
# ── Let's Encrypt (v2.0: full tree; v1.1: just flat certs/) ──
|
||||
if [ -n "$bk_domain" ] && [ -d "$src_le_live" ]; then
|
||||
local live_dir="/etc/letsencrypt/live/$bk_domain"
|
||||
mkdir -p "$live_dir"
|
||||
cp "$src_le_live/"*.pem "$live_dir/" 2>/dev/null
|
||||
if [ -n "$src_le_renewal" ] && [ -f "$src_le_renewal/${bk_domain}.conf" ]; then
|
||||
mkdir -p /etc/letsencrypt/renewal
|
||||
cp "$src_le_renewal/${bk_domain}.conf" "/etc/letsencrypt/renewal/"
|
||||
fi
|
||||
# Восстанавливаем SSL / Let's Encrypt structure
|
||||
if [ -d "$backup_dir/letsencrypt" ]; then
|
||||
mkdir -p /etc/letsencrypt/live /etc/letsencrypt/archive /etc/letsencrypt/renewal
|
||||
[ -d "$backup_dir/letsencrypt/live" ] && cp -a "$backup_dir/letsencrypt/live/." /etc/letsencrypt/live/ 2>/dev/null
|
||||
[ -d "$backup_dir/letsencrypt/archive" ] && cp -a "$backup_dir/letsencrypt/archive/." /etc/letsencrypt/archive/ 2>/dev/null
|
||||
[ -d "$backup_dir/letsencrypt/renewal" ] && cp -a "$backup_dir/letsencrypt/renewal/." /etc/letsencrypt/renewal/ 2>/dev/null
|
||||
log_success "$(_t_or backup_restored_ssl 'SSL сертификаты восстановлены')"
|
||||
elif [ -d "$backup_dir/certs" ]; then
|
||||
local domain
|
||||
domain=$(config_get domain 2>/dev/null)
|
||||
if [ -n "$domain" ]; then
|
||||
local cert_dir="/etc/letsencrypt/live/$domain"
|
||||
mkdir -p "$cert_dir"
|
||||
cp "$backup_dir/certs/"* "$cert_dir/" 2>/dev/null
|
||||
log_success "$(_t_or backup_restored_ssl 'SSL сертификаты восстановлены')"
|
||||
fi
|
||||
fi
|
||||
|
||||
# ── Site ──
|
||||
if [ -d "$src_site" ] && [ -f "$src_site/index.html" ]; then
|
||||
# Восстанавливаем шаблон сайта
|
||||
if [ -d "$backup_dir/site" ]; then
|
||||
mkdir -p "$WEBSITE_ROOT"
|
||||
cp -r "$src_site"/* "$WEBSITE_ROOT/"
|
||||
cp -a "$backup_dir/site/." "$WEBSITE_ROOT/"
|
||||
chown -R www-data:www-data "$WEBSITE_ROOT" 2>/dev/null
|
||||
log_success "$(_t_or backup_restored_site 'Шаблон сайта восстановлен')"
|
||||
fi
|
||||
|
||||
# ── Bot .env (v2.0 only) ──
|
||||
if [ -n "$src_bot" ] && [ -f "$src_bot" ]; then
|
||||
mkdir -p "$BOT_DIR"
|
||||
cp "$src_bot" "$BOT_DIR/.env"
|
||||
chmod 600 "$BOT_DIR/.env"
|
||||
log_success "$(_t_or backup_restored_bot 'Конфиг Telegram-бота восстановлен')"
|
||||
# Восстанавливаем custom templates/catalog/statistics
|
||||
if [ -d "$backup_dir/custom_templates" ]; then
|
||||
mkdir -p "$GOTELEGRAM_DIR/custom_templates"
|
||||
cp -a "$backup_dir/custom_templates/." "$GOTELEGRAM_DIR/custom_templates/" 2>/dev/null
|
||||
log_success "Пользовательские шаблоны восстановлены"
|
||||
fi
|
||||
if [ -f "$backup_dir/templates_catalog.json" ]; then
|
||||
cp "$backup_dir/templates_catalog.json" "$GOTELEGRAM_DIR/templates_catalog.json" 2>/dev/null
|
||||
fi
|
||||
if [ -f "$backup_dir/stats_history.csv" ]; then
|
||||
cp "$backup_dir/stats_history.csv" "$GOTELEGRAM_DIR/stats_history.csv" 2>/dev/null
|
||||
log_success "История статистики восстановлена"
|
||||
fi
|
||||
if [ -f "$backup_dir/user_stats_history.csv" ]; then
|
||||
cp "$backup_dir/user_stats_history.csv" "$GOTELEGRAM_DIR/user_stats_history.csv" 2>/dev/null
|
||||
log_success "История статистики пользователей восстановлена"
|
||||
fi
|
||||
if [ -f "$backup_dir/shared-443.json" ]; then
|
||||
cp "$backup_dir/shared-443.json" "$GOTELEGRAM_DIR/shared-443.json" 2>/dev/null
|
||||
fi
|
||||
|
||||
# ── Start services ──
|
||||
if type is_telemt_installed &>/dev/null && is_telemt_installed; then
|
||||
# Восстанавливаем состояние бота
|
||||
if [ -d "$backup_dir/bot" ]; then
|
||||
mkdir -p "$BOT_DIR"
|
||||
[ -f "$backup_dir/bot/.env" ] && cp "$backup_dir/bot/.env" "$BOT_DIR/.env" 2>/dev/null && chmod 600 "$BOT_DIR/.env"
|
||||
[ -d "$backup_dir/bot/lang" ] && cp -a "$backup_dir/bot/lang" "$BOT_DIR/" 2>/dev/null
|
||||
[ -f "$backup_dir/bot/i18n.py" ] && cp "$backup_dir/bot/i18n.py" "$BOT_DIR/i18n.py" 2>/dev/null
|
||||
log_success "Конфигурация Telegram-бота восстановлена"
|
||||
fi
|
||||
|
||||
# Восстанавливаем состояние локальной web-админки
|
||||
if [ -d "$backup_dir/admin_web" ]; then
|
||||
mkdir -p "$ADMIN_WEB_DIR"
|
||||
[ -f "$backup_dir/admin_web/server.py" ] && cp "$backup_dir/admin_web/server.py" "$ADMIN_WEB_DIR/server.py" 2>/dev/null
|
||||
[ -d "$backup_dir/admin_web/static" ] && cp -a "$backup_dir/admin_web/static" "$ADMIN_WEB_DIR/" 2>/dev/null
|
||||
rm -f "$ADMIN_WEB_DIR/token" 2>/dev/null || true
|
||||
log_success "Конфигурация web-админки восстановлена"
|
||||
fi
|
||||
|
||||
# Запускаем сервисы
|
||||
if is_telemt_installed && [ ! -f "/etc/systemd/system/${TELEMT_SERVICE}.service" ]; then
|
||||
install_telemt_service
|
||||
fi
|
||||
if is_telemt_installed; then
|
||||
start_telemt
|
||||
fi
|
||||
systemctl start nginx 2>/dev/null
|
||||
command -v nginx &>/dev/null && systemctl start nginx 2>/dev/null
|
||||
systemctl restart gotelegram-bot 2>/dev/null || true
|
||||
systemctl restart gotelegram-admin 2>/dev/null || true
|
||||
|
||||
# ── Cleanup ──
|
||||
# Очистка
|
||||
rm -rf "$tmp_dir"
|
||||
[ "$tar_file" != "$backup_file" ] && rm -f "$tar_file"
|
||||
|
||||
log_success "$(_t_or backup_restore_done 'Восстановление завершено!')"
|
||||
|
||||
# ── Auto-migrate v1.1 → v2.0 ──
|
||||
if [ "$bk_version" != "2.0" ]; then
|
||||
log_info "$(_t_or backup_automigrate 'Конвертирую старый бекап в UBF v2.0...')"
|
||||
create_backup "" >/dev/null 2>&1 && \
|
||||
log_success "$(_t_or backup_migrated 'Свежий UBF v2.0 бекап сохранён в $BACKUP_DIR')"
|
||||
fi
|
||||
|
||||
type show_proxy_info &>/dev/null && show_proxy_info
|
||||
show_proxy_info
|
||||
return 0
|
||||
}
|
||||
|
||||
@@ -442,37 +411,35 @@ list_backups() {
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo "" >&2
|
||||
echo -e " ${BOLD}${WHITE}📦 $(_t_or backup_list_title 'Доступные бекапы'):${NC}" >&2
|
||||
echo -e " ${DIM}$(printf '─%.0s' {1..70})${NC}" >&2
|
||||
echo ""
|
||||
echo -e " ${BOLD}${WHITE}📦 $(_t_or backup_list_title 'Доступные бекапы'):${NC}"
|
||||
echo -e " ${DIM}$(printf '─%.0s' {1..60})${NC}"
|
||||
|
||||
local i=1
|
||||
for f in "$BACKUP_DIR"/gotelegram_backup_*.tar.gz*; do
|
||||
for f in "$BACKUP_DIR"/*.tar.gz*; do
|
||||
[ -f "$f" ] || continue
|
||||
[[ "$f" == *.sha256 ]] && continue
|
||||
local size name date_str id_tail encrypted=""
|
||||
local size date_str name
|
||||
size=$(du -h "$f" | cut -f1)
|
||||
name=$(basename "$f")
|
||||
date_str=$(echo "$name" | grep -oE '[0-9]{8}_[0-9]{6}' | head -1)
|
||||
id_tail=$(echo "$name" | grep -oE '_[0-9a-f]{6}\.tar' | head -1 | tr -d '_.tar')
|
||||
local encrypted=""
|
||||
[[ "$f" == *.enc ]] && encrypted=" 🔒"
|
||||
local id_display=""
|
||||
[ -n "$id_tail" ] && id_display=" ${DIM}[...${id_tail}]${NC}"
|
||||
echo -e " ${CYAN}${i})${NC} ${name} (${size})${encrypted}${id_display}" >&2
|
||||
echo -e " ${CYAN}${i})${NC} ${name} (${size})${encrypted}"
|
||||
((i++))
|
||||
done
|
||||
echo -e " ${DIM}$(printf '─%.0s' {1..70})${NC}" >&2
|
||||
echo -e " ${DIM}$(printf '─%.0s' {1..60})${NC}"
|
||||
}
|
||||
|
||||
# ── Очистка старых бекапов ───────────────────────────────────────────────────
|
||||
cleanup_old_backups() {
|
||||
local keep="${1:-5}"
|
||||
local count
|
||||
count=$(find "$BACKUP_DIR" -name "gotelegram_backup_*.tar.gz*" ! -name "*.sha256" 2>/dev/null | wc -l)
|
||||
count=$(find "$BACKUP_DIR" -name "*.tar.gz*" ! -name "*.sha256" 2>/dev/null | wc -l)
|
||||
|
||||
if [ "$count" -gt "$keep" ]; then
|
||||
local to_delete=$((count - keep))
|
||||
find "$BACKUP_DIR" -name "gotelegram_backup_*.tar.gz*" ! -name "*.sha256" 2>/dev/null | sort | head -n "$to_delete" | while read -r f; do
|
||||
find "$BACKUP_DIR" -name "*.tar.gz*" ! -name "*.sha256" 2>/dev/null | sort | head -n "$to_delete" | while read -r f; do
|
||||
rm -f "$f" "${f}.sha256"
|
||||
done
|
||||
if type tf &>/dev/null; then
|
||||
@@ -483,6 +450,84 @@ cleanup_old_backups() {
|
||||
fi
|
||||
}
|
||||
|
||||
# ── Расписание бекапов ───────────────────────────────────────────────────────
|
||||
backup_schedule_calendar() {
|
||||
case "${1:-off}" in
|
||||
off) echo "" ;;
|
||||
daily) echo "*-*-* 03:20:00" ;;
|
||||
weekly) echo "Sun 03:20:00" ;;
|
||||
monthly) echo "*-*-01 03:20:00" ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
set_backup_schedule() {
|
||||
local frequency="${1:-off}"
|
||||
local calendar
|
||||
if ! calendar=$(backup_schedule_calendar "$frequency"); then
|
||||
log_error "Unsupported backup schedule: $frequency"
|
||||
return 1
|
||||
fi
|
||||
|
||||
mkdir -p "$GOTELEGRAM_DIR" "$BACKUP_DIR"
|
||||
|
||||
if [ "$frequency" = "off" ]; then
|
||||
systemctl disable --now gotelegram-backup.timer >/dev/null 2>&1 || true
|
||||
rm -f /etc/systemd/system/gotelegram-backup.timer /etc/systemd/system/gotelegram-backup.service
|
||||
systemctl daemon-reload >/dev/null 2>&1 || true
|
||||
else
|
||||
cat > /etc/systemd/system/gotelegram-backup.service << 'EOSVC'
|
||||
[Unit]
|
||||
Description=goTelegram Pro backup
|
||||
Wants=network-online.target
|
||||
After=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
Environment=GOTELEGRAM_BACKUP_KEEP=30
|
||||
ExecStart=/bin/bash -lc 'source /opt/gotelegram/lib/common.sh; source /opt/gotelegram/lib/i18n.sh; source /opt/gotelegram/lib/telemt.sh; source /opt/gotelegram/lib/website.sh; source /opt/gotelegram/lib/backup.sh; load_language "$(detect_language 2>/dev/null || echo en)"; create_backup ""; cleanup_old_backups "${GOTELEGRAM_BACKUP_KEEP:-30}"'
|
||||
EOSVC
|
||||
|
||||
cat > /etc/systemd/system/gotelegram-backup.timer << EOTIMER
|
||||
[Unit]
|
||||
Description=goTelegram Pro scheduled backup
|
||||
|
||||
[Timer]
|
||||
OnCalendar=$calendar
|
||||
Persistent=true
|
||||
RandomizedDelaySec=15m
|
||||
Unit=gotelegram-backup.service
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
EOTIMER
|
||||
systemctl daemon-reload >/dev/null 2>&1 || return 1
|
||||
systemctl enable --now gotelegram-backup.timer >/dev/null 2>&1 || return 1
|
||||
fi
|
||||
|
||||
cat > "$GOTELEGRAM_DIR/backup_schedule.json" << EOSCHEDULE
|
||||
{
|
||||
"frequency": "$frequency",
|
||||
"calendar": "$calendar",
|
||||
"keep": 30,
|
||||
"updated_at": "$(date -Iseconds)"
|
||||
}
|
||||
EOSCHEDULE
|
||||
chmod 600 "$GOTELEGRAM_DIR/backup_schedule.json" 2>/dev/null || true
|
||||
log_success "Backup schedule: $frequency"
|
||||
echo "$frequency"
|
||||
}
|
||||
|
||||
backup_schedule_status() {
|
||||
local frequency="off" calendar=""
|
||||
if [ -f "$GOTELEGRAM_DIR/backup_schedule.json" ] && command -v jq >/dev/null 2>&1; then
|
||||
frequency=$(jq -r '.frequency // "off"' "$GOTELEGRAM_DIR/backup_schedule.json" 2>/dev/null || echo "off")
|
||||
calendar=$(jq -r '.calendar // ""' "$GOTELEGRAM_DIR/backup_schedule.json" 2>/dev/null || echo "")
|
||||
fi
|
||||
echo "frequency=$frequency calendar=$calendar"
|
||||
systemctl list-timers gotelegram-backup.timer --no-pager 2>/dev/null || true
|
||||
}
|
||||
|
||||
# ── Интерактивный бекап ──────────────────────────────────────────────────────
|
||||
interactive_backup() {
|
||||
echo ""
|
||||
@@ -508,7 +553,7 @@ interactive_backup() {
|
||||
fi
|
||||
fi
|
||||
|
||||
create_backup "$password" >/dev/null
|
||||
create_backup "$password"
|
||||
cleanup_old_backups
|
||||
}
|
||||
|
||||
@@ -522,7 +567,7 @@ interactive_restore() {
|
||||
local backup_file=""
|
||||
if [[ "$choice" =~ ^[0-9]+$ ]]; then
|
||||
local i=1
|
||||
for f in "$BACKUP_DIR"/gotelegram_backup_*.tar.gz*; do
|
||||
for f in "$BACKUP_DIR"/*.tar.gz*; do
|
||||
[ -f "$f" ] || continue
|
||||
[[ "$f" == *.sha256 ]] && continue
|
||||
if [ "$i" -eq "$choice" ]; then
|
||||
@@ -542,115 +587,3 @@ interactive_restore() {
|
||||
|
||||
restore_backup "$backup_file"
|
||||
}
|
||||
|
||||
# ── Manual secret recovery (v2.4.9) ──────────────────────────────────────────
|
||||
# Parser accepts any of the 3 formats and emits key=value lines on stdout:
|
||||
# tg://proxy?server=X&port=Y&secret=Z → raw_secret, server, port, domain (if ee-prefix)
|
||||
# ee<32hex><hex_domain> → raw_secret, domain
|
||||
# <32hex> → raw_secret only
|
||||
# Returns 0 on success, 1 on parse failure.
|
||||
parse_manual_secret() {
|
||||
local input="$1"
|
||||
input=$(echo "$input" | tr -d ' \t\n\r')
|
||||
[ -z "$input" ] && return 1
|
||||
|
||||
local raw_secret="" domain="" server="" port=""
|
||||
|
||||
if echo "$input" | grep -q '^tg://proxy?'; then
|
||||
local qs="${input#tg://proxy?}"
|
||||
local kv k v
|
||||
local -a kvs
|
||||
IFS='&' read -ra kvs <<< "$qs"
|
||||
for kv in "${kvs[@]}"; do
|
||||
k="${kv%%=*}"
|
||||
v="${kv#*=}"
|
||||
case "$k" in
|
||||
server) server="$v" ;;
|
||||
port) port="$v" ;;
|
||||
secret) raw_secret="$v" ;;
|
||||
esac
|
||||
done
|
||||
[ -z "$raw_secret" ] && return 1
|
||||
# Strip hex-escapes that Telegram sometimes URL-encodes
|
||||
raw_secret=$(echo "$raw_secret" | tr -d '%')
|
||||
fi
|
||||
|
||||
# After pulling from URL (if any), raw_secret might still be ee-prefixed.
|
||||
# Otherwise, try the raw_secret as the whole input.
|
||||
local candidate="${raw_secret:-$input}"
|
||||
|
||||
if [[ "$candidate" =~ ^[eE][eE][0-9a-fA-F]{32}[0-9a-fA-F]*$ ]]; then
|
||||
raw_secret="${candidate:2:32}"
|
||||
local hex_domain="${candidate:34}"
|
||||
if [ -n "$hex_domain" ]; then
|
||||
local decoded
|
||||
decoded=$(echo "$hex_domain" | xxd -r -p 2>/dev/null)
|
||||
# Validate decoded looks like a domain
|
||||
if echo "$decoded" | grep -qE '^[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'; then
|
||||
domain="$decoded"
|
||||
fi
|
||||
fi
|
||||
elif [[ "$candidate" =~ ^[0-9a-fA-F]{32}$ ]]; then
|
||||
raw_secret="$candidate"
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
|
||||
[[ "$raw_secret" =~ ^[0-9a-fA-F]{32}$ ]] || return 1
|
||||
raw_secret=$(echo "$raw_secret" | tr 'A-F' 'a-f')
|
||||
|
||||
echo "raw_secret=$raw_secret"
|
||||
[ -n "$domain" ] && echo "domain=$domain"
|
||||
[ -n "$server" ] && echo "server=$server"
|
||||
[ -n "$port" ] && echo "port=$port"
|
||||
return 0
|
||||
}
|
||||
|
||||
# ── Interactive: user types their old key, we parse it ─────────────────────
|
||||
manual_secret_input() {
|
||||
echo "" >&2
|
||||
echo -e " ${BOLD}${WHITE}🔑 $(_t_or manual_secret_title 'Ввод существующего ключа')${NC}" >&2
|
||||
echo -e " ${DIM}$(printf '─%.0s' {1..60})${NC}" >&2
|
||||
echo -e " ${DIM}$(_t_or manual_secret_help1 'Поддерживаются форматы:')${NC}" >&2
|
||||
echo -e " ${DIM} • tg://proxy?server=...&port=...&secret=...${NC}" >&2
|
||||
echo -e " ${DIM} • ee<32hex><hexdomain> (fake-TLS)${NC}" >&2
|
||||
echo -e " ${DIM} • 32hex (только raw secret)${NC}" >&2
|
||||
echo -e " ${DIM}$(printf '─%.0s' {1..60})${NC}" >&2
|
||||
echo -ne " ${WHITE}$(_t_or manual_secret_prompt 'Вставьте ключ'):${NC} " >&2
|
||||
read -r user_input
|
||||
|
||||
if [ -z "$user_input" ]; then
|
||||
log_error "$(_t_or manual_secret_empty 'Ключ не введён')"
|
||||
return 1
|
||||
fi
|
||||
|
||||
local parsed
|
||||
if ! parsed=$(parse_manual_secret "$user_input"); then
|
||||
log_error "$(_t_or manual_secret_bad 'Не удалось распознать формат ключа')"
|
||||
return 1
|
||||
fi
|
||||
|
||||
local p_raw="" p_domain="" p_server="" p_port=""
|
||||
while IFS='=' read -r k v; do
|
||||
case "$k" in
|
||||
raw_secret) p_raw="$v" ;;
|
||||
domain) p_domain="$v" ;;
|
||||
server) p_server="$v" ;;
|
||||
port) p_port="$v" ;;
|
||||
esac
|
||||
done <<< "$parsed"
|
||||
|
||||
echo "" >&2
|
||||
echo -e " ${GREEN}✓ $(_t_or manual_secret_parsed 'Ключ распознан')${NC}" >&2
|
||||
echo -e " ${WHITE}raw_secret:${NC} ${DIM}${p_raw:0:8}...${p_raw: -4}${NC}" >&2
|
||||
[ -n "$p_domain" ] && echo -e " ${WHITE}domain:${NC} ${CYAN}${p_domain}${NC}" >&2
|
||||
[ -n "$p_server" ] && echo -e " ${WHITE}server:${NC} ${CYAN}${p_server}${NC}" >&2
|
||||
[ -n "$p_port" ] && echo -e " ${WHITE}port:${NC} ${CYAN}${p_port}${NC}" >&2
|
||||
echo "" >&2
|
||||
|
||||
# Export for the subsequent install flow to pick up
|
||||
export GOTELEGRAM_EXISTING_SECRET="$p_raw"
|
||||
export GOTELEGRAM_EXISTING_DOMAIN="$p_domain"
|
||||
export GOTELEGRAM_EXISTING_PORT="$p_port"
|
||||
return 0
|
||||
}
|
||||
|
||||
196
lib/common.sh
Normal file → Executable file
196
lib/common.sh
Normal file → Executable file
@@ -1,10 +1,10 @@
|
||||
#!/bin/bash
|
||||
# GoTelegram v2.4 — common utilities
|
||||
# goTelegram Pro v2.5.0 — common utilities
|
||||
# Colors, logging, spinner, system helpers, v1 compat, i18n-aware
|
||||
|
||||
# ── Version ───────────────────────────────────────────────────────────────────
|
||||
GOTELEGRAM_VERSION="2.4.10"
|
||||
GOTELEGRAM_NAME="GoTelegram"
|
||||
GOTELEGRAM_VERSION="2.5.0"
|
||||
GOTELEGRAM_NAME="goTelegram Pro"
|
||||
|
||||
# ── Пути ──────────────────────────────────────────────────────────────────────
|
||||
GOTELEGRAM_DIR="/opt/gotelegram"
|
||||
@@ -18,6 +18,10 @@ WEBSITE_ROOT="/var/www/gotelegram-site"
|
||||
BACKUP_DIR="$GOTELEGRAM_DIR/backups"
|
||||
LOG_FILE="/var/log/gotelegram.log"
|
||||
BOT_DIR="/opt/gotelegram-bot"
|
||||
ADMIN_WEB_DIR="/opt/gotelegram-admin"
|
||||
ADMIN_WEB_SERVICE="gotelegram-admin"
|
||||
ADMIN_WEB_HOST="127.0.0.1"
|
||||
ADMIN_WEB_PORT="1984"
|
||||
|
||||
# ── V1 совместимость ─────────────────────────────────────────────────────────
|
||||
V1_CONTAINER_NAME="mtproto-proxy"
|
||||
@@ -119,7 +123,7 @@ show_banner() {
|
||||
echo -e " ${DIM}$(t banner_subtitle)${NC}"
|
||||
echo -e " ${DIM}$(t banner_features)${NC}"
|
||||
else
|
||||
echo -e " ${BOLD}${WHITE}🚀 GoTelegram v${GOTELEGRAM_VERSION}${NC}"
|
||||
echo -e " ${BOLD}${WHITE}🚀 goTelegram Pro v${GOTELEGRAM_VERSION}${NC}"
|
||||
echo -e " ${DIM}MTProxy powered by telemt (Rust + Tokio)${NC}"
|
||||
echo -e " ${DIM}Anti-DPI • Fake TLS • TCP Splice • JA3/JA4${NC}"
|
||||
fi
|
||||
@@ -333,6 +337,7 @@ apt_pkg_for_cmd() {
|
||||
ss) echo "iproute2" ;;
|
||||
netstat) echo "net-tools" ;;
|
||||
flock) echo "util-linux" ;;
|
||||
iptables) echo "iptables" ;;
|
||||
*) echo "$1" ;; # команда == имя пакета
|
||||
esac
|
||||
}
|
||||
@@ -344,6 +349,7 @@ dnf_pkg_for_cmd() {
|
||||
ss) echo "iproute" ;;
|
||||
netstat) echo "net-tools" ;;
|
||||
flock) echo "util-linux" ;;
|
||||
iptables) echo "iptables" ;;
|
||||
*) echo "$1" ;;
|
||||
esac
|
||||
}
|
||||
@@ -355,7 +361,7 @@ ensure_deps() {
|
||||
# change-lite-domain из бота).
|
||||
local critical=(curl jq openssl git xxd tar dig flock)
|
||||
# Желательные — есть fallback, устанавливать всё равно, но не падать если не смогли
|
||||
local optional=(qrencode bc)
|
||||
local optional=(qrencode bc iptables)
|
||||
|
||||
local missing_critical=() missing_optional=() cmd
|
||||
for cmd in "${critical[@]}"; do
|
||||
@@ -463,165 +469,51 @@ check_port() {
|
||||
return 1 # свободен
|
||||
}
|
||||
|
||||
# ── Preflight: port conflict detection ───────────────────────────────────────
|
||||
# Проверяет, что нужные для установки порты свободны. Если порт занят —
|
||||
# определяет процесс и сопоставляет с известным списком proxy/VPN софта
|
||||
# (xray, sing-box, v2ray, trojan, hysteria, mtg, shadowsocks, x-ui/3x-ui,
|
||||
# marzban, amneziawg, caddy, apache, haproxy). Пользователь видит явное
|
||||
# предупреждение и может либо прервать установку, либо продолжить на свой
|
||||
# страх и риск (GOTELEGRAM_SKIP_PREFLIGHT=1 — полностью отключить проверку).
|
||||
#
|
||||
# Используемые порты GoTelegram:
|
||||
# 443 — telemt (внешний, MTProxy + fake-TLS) — lite и pro
|
||||
# 80 — nginx redirect + certbot ACME HTTP-01 — только pro
|
||||
# 8443 — nginx internal mask (127.0.0.1:8443) — только pro
|
||||
|
||||
# get_port_process <port> → "<pid>|<comm>" если занят, иначе пусто
|
||||
get_port_process() {
|
||||
local port="$1"
|
||||
local line="" pid="" proc=""
|
||||
line=$(ss -tlnp 2>/dev/null | grep -E ":${port}[[:space:]]" | head -1)
|
||||
if [ -z "$line" ]; then
|
||||
line=$(netstat -tlnp 2>/dev/null | grep -E ":${port}[[:space:]]" | head -1)
|
||||
fi
|
||||
if [ -n "$line" ]; then
|
||||
pid=$(echo "$line" | grep -oE 'pid=[0-9]+' | head -1 | cut -d= -f2)
|
||||
if [ -z "$pid" ]; then
|
||||
# netstat format: "12345/procname"
|
||||
pid=$(echo "$line" | grep -oE '[0-9]+/[^ ]+' | head -1 | cut -d/ -f1)
|
||||
fi
|
||||
fi
|
||||
if [ -z "$pid" ]; then
|
||||
pid=$(fuser -n tcp "$port" 2>/dev/null | tr -s ' ' | awk '{print $1}' | head -1)
|
||||
pid="${pid:-}"
|
||||
fi
|
||||
if [ -n "$pid" ] && [ "$pid" -gt 0 ] 2>/dev/null; then
|
||||
proc=$(ps -p "$pid" -o comm= 2>/dev/null | tr -d ' \n')
|
||||
[ -z "$proc" ] && proc="unknown"
|
||||
echo "${pid}|${proc}"
|
||||
detect_3xui() {
|
||||
if systemctl list-unit-files 2>/dev/null | grep -Eq '^(x-ui|3x-ui)\.service'; then
|
||||
return 0
|
||||
fi
|
||||
if [ -n "$line" ]; then
|
||||
# Port is occupied but process cannot be identified (kernel socket / no root)
|
||||
echo "0|unknown"
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
[ -d /etc/x-ui ] || [ -d /usr/local/x-ui ] || [ -f /etc/x-ui/x-ui.db ]
|
||||
}
|
||||
|
||||
# match_known_conflict <comm> → печатает человекочитаемое имя если это
|
||||
# известный proxy/VPN/web софт. Возвращает 0 если нашли, 1 иначе.
|
||||
match_known_conflict() {
|
||||
local proc="$1"
|
||||
case "$proc" in
|
||||
*xray*|*Xray*) echo "Xray"; return 0 ;;
|
||||
*sing-box*|*sing_box*|*singbox*) echo "sing-box"; return 0 ;;
|
||||
*v2ray*|*V2Ray*) echo "V2Ray"; return 0 ;;
|
||||
*trojan*) echo "Trojan"; return 0 ;;
|
||||
*hysteria*) echo "Hysteria"; return 0 ;;
|
||||
*mtg*) echo "mtg (old MTProxy)"; return 0 ;;
|
||||
*ss-server*|*ss-local*|*shadowsocks*|*ssserver*) echo "Shadowsocks"; return 0 ;;
|
||||
*x-ui*|*3x-ui*|*xui*) echo "x-ui / 3x-ui panel"; return 0 ;;
|
||||
*marzban*) echo "Marzban panel"; return 0 ;;
|
||||
*amneziawg*|*awg-go*|*awg*) echo "AmneziaWG"; return 0 ;;
|
||||
*caddy*) echo "Caddy web server"; return 0 ;;
|
||||
*apache2*|*httpd*) echo "Apache httpd"; return 0 ;;
|
||||
*haproxy*) echo "HAProxy"; return 0 ;;
|
||||
*nginx*) echo "nginx (already running)"; return 0 ;;
|
||||
*tgproxy*|*mtproxy*|*mtproto*) echo "MTProto Proxy (other impl)"; return 0 ;;
|
||||
*wireguard*|*wg-quick*) echo "WireGuard"; return 0 ;;
|
||||
*openvpn*) echo "OpenVPN"; return 0 ;;
|
||||
esac
|
||||
return 1
|
||||
detect_3xui_443_listener() {
|
||||
ss -ltnp 2>/dev/null | grep -E '(:|])443[[:space:]]' | grep -Eiq '(xray|x-ui|3x-ui)'
|
||||
}
|
||||
|
||||
# preflight_check <mode> [port]
|
||||
# mode = "lite" | "pro"
|
||||
# port = selected port for lite mode (default 443)
|
||||
# Returns:
|
||||
# 0 — OK to proceed (no conflicts, or user confirmed to force)
|
||||
# 1 — user aborted (caller should show promo and return)
|
||||
preflight_check() {
|
||||
local mode="${1:-lite}"
|
||||
local lite_port="${2:-443}"
|
||||
warn_3xui_443_conflict() {
|
||||
detect_3xui_443_listener || return 1
|
||||
log_warning "Обнаружен 3x-ui/Xray, который уже слушает TCP/443."
|
||||
log_warning "goTelegram Pro не будет молча останавливать или переписывать 3x-ui."
|
||||
log_dim "Для настоящего shared-443 нужен один фронтовой TLS/SNI-диспетчер и разные SNI-домены для Xray и goTelegram Pro."
|
||||
mkdir -p "$GOTELEGRAM_DIR" 2>/dev/null
|
||||
cat > "$GOTELEGRAM_DIR/shared-443-3xui.md" <<'EOF' 2>/dev/null || true
|
||||
# goTelegram Pro + 3x-ui on one TCP/443
|
||||
|
||||
# Escape hatch
|
||||
if [ "${GOTELEGRAM_SKIP_PREFLIGHT:-0}" = "1" ]; then
|
||||
log_dim "preflight: skipped (GOTELEGRAM_SKIP_PREFLIGHT=1)"
|
||||
return 0
|
||||
fi
|
||||
goTelegram Pro detected that 3x-ui/Xray already owns TCP/443. Two independent
|
||||
processes cannot bind the same IP:port at the same time. A safe shared setup
|
||||
needs one front TLS/SNI dispatcher on 443 and internal backends, for example:
|
||||
|
||||
local required_ports=()
|
||||
if [ "$mode" = "pro" ]; then
|
||||
required_ports=(443 80 8443)
|
||||
else
|
||||
# lite: проверяем только выбранный внешний порт
|
||||
required_ports=("$lite_port")
|
||||
fi
|
||||
- dispatcher: 0.0.0.0:443 (nginx stream ssl_preread)
|
||||
- goTelegram Pro telemt: 127.0.0.1:7443
|
||||
- 3x-ui/Xray inbound: 127.0.0.1:9443
|
||||
- goTelegram Pro nginx mask site: 127.0.0.1:8443
|
||||
|
||||
local known_conflicts=() unknown_conflicts=() info port pid proc label
|
||||
for port in "${required_ports[@]}"; do
|
||||
info=$(get_port_process "$port")
|
||||
if [ -n "$info" ]; then
|
||||
pid="${info%%|*}"
|
||||
proc="${info##*|}"
|
||||
if label=$(match_known_conflict "$proc"); then
|
||||
known_conflicts+=("${port}|${label}|${pid}|${proc}")
|
||||
else
|
||||
unknown_conflicts+=("${port}|${pid}|${proc}")
|
||||
fi
|
||||
fi
|
||||
done
|
||||
The dispatcher routes Xray SNI domains to Xray. Everything else goes to telemt;
|
||||
telemt then decides whether the session is MTProxy or regular HTTPS and forwards
|
||||
the website to nginx through dns_overrides.
|
||||
|
||||
if [ ${#known_conflicts[@]} -eq 0 ] && [ ${#unknown_conflicts[@]} -eq 0 ]; then
|
||||
log_dim "preflight: ports ${required_ports[*]} свободны"
|
||||
return 0
|
||||
fi
|
||||
goTelegram Pro can generate the dispatcher with:
|
||||
|
||||
# Показываем баннер конфликта
|
||||
echo "" >&2
|
||||
echo -e " ${BOLD}${YELLOW}⚠ $(_t_or preflight_title 'Предустановочная проверка: обнаружены конфликты портов')${NC}" >&2
|
||||
echo -e " ${DIM}$(printf '─%.0s' {1..60})${NC}" >&2
|
||||
source /opt/gotelegram/lib/shared443.sh
|
||||
shared443_enable <gotelegram-domain> <xray-sni-domain> 127.0.0.1:9443
|
||||
|
||||
local item p label2 pid2 proc2 rest
|
||||
if [ ${#known_conflicts[@]} -gt 0 ]; then
|
||||
echo -e " ${RED}$(_t_or preflight_known 'Известный proxy/VPN/веб-софт занимает нужные порты:')${NC}" >&2
|
||||
for item in "${known_conflicts[@]}"; do
|
||||
p="${item%%|*}"
|
||||
rest="${item#*|}"
|
||||
label2="${rest%%|*}"
|
||||
rest="${rest#*|}"
|
||||
pid2="${rest%%|*}"
|
||||
proc2="${rest##*|}"
|
||||
echo -e " ${RED}✗${NC} ${BOLD}:${p}${NC} → ${BOLD}${label2}${NC} ${DIM}(pid=${pid2}, cmd=${proc2})${NC}" >&2
|
||||
done
|
||||
fi
|
||||
if [ ${#unknown_conflicts[@]} -gt 0 ]; then
|
||||
echo -e " ${YELLOW}$(_t_or preflight_unknown 'Порты заняты неизвестными процессами:')${NC}" >&2
|
||||
for item in "${unknown_conflicts[@]}"; do
|
||||
p="${item%%|*}"
|
||||
rest="${item#*|}"
|
||||
pid2="${rest%%|*}"
|
||||
proc2="${rest##*|}"
|
||||
echo -e " ${YELLOW}⚠${NC} ${BOLD}:${p}${NC} ${DIM}(pid=${pid2}, cmd=${proc2})${NC}" >&2
|
||||
done
|
||||
fi
|
||||
|
||||
echo -e " ${DIM}$(printf '─%.0s' {1..60})${NC}" >&2
|
||||
echo -e " ${WHITE}$(_t_or preflight_needed 'GoTelegram нужны порты:')${NC} ${CYAN}${required_ports[*]}${NC}" >&2
|
||||
echo -e " ${WHITE}$(_t_or preflight_hint_header 'Рекомендации:')${NC}" >&2
|
||||
echo -e " ${DIM}• $(_t_or preflight_hint1 'Остановите и удалите конфликтующие сервисы (systemctl stop ...)')${NC}" >&2
|
||||
echo -e " ${DIM}• $(_t_or preflight_hint2 'Либо возьмите чистый VPS без других прокси')${NC}" >&2
|
||||
echo -e " ${DIM}• $(_t_or preflight_hint3 'Установка поверх, скорее всего, завершится некорректно')${NC}" >&2
|
||||
echo -e " ${DIM}$(_t_or preflight_skip_hint 'Override: GOTELEGRAM_SKIP_PREFLIGHT=1 gotelegram')${NC}" >&2
|
||||
echo "" >&2
|
||||
|
||||
if confirm "$(_t_or preflight_proceed 'Продолжить установку всё равно (скорее всего не заработает)?')"; then
|
||||
log_warning "$(_t_or preflight_forced 'Установка продолжена вопреки конфликтам — возможны ошибки')"
|
||||
return 0
|
||||
fi
|
||||
log_info "$(_t_or preflight_aborted 'Установка отменена из-за конфликтов портов')"
|
||||
return 1
|
||||
Move the 3x-ui/Xray inbound from 0.0.0.0:443 to 127.0.0.1:9443 in the panel first,
|
||||
or nginx will not be able to own the public 443 socket. goTelegram Pro intentionally
|
||||
does not rewrite the 3x-ui SQLite database or generated Xray config without explicit
|
||||
operator confirmation, because 3x-ui can overwrite manual JSON edits on the next
|
||||
panel change.
|
||||
EOF
|
||||
return 0
|
||||
}
|
||||
|
||||
check_disk_space() {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/bin/bash
|
||||
# GoTelegram v2.4 — i18n engine
|
||||
# GoTelegram v2.5.0 — i18n engine
|
||||
# Internationalization support: EN (English) / RU (Русский)
|
||||
#
|
||||
# Usage:
|
||||
|
||||
126
lib/lang/en.sh
Normal file → Executable file
126
lib/lang/en.sh
Normal file → Executable file
@@ -1,5 +1,5 @@
|
||||
#!/bin/bash
|
||||
# GoTelegram v2.4 — English translations
|
||||
# goTelegram Pro v2.5.0 — English translations
|
||||
# shellcheck disable=SC2034,SC2148
|
||||
|
||||
# ── Common words ────────────────────────────────────────────────────────
|
||||
@@ -25,7 +25,7 @@ I18N[success]="Done"
|
||||
I18N[wait]="Please wait..."
|
||||
|
||||
# ── Banner ──────────────────────────────────────────────────────────────
|
||||
I18N[banner_title]="GoTelegram v%s"
|
||||
I18N[banner_title]="goTelegram Pro v%s"
|
||||
I18N[banner_subtitle]="MTProxy powered by telemt (Rust + Tokio)"
|
||||
I18N[banner_features]="Anti-DPI • Fake TLS • TCP Splice • JA3/JA4"
|
||||
I18N[credits_title]="Credits / Thanks"
|
||||
@@ -44,33 +44,6 @@ I18N[net_mode]="Mode:"
|
||||
I18N[net_domain]="Domain:"
|
||||
I18N[connection_link]="Telegram connection link:"
|
||||
I18N[proxy_not_configured]="Proxy is not configured. Select option 1."
|
||||
# ── show_proxy_info labels ─────────────────────────────────────────────
|
||||
I18N[info_status_running]="Running"
|
||||
I18N[info_status_stopped]="Stopped"
|
||||
I18N[info_status_not_installed]="Not installed"
|
||||
I18N[info_proxy_status]="Proxy status"
|
||||
I18N[info_engine]="Engine"
|
||||
I18N[info_ip]="IP"
|
||||
I18N[info_domain]="Domain"
|
||||
I18N[info_port]="Port"
|
||||
I18N[info_mode]="Mode"
|
||||
I18N[info_mask]="Mask host"
|
||||
I18N[info_secret]="Secret"
|
||||
I18N[info_link]="Link"
|
||||
# ── show_traffic_stats labels ──────────────────────────────────────────
|
||||
I18N[stats_sh_proxy]="Proxy (telemt, port 443)"
|
||||
I18N[stats_sh_site]="Site (nginx, port 8443)"
|
||||
I18N[stats_sh_hdr_period]="Period"
|
||||
I18N[stats_sh_hdr_inbound]="Inbound"
|
||||
I18N[stats_sh_hdr_rate]="Rate"
|
||||
I18N[stats_sh_packets]="Packets"
|
||||
I18N[stats_sh_1min]="1 min"
|
||||
I18N[stats_sh_5min]="5 min"
|
||||
I18N[stats_sh_60min]="60 min"
|
||||
I18N[stats_sh_1day]="1 day"
|
||||
I18N[stats_sh_7days]="7 days"
|
||||
I18N[stats_sh_30days]="30 days"
|
||||
I18N[stats_sh_365days]="365 days"
|
||||
I18N[menu_proxy]="Proxy ▸"
|
||||
I18N[menu_stats]="Statistics ▸"
|
||||
I18N[menu_manage]="Management ▸"
|
||||
@@ -102,7 +75,7 @@ I18N[submenu_about_title]="ℹ️ ABOUT"
|
||||
I18N[about_version_info]="Version info"
|
||||
I18N[about_promo]="Promo / Donate"
|
||||
I18N[version_title]="🔍 Information"
|
||||
I18N[version_label]="GoTelegram:"
|
||||
I18N[version_label]="goTelegram Pro:"
|
||||
I18N[version_engine]="Engine:"
|
||||
I18N[version_tech]="Technology:"
|
||||
I18N[version_license]="License:"
|
||||
@@ -133,7 +106,7 @@ I18N[install_cfg_mode]="Mode:"
|
||||
I18N[install_cfg_domain]="Domain:"
|
||||
I18N[install_confirm_proxy]="Install proxy?"
|
||||
I18N[install_confirm_proxy_site]="Install proxy + website?"
|
||||
I18N[install_done]="GoTelegram v%s installed! (%s mode)"
|
||||
I18N[install_done]="goTelegram Pro v%s installed! (%s mode)"
|
||||
I18N[install_arch_desc1]="telemt accepts all traffic on 443 (HTTPS masquerade)"
|
||||
I18N[install_arch_desc2]="nginx serves the site on internal port %s"
|
||||
I18N[install_arch_desc3]="ISP only sees HTTPS traffic to %s:443"
|
||||
@@ -152,7 +125,7 @@ I18N[logs_telemt_title]="📋 telemt logs (last %s lines):"
|
||||
# ── Link / Share ────────────────────────────────────────────────────────
|
||||
I18N[link_title]="🔗 Connection link:"
|
||||
I18N[share_title]="📤 Forward this message:"
|
||||
I18N[share_line1]="🔐 MTProxy for Telegram (GoTelegram v%s)"
|
||||
I18N[share_line1]="🔐 MTProxy for Telegram (goTelegram Pro v%s)"
|
||||
I18N[share_server]="🌍 Server: %s"
|
||||
I18N[share_port]="🔌 Port: %s"
|
||||
I18N[share_connect_cta]="👉 Connect with one tap:"
|
||||
@@ -168,7 +141,7 @@ I18N[website_restart_nginx]="Restart nginx"
|
||||
I18N[website_change_template]="Change template"
|
||||
|
||||
# ── Remove ──────────────────────────────────────────────────────────────
|
||||
I18N[remove_title]="🗑 Remove GoTelegram"
|
||||
I18N[remove_title]="🗑 Remove goTelegram Pro"
|
||||
I18N[remove_proxy_only]="Remove proxy only (telemt)"
|
||||
I18N[remove_bot_only]="Remove Telegram bot only"
|
||||
I18N[remove_all]="Remove everything (proxy + bot + settings)"
|
||||
@@ -178,7 +151,7 @@ I18N[remove_backup_before]="Create a backup before removal?"
|
||||
I18N[remove_warn_all]="This will remove EVERYTHING: proxy, bot, site, settings."
|
||||
I18N[remove_confirm_all]="Are you absolutely sure?"
|
||||
I18N[remove_proxy_done]="Proxy removed"
|
||||
I18N[remove_all_done]="GoTelegram fully removed (proxy + bot)"
|
||||
I18N[remove_all_done]="goTelegram Pro fully removed (proxy + bot)"
|
||||
|
||||
# ── Telegram bot submenu ────────────────────────────────────────────────
|
||||
I18N[bot_title]="🤖 Telegram bot"
|
||||
@@ -245,6 +218,7 @@ I18N[bot_access_ids_fmt]="ID: %s"
|
||||
I18N[promo_host1_title]="💰 HOSTING #1 — UP TO 60% OFF"
|
||||
I18N[promo_host2_title]="💰 HOSTING #2 — UP TO 60% OFF"
|
||||
I18N[promo_tips_title]="☕ Donate / Tips"
|
||||
I18N[promo_youtube_title]="▶ YouTube Channel"
|
||||
I18N[promo_link_label]="Link:"
|
||||
I18N[promo_off60]="60%% discount on the first month"
|
||||
I18N[promo_ant20]="20%% + 3%% when paid for 3 months"
|
||||
@@ -252,6 +226,7 @@ I18N[promo_ant6]="15%% + 5%% when paid for 6 months"
|
||||
I18N[promo_qr_host1]="── QR: Hosting #1 ──"
|
||||
I18N[promo_qr_host2]="── QR: Hosting #2 ──"
|
||||
I18N[promo_qr_tips]="── QR: Donate / Tips ──"
|
||||
I18N[promo_qr_youtube]="── QR: YouTube Channel ──"
|
||||
I18N[promo_menu_in]="Menu in %d sec..."
|
||||
|
||||
# ── Stats ───────────────────────────────────────────────────────────────
|
||||
@@ -357,7 +332,7 @@ I18N[backup_lang_label]="Language"
|
||||
I18N[backup_date_label]="Date"
|
||||
I18N[backup_confirm_restore]="Restore configuration? Current settings will be overwritten."
|
||||
I18N[backup_restored_telemt]="telemt config restored"
|
||||
I18N[backup_restored_gotelegram]="GoTelegram config restored"
|
||||
I18N[backup_restored_gotelegram]="goTelegram Pro config restored"
|
||||
I18N[backup_restored_lang]="Interface language restored"
|
||||
I18N[backup_restored_nginx]="nginx config restored"
|
||||
I18N[backup_restored_ssl]="SSL certificates restored"
|
||||
@@ -374,20 +349,6 @@ I18N[backup_pass_short]="Password too short (minimum 6 characters)"
|
||||
I18N[backup_pick_prompt]="Backup number (or path to file)"
|
||||
I18N[backup_not_found]="Backup not found"
|
||||
|
||||
# ── Preflight (v2.4.8) ──────────────────────────────────────────────────
|
||||
I18N[preflight_title]="Preflight: port conflicts detected"
|
||||
I18N[preflight_known]="Known proxy/VPN/web software is using required ports:"
|
||||
I18N[preflight_unknown]="Required ports are held by unknown processes:"
|
||||
I18N[preflight_needed]="GoTelegram requires ports:"
|
||||
I18N[preflight_hint_header]="Recommended actions:"
|
||||
I18N[preflight_hint1]="Stop and remove the conflicting services (systemctl stop ...)"
|
||||
I18N[preflight_hint2]="Or use a clean VPS without other proxies"
|
||||
I18N[preflight_hint3]="Installing on top will most likely fail"
|
||||
I18N[preflight_skip_hint]="Bypass: GOTELEGRAM_SKIP_PREFLIGHT=1 gotelegram"
|
||||
I18N[preflight_proceed]="Continue installation anyway (likely to fail)?"
|
||||
I18N[preflight_forced]="Installation continued despite conflicts — errors likely"
|
||||
I18N[preflight_aborted]="Installation aborted due to port conflicts"
|
||||
|
||||
# ── Errors / misc ───────────────────────────────────────────────────────
|
||||
I18N[err_need_root]="Run the script with sudo / as root"
|
||||
I18N[err_os_unknown]="Failed to detect OS. Linux is required."
|
||||
@@ -401,7 +362,7 @@ I18N[auto_refresh]="Refresh in 30 sec"
|
||||
I18N[deps_installing]="Installing dependencies: %s"
|
||||
|
||||
# ── Migration ───────────────────────────────────────────────────────────
|
||||
I18N[v1_detected]="⚠️ GoTelegram v1 (mtg) installation detected"
|
||||
I18N[v1_detected]="⚠️ goTelegram Pro v1 (mtg) installation detected"
|
||||
I18N[v1_container]="Container: %s"
|
||||
I18N[v1_migration_step]="Migrating from v1 (mtg) to v2 (telemt)"
|
||||
I18N[v1_found_title]="Found v1 (mtg) installation:"
|
||||
@@ -414,68 +375,3 @@ I18N[v1_migration_cancelled]="Migration cancelled. v1 left intact."
|
||||
I18N[v1_stopping]="Stopping v1 container..."
|
||||
I18N[v1_config_saved]="v1 config saved to %s"
|
||||
I18N[v1_port_freed]="v1 stopped. Port %s freed."
|
||||
|
||||
# ── v2.4.9: UBF v2.0 backup + manual secret recovery ─────────────────────
|
||||
I18N[install_source_title]="Installation source"
|
||||
I18N[install_source_choice]="Choose source [1-3]:"
|
||||
I18N[install_menu_new]="Fresh installation"
|
||||
I18N[install_menu_new_desc]="Generate a new key and set up from scratch"
|
||||
I18N[install_menu_restore]="Restore from backup"
|
||||
I18N[install_menu_restore_desc]="Full restore from a .tar.gz[.enc] file"
|
||||
I18N[install_menu_existing_key]="Use existing key"
|
||||
I18N[install_menu_existing_key_desc]="Paste a tg://proxy link or a key manually"
|
||||
I18N[install_hint_pro_mode]="The key contains a domain — this is usually Pro mode"
|
||||
I18N[install_reuse_secret]="Using the provided key"
|
||||
I18N[install_reuse_domain]="Using domain from the key"
|
||||
I18N[install_reuse_port]="Using port from the key"
|
||||
|
||||
I18N[manual_secret_title]="Enter existing key"
|
||||
I18N[manual_secret_help1]="Supported formats:"
|
||||
I18N[manual_secret_prompt]="Paste the key"
|
||||
I18N[manual_secret_empty]="Key is empty"
|
||||
I18N[manual_secret_bad]="Could not parse the key format"
|
||||
I18N[manual_secret_parsed]="Key parsed"
|
||||
|
||||
I18N[backup_id_label]="Backup ID"
|
||||
I18N[backup_file_label]="File"
|
||||
I18N[backup_size_label]="Size"
|
||||
I18N[backup_key_label]="Key in backup (fingerprint)"
|
||||
I18N[backup_format_label]="Format"
|
||||
I18N[backup_mode_label]="Mode"
|
||||
I18N[backup_lang_label]="Language"
|
||||
I18N[backup_date_label]="Date"
|
||||
I18N[backup_label]="Backup"
|
||||
I18N[backup_ssl_included]="SSL certificates included (+ chain + renewal)"
|
||||
I18N[backup_site_included]="Website template included"
|
||||
I18N[backup_bot_included]="Telegram bot config included"
|
||||
I18N[backup_restored_bot]="Telegram bot config restored"
|
||||
I18N[backup_automigrate]="Converting legacy backup to UBF v2.0..."
|
||||
I18N[backup_migrated]="Fresh UBF v2.0 backup saved"
|
||||
I18N[backup_collecting]="Collecting configuration..."
|
||||
I18N[backup_archive_err]="Archive creation failed"
|
||||
I18N[backup_archive_missing]="Archive was not created"
|
||||
I18N[backup_encrypt_err]="Encryption failed"
|
||||
I18N[backup_encrypted]="Backup encrypted (AES-256-CBC)"
|
||||
I18N[backup_created]="Backup created"
|
||||
I18N[backup_enter_pass]="Enter password"
|
||||
I18N[backup_repeat_pass]="Repeat password"
|
||||
I18N[backup_pass_mismatch]="Passwords do not match"
|
||||
I18N[backup_pass_short]="Password too short (min 6 chars)"
|
||||
I18N[backup_bad_pass]="Wrong password or corrupted file"
|
||||
I18N[backup_extract_err]="Archive extraction failed"
|
||||
I18N[backup_confirm_restore]="Restore configuration? Current settings will be overwritten."
|
||||
I18N[backup_restored_telemt]="telemt config restored"
|
||||
I18N[backup_restored_gotelegram]="GoTelegram config restored"
|
||||
I18N[backup_restored_lang]="Interface language restored"
|
||||
I18N[backup_restored_nginx]="nginx config restored"
|
||||
I18N[backup_restored_ssl]="SSL certificates restored"
|
||||
I18N[backup_restored_site]="Website template restored"
|
||||
I18N[backup_restore_done]="Restore completed!"
|
||||
I18N[backup_create_title]="Create backup"
|
||||
I18N[backup_encrypt_prompt]="Encrypt the backup with a password?"
|
||||
I18N[backup_none]="No backups found"
|
||||
I18N[backup_list_title]="Available backups"
|
||||
I18N[backup_pick_prompt]="Backup number (or file path)"
|
||||
I18N[backup_not_found]="Backup not found"
|
||||
I18N[backup_file_not_found_fmt]="File not found: %s"
|
||||
I18N[backup_cleanup_fmt]="Deleted %s old backups (kept %s)"
|
||||
|
||||
126
lib/lang/ru.sh
Normal file → Executable file
126
lib/lang/ru.sh
Normal file → Executable file
@@ -1,5 +1,5 @@
|
||||
#!/bin/bash
|
||||
# GoTelegram v2.4 — Russian translations
|
||||
# goTelegram Pro v2.5.0 — Russian translations
|
||||
# shellcheck disable=SC2034,SC2148
|
||||
|
||||
# ── Common words ────────────────────────────────────────────────────────
|
||||
@@ -25,7 +25,7 @@ I18N[success]="Готово"
|
||||
I18N[wait]="Подождите..."
|
||||
|
||||
# ── Banner ──────────────────────────────────────────────────────────────
|
||||
I18N[banner_title]="GoTelegram v%s"
|
||||
I18N[banner_title]="goTelegram Pro v%s"
|
||||
I18N[banner_subtitle]="MTProxy на ядре telemt (Rust + Tokio)"
|
||||
I18N[banner_features]="Anti-DPI • Fake TLS • TCP Splice • JA3/JA4"
|
||||
I18N[credits_title]="Благодарности / Credits"
|
||||
@@ -44,33 +44,6 @@ I18N[net_mode]="Режим:"
|
||||
I18N[net_domain]="Домен:"
|
||||
I18N[connection_link]="Ссылка для Telegram:"
|
||||
I18N[proxy_not_configured]="Прокси не настроен. Выберите пункт 1."
|
||||
# ── show_proxy_info labels ─────────────────────────────────────────────
|
||||
I18N[info_status_running]="Работает"
|
||||
I18N[info_status_stopped]="Остановлен"
|
||||
I18N[info_status_not_installed]="Не установлен"
|
||||
I18N[info_proxy_status]="Статус прокси"
|
||||
I18N[info_engine]="Ядро"
|
||||
I18N[info_ip]="IP"
|
||||
I18N[info_domain]="Домен"
|
||||
I18N[info_port]="Порт"
|
||||
I18N[info_mode]="Режим"
|
||||
I18N[info_mask]="Маскировка"
|
||||
I18N[info_secret]="Secret"
|
||||
I18N[info_link]="Ссылка"
|
||||
# ── show_traffic_stats labels ──────────────────────────────────────────
|
||||
I18N[stats_sh_proxy]="Proxy (telemt, порт 443)"
|
||||
I18N[stats_sh_site]="Сайт (nginx, порт 8443)"
|
||||
I18N[stats_sh_hdr_period]="Период"
|
||||
I18N[stats_sh_hdr_inbound]="Входящий"
|
||||
I18N[stats_sh_hdr_rate]="Скорость"
|
||||
I18N[stats_sh_packets]="Пакетов"
|
||||
I18N[stats_sh_1min]="1 мин"
|
||||
I18N[stats_sh_5min]="5 мин"
|
||||
I18N[stats_sh_60min]="60 мин"
|
||||
I18N[stats_sh_1day]="1 день"
|
||||
I18N[stats_sh_7days]="7 дней"
|
||||
I18N[stats_sh_30days]="30 дней"
|
||||
I18N[stats_sh_365days]="365 дней"
|
||||
I18N[menu_proxy]="Прокси ▸"
|
||||
I18N[menu_stats]="Статистика ▸"
|
||||
I18N[menu_manage]="Управление ▸"
|
||||
@@ -102,7 +75,7 @@ I18N[submenu_about_title]="ℹ️ О ПРОГРАММЕ"
|
||||
I18N[about_version_info]="Информация о версии"
|
||||
I18N[about_promo]="Промо / Донат"
|
||||
I18N[version_title]="🔍 Информация"
|
||||
I18N[version_label]="GoTelegram:"
|
||||
I18N[version_label]="goTelegram Pro:"
|
||||
I18N[version_engine]="Ядро:"
|
||||
I18N[version_tech]="Технология:"
|
||||
I18N[version_license]="Лицензия:"
|
||||
@@ -133,7 +106,7 @@ I18N[install_cfg_mode]="Режим:"
|
||||
I18N[install_cfg_domain]="Домен:"
|
||||
I18N[install_confirm_proxy]="Установить прокси?"
|
||||
I18N[install_confirm_proxy_site]="Установить прокси + сайт?"
|
||||
I18N[install_done]="GoTelegram v%s установлен! (%s-режим)"
|
||||
I18N[install_done]="goTelegram Pro v%s установлен! (%s-режим)"
|
||||
I18N[install_arch_desc1]="telemt принимает весь трафик на 443 (маскировка под HTTPS)"
|
||||
I18N[install_arch_desc2]="nginx обслуживает сайт на внутреннем порту %s"
|
||||
I18N[install_arch_desc3]="Провайдер видит только HTTPS-трафик к %s:443"
|
||||
@@ -152,7 +125,7 @@ I18N[logs_telemt_title]="📋 Логи telemt (последние %s строк)
|
||||
# ── Link / Share ────────────────────────────────────────────────────────
|
||||
I18N[link_title]="🔗 Ссылка для подключения:"
|
||||
I18N[share_title]="📤 Перешлите это сообщение:"
|
||||
I18N[share_line1]="🔐 MTProxy для Telegram (GoTelegram v%s)"
|
||||
I18N[share_line1]="🔐 MTProxy для Telegram (goTelegram Pro v%s)"
|
||||
I18N[share_server]="🌍 Сервер: %s"
|
||||
I18N[share_port]="🔌 Порт: %s"
|
||||
I18N[share_connect_cta]="👉 Подключиться одним нажатием:"
|
||||
@@ -168,7 +141,7 @@ I18N[website_restart_nginx]="Перезапустить nginx"
|
||||
I18N[website_change_template]="Сменить шаблон"
|
||||
|
||||
# ── Remove ──────────────────────────────────────────────────────────────
|
||||
I18N[remove_title]="🗑 Удаление GoTelegram"
|
||||
I18N[remove_title]="🗑 Удаление goTelegram Pro"
|
||||
I18N[remove_proxy_only]="Удалить только прокси (telemt)"
|
||||
I18N[remove_bot_only]="Удалить только Telegram-бота"
|
||||
I18N[remove_all]="Удалить всё (прокси + бот + настройки)"
|
||||
@@ -178,7 +151,7 @@ I18N[remove_backup_before]="Сделать бекап перед удалени
|
||||
I18N[remove_warn_all]="Это удалит ВСЁ: прокси, бот, сайт, настройки."
|
||||
I18N[remove_confirm_all]="Вы точно уверены?"
|
||||
I18N[remove_proxy_done]="Прокси удалён"
|
||||
I18N[remove_all_done]="GoTelegram полностью удалён (прокси + бот)"
|
||||
I18N[remove_all_done]="goTelegram Pro полностью удалён (прокси + бот)"
|
||||
|
||||
# ── Telegram bot submenu ────────────────────────────────────────────────
|
||||
I18N[bot_title]="🤖 Telegram-бот"
|
||||
@@ -245,6 +218,7 @@ I18N[bot_access_ids_fmt]="ID: %s"
|
||||
I18N[promo_host1_title]="💰 ХОСТИНГ #1 — СКИДКА ДО 60%"
|
||||
I18N[promo_host2_title]="💰 ХОСТИНГ #2 — СКИДКА ДО 60%"
|
||||
I18N[promo_tips_title]="☕ Донат / Чаевые"
|
||||
I18N[promo_youtube_title]="▶ YouTube-канал"
|
||||
I18N[promo_link_label]="Ссылка:"
|
||||
I18N[promo_off60]="60%% скидки на первый месяц"
|
||||
I18N[promo_ant20]="20%% + 3%% при оплате за 3 месяца"
|
||||
@@ -252,6 +226,7 @@ I18N[promo_ant6]="15%% + 5%% при оплате за 6 месяцев"
|
||||
I18N[promo_qr_host1]="── QR: Хостинг #1 ──"
|
||||
I18N[promo_qr_host2]="── QR: Хостинг #2 ──"
|
||||
I18N[promo_qr_tips]="── QR: Чаевые / Донат ──"
|
||||
I18N[promo_qr_youtube]="── QR: YouTube-канал ──"
|
||||
I18N[promo_menu_in]="Меню через %d сек..."
|
||||
|
||||
# ── Stats ───────────────────────────────────────────────────────────────
|
||||
@@ -357,7 +332,7 @@ I18N[backup_lang_label]="Язык"
|
||||
I18N[backup_date_label]="Дата"
|
||||
I18N[backup_confirm_restore]="Восстановить конфигурацию? Текущие настройки будут перезаписаны."
|
||||
I18N[backup_restored_telemt]="telemt конфиг восстановлен"
|
||||
I18N[backup_restored_gotelegram]="GoTelegram конфиг восстановлен"
|
||||
I18N[backup_restored_gotelegram]="goTelegram Pro конфиг восстановлен"
|
||||
I18N[backup_restored_lang]="Язык интерфейса восстановлен"
|
||||
I18N[backup_restored_nginx]="nginx конфиг восстановлен"
|
||||
I18N[backup_restored_ssl]="SSL сертификаты восстановлены"
|
||||
@@ -374,20 +349,6 @@ I18N[backup_pass_short]="Пароль слишком короткий (мини
|
||||
I18N[backup_pick_prompt]="Номер бекапа (или путь к файлу)"
|
||||
I18N[backup_not_found]="Бекап не найден"
|
||||
|
||||
# ── Preflight (v2.4.8) ──────────────────────────────────────────────────
|
||||
I18N[preflight_title]="Предустановочная проверка: обнаружены конфликты портов"
|
||||
I18N[preflight_known]="Известный proxy/VPN/веб-софт занимает нужные порты:"
|
||||
I18N[preflight_unknown]="Порты заняты неизвестными процессами:"
|
||||
I18N[preflight_needed]="GoTelegram нужны порты:"
|
||||
I18N[preflight_hint_header]="Рекомендации:"
|
||||
I18N[preflight_hint1]="Остановите и удалите конфликтующие сервисы (systemctl stop ...)"
|
||||
I18N[preflight_hint2]="Либо возьмите чистый VPS без других прокси"
|
||||
I18N[preflight_hint3]="Установка поверх, скорее всего, завершится некорректно"
|
||||
I18N[preflight_skip_hint]="Обойти проверку: GOTELEGRAM_SKIP_PREFLIGHT=1 gotelegram"
|
||||
I18N[preflight_proceed]="Продолжить установку всё равно (скорее всего не заработает)?"
|
||||
I18N[preflight_forced]="Установка продолжена вопреки конфликтам — возможны ошибки"
|
||||
I18N[preflight_aborted]="Установка отменена из-за конфликтов портов"
|
||||
|
||||
# ── Errors / misc ───────────────────────────────────────────────────────
|
||||
I18N[err_need_root]="Запустите скрипт с sudo / от root"
|
||||
I18N[err_os_unknown]="Не удалось определить ОС. Требуется Linux."
|
||||
@@ -401,7 +362,7 @@ I18N[auto_refresh]="Обновление через 30 сек"
|
||||
I18N[deps_installing]="Установка зависимостей: %s"
|
||||
|
||||
# ── Migration ───────────────────────────────────────────────────────────
|
||||
I18N[v1_detected]="⚠️ Обнаружена установка GoTelegram v1 (mtg)"
|
||||
I18N[v1_detected]="⚠️ Обнаружена установка goTelegram Pro v1 (mtg)"
|
||||
I18N[v1_container]="Контейнер: %s"
|
||||
I18N[v1_migration_step]="Миграция с v1 (mtg) на v2 (telemt)"
|
||||
I18N[v1_found_title]="Найдена установка v1 (mtg):"
|
||||
@@ -414,68 +375,3 @@ I18N[v1_migration_cancelled]="Миграция отменена. v1 оставл
|
||||
I18N[v1_stopping]="Остановка v1 контейнера..."
|
||||
I18N[v1_config_saved]="Конфиг v1 сохранён в %s"
|
||||
I18N[v1_port_freed]="v1 остановлен. Порт %s освобождён."
|
||||
|
||||
# ── v2.4.9: UBF v2.0 backup + manual secret recovery ─────────────────────
|
||||
I18N[install_source_title]="Источник установки"
|
||||
I18N[install_source_choice]="Выберите источник [1-3]:"
|
||||
I18N[install_menu_new]="Новая установка"
|
||||
I18N[install_menu_new_desc]="Сгенерировать новый ключ и настроить с нуля"
|
||||
I18N[install_menu_restore]="Восстановить из бекапа"
|
||||
I18N[install_menu_restore_desc]="Полное восстановление из файла .tar.gz[.enc]"
|
||||
I18N[install_menu_existing_key]="Использовать существующий ключ"
|
||||
I18N[install_menu_existing_key_desc]="Ввести ссылку tg://proxy или ключ вручную"
|
||||
I18N[install_hint_pro_mode]="Ключ содержит домен — обычно это Pro режим"
|
||||
I18N[install_reuse_secret]="Используется переданный ключ"
|
||||
I18N[install_reuse_domain]="Используется домен из ключа"
|
||||
I18N[install_reuse_port]="Используется порт из ключа"
|
||||
|
||||
I18N[manual_secret_title]="Ввод существующего ключа"
|
||||
I18N[manual_secret_help1]="Поддерживаются форматы:"
|
||||
I18N[manual_secret_prompt]="Вставьте ключ"
|
||||
I18N[manual_secret_empty]="Ключ не введён"
|
||||
I18N[manual_secret_bad]="Не удалось распознать формат ключа"
|
||||
I18N[manual_secret_parsed]="Ключ распознан"
|
||||
|
||||
I18N[backup_id_label]="Backup ID"
|
||||
I18N[backup_file_label]="Файл"
|
||||
I18N[backup_size_label]="Размер"
|
||||
I18N[backup_key_label]="Ключ в бекапе (fingerprint)"
|
||||
I18N[backup_format_label]="Формат"
|
||||
I18N[backup_mode_label]="Режим"
|
||||
I18N[backup_lang_label]="Язык"
|
||||
I18N[backup_date_label]="Дата"
|
||||
I18N[backup_label]="Бекап"
|
||||
I18N[backup_ssl_included]="SSL-сертификаты включены (+ chain + renewal)"
|
||||
I18N[backup_site_included]="Шаблон сайта включён"
|
||||
I18N[backup_bot_included]="Конфиг Telegram-бота включён"
|
||||
I18N[backup_restored_bot]="Конфиг Telegram-бота восстановлен"
|
||||
I18N[backup_automigrate]="Конвертирую старый бекап в UBF v2.0..."
|
||||
I18N[backup_migrated]="Свежий UBF v2.0 бекап сохранён"
|
||||
I18N[backup_collecting]="Собираю конфигурацию..."
|
||||
I18N[backup_archive_err]="Ошибка создания архива"
|
||||
I18N[backup_archive_missing]="Архив не создан"
|
||||
I18N[backup_encrypt_err]="Ошибка шифрования"
|
||||
I18N[backup_encrypted]="Бекап зашифрован (AES-256-CBC)"
|
||||
I18N[backup_created]="Бекап создан"
|
||||
I18N[backup_enter_pass]="Введите пароль"
|
||||
I18N[backup_repeat_pass]="Повторите пароль"
|
||||
I18N[backup_pass_mismatch]="Пароли не совпадают"
|
||||
I18N[backup_pass_short]="Пароль слишком короткий (минимум 6 символов)"
|
||||
I18N[backup_bad_pass]="Неверный пароль или повреждённый файл"
|
||||
I18N[backup_extract_err]="Ошибка распаковки архива"
|
||||
I18N[backup_confirm_restore]="Восстановить конфигурацию? Текущие настройки будут перезаписаны."
|
||||
I18N[backup_restored_telemt]="telemt конфиг восстановлен"
|
||||
I18N[backup_restored_gotelegram]="GoTelegram конфиг восстановлен"
|
||||
I18N[backup_restored_lang]="Язык интерфейса восстановлен"
|
||||
I18N[backup_restored_nginx]="nginx конфиг восстановлен"
|
||||
I18N[backup_restored_ssl]="SSL сертификаты восстановлены"
|
||||
I18N[backup_restored_site]="Шаблон сайта восстановлен"
|
||||
I18N[backup_restore_done]="Восстановление завершено!"
|
||||
I18N[backup_create_title]="Создание бекапа"
|
||||
I18N[backup_encrypt_prompt]="Зашифровать бекап паролем?"
|
||||
I18N[backup_none]="Бекапов нет"
|
||||
I18N[backup_list_title]="Доступные бекапы"
|
||||
I18N[backup_pick_prompt]="Номер бекапа (или путь к файлу)"
|
||||
I18N[backup_not_found]="Бекап не найден"
|
||||
I18N[backup_file_not_found_fmt]="Файл не найден: %s"
|
||||
I18N[backup_cleanup_fmt]="Удалено %s старых бекапов (оставлено %s)"
|
||||
|
||||
248
lib/shared443.sh
Normal file
248
lib/shared443.sh
Normal file
@@ -0,0 +1,248 @@
|
||||
#!/bin/bash
|
||||
# goTelegram Pro v2.5.0 — shared TCP/443 dispatcher helpers
|
||||
|
||||
SHARED443_CONFIG="${SHARED443_CONFIG:-/opt/gotelegram/shared-443.json}"
|
||||
SHARED443_STREAM_CONF="${SHARED443_STREAM_CONF:-/etc/nginx/stream-conf.d/gotelegram-shared443.conf}"
|
||||
SHARED443_TELEMT_PORT="${SHARED443_TELEMT_PORT:-7443}"
|
||||
SHARED443_PUBLIC_PORT="${SHARED443_PUBLIC_PORT:-443}"
|
||||
|
||||
SHARED443_LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
type log_error >/dev/null 2>&1 || source "$SHARED443_LIB_DIR/common.sh"
|
||||
type install_nginx >/dev/null 2>&1 || source "$SHARED443_LIB_DIR/website.sh"
|
||||
|
||||
shared443_detect_nginx_stream() {
|
||||
nginx -V 2>&1 | grep -Eq -- '--with-stream|ngx_stream_module|ngx_stream_ssl_preread_module'
|
||||
}
|
||||
|
||||
shared443_install_stream_module() {
|
||||
if shared443_detect_nginx_stream; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
case "$(get_pkg_manager 2>/dev/null || echo unknown)" in
|
||||
apt)
|
||||
apt_update >/dev/null 2>&1 || true
|
||||
apt_install libnginx-mod-stream || return 1
|
||||
;;
|
||||
dnf|yum)
|
||||
install_pkg nginx-mod-stream || true
|
||||
;;
|
||||
esac
|
||||
|
||||
shared443_detect_nginx_stream
|
||||
}
|
||||
|
||||
shared443_ensure_nginx_include() {
|
||||
mkdir -p /etc/nginx/stream-conf.d
|
||||
if nginx -T 2>/dev/null | grep -q '/etc/nginx/stream-conf.d/\*.conf'; then
|
||||
return 0
|
||||
fi
|
||||
if grep -Eq '^[[:space:]]*stream[[:space:]]*\{' /etc/nginx/nginx.conf 2>/dev/null; then
|
||||
log_warning "В nginx уже есть stream-блок, но нет include /etc/nginx/stream-conf.d/*.conf"
|
||||
log_dim "Добавьте include вручную или перенесите $SHARED443_STREAM_CONF в существующий stream-блок."
|
||||
return 1
|
||||
fi
|
||||
|
||||
cp /etc/nginx/nginx.conf "/etc/nginx/nginx.conf.gotelegram.$(date +%Y%m%d_%H%M%S).bak" 2>/dev/null || true
|
||||
cat >> /etc/nginx/nginx.conf <<'EOF'
|
||||
|
||||
# goTelegram Pro shared TCP/443 routes
|
||||
stream {
|
||||
include /etc/nginx/stream-conf.d/*.conf;
|
||||
}
|
||||
EOF
|
||||
}
|
||||
|
||||
shared443_rewrite_telemt_bind() {
|
||||
local listen_port="${1:-$SHARED443_TELEMT_PORT}"
|
||||
local public_port="${2:-$SHARED443_PUBLIC_PORT}"
|
||||
local listen_addr="${3:-127.0.0.1}"
|
||||
|
||||
command -v python3 >/dev/null 2>&1 || {
|
||||
log_error "python3 нужен для безопасного изменения $TELEMT_CONFIG"
|
||||
return 1
|
||||
}
|
||||
|
||||
python3 - "$TELEMT_CONFIG" "$listen_port" "$public_port" "$listen_addr" <<'PY'
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
path = Path(sys.argv[1])
|
||||
listen_port = sys.argv[2]
|
||||
public_port = sys.argv[3]
|
||||
listen_addr = sys.argv[4]
|
||||
lines = path.read_text(encoding="utf-8", errors="ignore").splitlines() if path.exists() else []
|
||||
out = []
|
||||
section = ""
|
||||
server_seen = False
|
||||
server_port_seen = False
|
||||
server_addr_seen = False
|
||||
links_seen = False
|
||||
public_seen = False
|
||||
|
||||
def flush_section(next_line=None):
|
||||
global section, server_port_seen, server_addr_seen, public_seen
|
||||
if section == "server":
|
||||
if not server_port_seen:
|
||||
out.append(f"port = {listen_port}")
|
||||
if not server_addr_seen:
|
||||
out.append(f'listen_addr_ipv4 = "{listen_addr}"')
|
||||
if section == "general.links" and not public_seen:
|
||||
out.append(f"public_port = {public_port}")
|
||||
if next_line is not None:
|
||||
out.append(next_line)
|
||||
|
||||
for raw in lines:
|
||||
stripped = raw.strip()
|
||||
if stripped.startswith("[") and stripped.endswith("]"):
|
||||
flush_section(raw)
|
||||
section = stripped.strip("[]")
|
||||
if section == "server":
|
||||
server_seen = True
|
||||
server_port_seen = False
|
||||
server_addr_seen = False
|
||||
elif section == "general.links":
|
||||
links_seen = True
|
||||
public_seen = False
|
||||
continue
|
||||
|
||||
if section == "server" and stripped.startswith("port") and "=" in stripped:
|
||||
out.append(f"port = {listen_port}")
|
||||
server_port_seen = True
|
||||
continue
|
||||
if section == "server" and stripped.startswith("listen_addr_ipv4") and "=" in stripped:
|
||||
out.append(f'listen_addr_ipv4 = "{listen_addr}"')
|
||||
server_addr_seen = True
|
||||
continue
|
||||
if section == "general.links" and stripped.startswith("public_port") and "=" in stripped:
|
||||
out.append(f"public_port = {public_port}")
|
||||
public_seen = True
|
||||
continue
|
||||
out.append(raw)
|
||||
|
||||
flush_section()
|
||||
if not links_seen:
|
||||
if out and out[-1].strip():
|
||||
out.append("")
|
||||
out.extend(["[general.links]", f"public_port = {public_port}"])
|
||||
if not server_seen:
|
||||
if out and out[-1].strip():
|
||||
out.append("")
|
||||
out.extend(["[server]", f"port = {listen_port}", f'listen_addr_ipv4 = "{listen_addr}"'])
|
||||
|
||||
tmp = path.with_suffix(path.suffix + ".tmp")
|
||||
tmp.write_text("\n".join(out).rstrip() + "\n", encoding="utf-8")
|
||||
tmp.chmod(0o600)
|
||||
tmp.replace(path)
|
||||
PY
|
||||
}
|
||||
|
||||
shared443_write_stream_config() {
|
||||
local domain="$1"
|
||||
local xray_domain="${2:-}"
|
||||
local xray_target="${3:-}"
|
||||
local telemt_target="${4:-127.0.0.1:${SHARED443_TELEMT_PORT}}"
|
||||
|
||||
mkdir -p "$(dirname "$SHARED443_STREAM_CONF")"
|
||||
{
|
||||
echo "# goTelegram Pro shared TCP/443 dispatcher"
|
||||
echo "# Browser/Telegram for goTelegram domain goes to telemt; telemt masks the site to nginx."
|
||||
echo "map \$ssl_preread_server_name \$gotelegram_shared443_backend {"
|
||||
echo " hostnames;"
|
||||
if [[ -n "$xray_domain" && -n "$xray_target" ]]; then
|
||||
echo " ${xray_domain} ${xray_target};"
|
||||
fi
|
||||
echo " default ${telemt_target};"
|
||||
echo "}"
|
||||
echo ""
|
||||
echo "server {"
|
||||
echo " listen 0.0.0.0:${SHARED443_PUBLIC_PORT};"
|
||||
echo " proxy_pass \$gotelegram_shared443_backend;"
|
||||
echo " ssl_preread on;"
|
||||
echo " proxy_connect_timeout 5s;"
|
||||
echo " proxy_timeout 10m;"
|
||||
echo "}"
|
||||
} > "$SHARED443_STREAM_CONF"
|
||||
|
||||
mkdir -p "$(dirname "$SHARED443_CONFIG")"
|
||||
if command -v jq >/dev/null 2>&1; then
|
||||
jq -n \
|
||||
--arg domain "$domain" \
|
||||
--arg telemt "$telemt_target" \
|
||||
--arg xdomain "$xray_domain" \
|
||||
--arg xtarget "$xray_target" \
|
||||
--arg updated "$(date -Iseconds)" \
|
||||
--argjson public_port "$SHARED443_PUBLIC_PORT" \
|
||||
'{
|
||||
enabled: true,
|
||||
dispatcher: "nginx-stream",
|
||||
public_port: $public_port,
|
||||
domain: $domain,
|
||||
telemt_target: $telemt,
|
||||
site_target: "127.0.0.1:8443",
|
||||
xray_routes: (if ($xdomain != "" and $xtarget != "") then [{public: ($xdomain + ":443"), target: $xtarget}] else [] end),
|
||||
updated_at: $updated
|
||||
}' > "$SHARED443_CONFIG"
|
||||
else
|
||||
cat > "$SHARED443_CONFIG" <<EOF
|
||||
{"enabled":true,"dispatcher":"nginx-stream","public_port":${SHARED443_PUBLIC_PORT},"domain":"${domain}","telemt_target":"${telemt_target}","site_target":"127.0.0.1:8443","xray_routes":[],"updated_at":"$(date -Iseconds)"}
|
||||
EOF
|
||||
fi
|
||||
chmod 600 "$SHARED443_CONFIG" 2>/dev/null || true
|
||||
}
|
||||
|
||||
shared443_enable() {
|
||||
local domain="$1"
|
||||
local xray_domain="${2:-}"
|
||||
local xray_target="${3:-}"
|
||||
local telemt_target="127.0.0.1:${SHARED443_TELEMT_PORT}"
|
||||
|
||||
[[ -n "$domain" ]] || domain="$(config_get domain 2>/dev/null || echo "")"
|
||||
[[ -n "$domain" ]] || {
|
||||
log_error "Не указан домен goTelegram Pro для shared-443"
|
||||
return 1
|
||||
}
|
||||
|
||||
install_nginx || return 1
|
||||
shared443_install_stream_module || {
|
||||
log_error "nginx stream/ssl_preread недоступен"
|
||||
return 1
|
||||
}
|
||||
shared443_ensure_nginx_include || return 1
|
||||
|
||||
shared443_rewrite_telemt_bind "$SHARED443_TELEMT_PORT" "$SHARED443_PUBLIC_PORT" "127.0.0.1" || return 1
|
||||
systemctl restart "$TELEMT_SERVICE" 2>/dev/null || true
|
||||
|
||||
shared443_write_stream_config "$domain" "$xray_domain" "$xray_target" "$telemt_target"
|
||||
if nginx -t 2>/dev/null; then
|
||||
systemctl restart nginx
|
||||
log_success "shared-443 включён: 0.0.0.0:${SHARED443_PUBLIC_PORT} -> nginx stream -> telemt ${telemt_target}"
|
||||
if [[ -n "$xray_domain" && -n "$xray_target" ]]; then
|
||||
log_success "Xray route: ${xray_domain}:443 -> ${xray_target}"
|
||||
fi
|
||||
else
|
||||
log_error "nginx -t не прошёл после настройки shared-443"
|
||||
nginx -t
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
shared443_detect_direct_conflict() {
|
||||
ss -ltnp 2>/dev/null | grep -E '(:|])443[[:space:]]' | grep -Eiv '(nginx|telemt)' || true
|
||||
}
|
||||
|
||||
shared443_status() {
|
||||
echo "shared-443 config: $SHARED443_CONFIG"
|
||||
[ -f "$SHARED443_CONFIG" ] && cat "$SHARED443_CONFIG" || echo "not enabled"
|
||||
local conflict
|
||||
conflict="$(shared443_detect_direct_conflict)"
|
||||
if [[ -n "$conflict" ]]; then
|
||||
echo ""
|
||||
echo "direct 443 listeners that need migration behind dispatcher:"
|
||||
echo "$conflict"
|
||||
fi
|
||||
}
|
||||
|
||||
export -f shared443_detect_nginx_stream shared443_install_stream_module shared443_ensure_nginx_include
|
||||
export -f shared443_rewrite_telemt_bind shared443_write_stream_config shared443_enable
|
||||
export -f shared443_detect_direct_conflict shared443_status
|
||||
260
lib/stats.sh
260
lib/stats.sh
@@ -1,5 +1,5 @@
|
||||
#!/bin/bash
|
||||
# stats.sh — Traffic statistics module for GoTelegram
|
||||
# stats.sh — Traffic statistics module for GoTelegram v2.5.0
|
||||
# Tracks proxy (telemt port 443) and site (nginx port 8443) traffic
|
||||
# Uses iptables counters + real-time snapshots + historical CSV
|
||||
|
||||
@@ -12,12 +12,24 @@ NC='\033[0m' # No Color
|
||||
|
||||
STATS_DIR="/run/gotelegram"
|
||||
HISTORY_FILE="/opt/gotelegram/stats_history.csv"
|
||||
USER_HISTORY_FILE="/opt/gotelegram/user_stats_history.csv"
|
||||
SNAPSHOTS_DIR="$STATS_DIR/snapshots"
|
||||
CURRENT_SNAPSHOT="$STATS_DIR/stats_current.json"
|
||||
CONFIG_FILE="/opt/gotelegram/config.json"
|
||||
TELEMT_CONFIG_FILE="/etc/telemt/config.toml"
|
||||
STATS_RETENTION_DAYS="${STATS_RETENTION_DAYS:-365}"
|
||||
STATS_MINUTE_RETENTION_DAYS="${STATS_MINUTE_RETENTION_DAYS:-31}"
|
||||
STATS_CLEANUP_INTERVAL="${STATS_CLEANUP_INTERVAL:-3600}"
|
||||
STATS_CLEANUP_STAMP="$STATS_DIR/last_history_cleanup"
|
||||
USER_STATS_COLLECT_STAMP="$STATS_DIR/last_user_stats_minute"
|
||||
|
||||
# Initialize stats infrastructure
|
||||
stats_init() {
|
||||
if ! command -v iptables &>/dev/null; then
|
||||
log_warning "iptables не найден: установите пакет iptables или запустите установку зависимостей"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Create runtime directory
|
||||
mkdir -p "$STATS_DIR" "$SNAPSHOTS_DIR" 2>/dev/null
|
||||
chmod 755 "$STATS_DIR" "$SNAPSHOTS_DIR" 2>/dev/null
|
||||
@@ -46,6 +58,9 @@ stats_init() {
|
||||
if [[ ! -f "$HISTORY_FILE" ]]; then
|
||||
echo "epoch,proxy_bytes,site_bytes" > "$HISTORY_FILE" 2>/dev/null
|
||||
fi
|
||||
if [[ ! -f "$USER_HISTORY_FILE" ]]; then
|
||||
echo "epoch,user,total_octets,current_connections,active_unique_ips,recent_unique_ips" > "$USER_HISTORY_FILE" 2>/dev/null
|
||||
fi
|
||||
|
||||
# Write initial snapshot
|
||||
stats_collect
|
||||
@@ -57,6 +72,13 @@ stats_collect() {
|
||||
local ts=$(date +%s)
|
||||
local temp_file=$(mktemp)
|
||||
|
||||
if ! command -v iptables &>/dev/null; then
|
||||
mkdir -p "$STATS_DIR" 2>/dev/null
|
||||
echo "{\"ts\":$ts,\"proxy_bytes\":0,\"proxy_pkts\":0,\"site_bytes\":0,\"site_pkts\":0,\"error\":\"iptables_missing\"}" > "$CURRENT_SNAPSHOT" 2>/dev/null
|
||||
rm -f "$temp_file" 2>/dev/null
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Parse iptables output: format is "pkts bytes target"
|
||||
# We need to extract bytes (2nd column) for each rule
|
||||
local iptables_output=$(iptables -L GOTELEGRAM_STATS -v -n -x 2>/dev/null)
|
||||
@@ -107,14 +129,76 @@ EOF
|
||||
if [[ "$last_ts" -eq 0 ]] || [[ $((current_minute - last_ts)) -ge 60 ]]; then
|
||||
echo "$current_minute,$proxy_bytes,$site_bytes" >> "$HISTORY_FILE" 2>/dev/null
|
||||
|
||||
# Cleanup old entries (keep only 365 days)
|
||||
# Cleanup/compact history at most once per hour.
|
||||
stats_cleanup_history
|
||||
fi
|
||||
fi
|
||||
|
||||
stats_collect_users "$ts"
|
||||
|
||||
rm -f "$temp_file" 2>/dev/null
|
||||
}
|
||||
|
||||
# Print active telemt usernames from [access.users]. Usernames are restricted by
|
||||
# goTelegram to A-Z/a-z/0-9/_.- so they are safe in URLs and CSV fields.
|
||||
stats_active_users() {
|
||||
[[ -f "$TELEMT_CONFIG_FILE" ]] || return 0
|
||||
awk '
|
||||
/^\[access\.users\]$/ { in_users=1; next }
|
||||
in_users && /^\[/ { exit }
|
||||
in_users && /^[[:space:]]*#/ { next }
|
||||
in_users && /=/ {
|
||||
key=$1
|
||||
gsub(/^[[:space:]]+|[[:space:]]+$/, "", key)
|
||||
gsub(/^"|"$/, "", key)
|
||||
if (key ~ /^[A-Za-z0-9_.-]{1,48}$/) print key
|
||||
}
|
||||
' "$TELEMT_CONFIG_FILE" 2>/dev/null
|
||||
}
|
||||
|
||||
stats_collect_users() {
|
||||
local ts="${1:-$(date +%s)}"
|
||||
local current_minute=$((ts - (ts % 60)))
|
||||
|
||||
mkdir -p "$(dirname "$USER_HISTORY_FILE")" 2>/dev/null
|
||||
if [[ ! -f "$USER_HISTORY_FILE" ]]; then
|
||||
echo "epoch,user,total_octets,current_connections,active_unique_ips,recent_unique_ips" > "$USER_HISTORY_FILE" 2>/dev/null
|
||||
fi
|
||||
|
||||
command -v curl &>/dev/null || return 0
|
||||
command -v jq &>/dev/null || return 0
|
||||
|
||||
if [[ -f "$USER_STATS_COLLECT_STAMP" ]] && [[ "$(cat "$USER_STATS_COLLECT_STAMP" 2>/dev/null)" == "$current_minute" ]]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
local existing_users=""
|
||||
existing_users=$(awk -F, -v ts="$current_minute" '$1 == ts { print $2 }' "$USER_HISTORY_FILE" 2>/dev/null || true)
|
||||
|
||||
local user payload total conns active_ips recent_ips
|
||||
while IFS= read -r user; do
|
||||
[[ -n "$user" ]] || continue
|
||||
if printf '%s\n' "$existing_users" | grep -Fxq "$user"; then
|
||||
continue
|
||||
fi
|
||||
|
||||
payload=$(curl -sS --max-time 2 "http://127.0.0.1:9091/v1/users/${user}" 2>/dev/null || true)
|
||||
[[ -n "$payload" ]] || continue
|
||||
total=$(echo "$payload" | jq -r '.data.total_octets // .total_octets // 0' 2>/dev/null)
|
||||
conns=$(echo "$payload" | jq -r '.data.current_connections // .current_connections // 0' 2>/dev/null)
|
||||
active_ips=$(echo "$payload" | jq -r '.data.active_unique_ips // .active_unique_ips // 0' 2>/dev/null)
|
||||
recent_ips=$(echo "$payload" | jq -r '.data.recent_unique_ips // .recent_unique_ips // 0' 2>/dev/null)
|
||||
[[ "$total" =~ ^[0-9]+$ ]] || total=0
|
||||
[[ "$conns" =~ ^[0-9]+$ ]] || conns=0
|
||||
[[ "$active_ips" =~ ^[0-9]+$ ]] || active_ips=0
|
||||
[[ "$recent_ips" =~ ^[0-9]+$ ]] || recent_ips=0
|
||||
echo "$current_minute,$user,$total,$conns,$active_ips,$recent_ips" >> "$USER_HISTORY_FILE" 2>/dev/null
|
||||
done < <(stats_active_users)
|
||||
|
||||
echo "$current_minute" > "$USER_STATS_COLLECT_STAMP" 2>/dev/null || true
|
||||
stats_cleanup_user_history
|
||||
}
|
||||
|
||||
# Read current snapshot as JSON
|
||||
stats_read_current() {
|
||||
if [[ -f "$CURRENT_SNAPSHOT" ]]; then
|
||||
@@ -243,72 +327,137 @@ show_traffic_stats() {
|
||||
local site_rates=$(stats_calculate_rates "site")
|
||||
IFS='|' read -r s1m s1mr s5m s5mr s60m s60mr s1d s1dr s7d s7dr s30d s30dr s365d s365dr <<< "$site_rates"
|
||||
|
||||
# i18n labels (fall back to English if t() not loaded)
|
||||
local lbl_proxy; lbl_proxy="$(_t_or stats_sh_proxy 'Proxy (telemt, port 443)')"
|
||||
local lbl_site; lbl_site="$(_t_or stats_sh_site 'Site (nginx, port 8443)')"
|
||||
local lbl_hdr; lbl_hdr="$(_t_or stats_sh_hdr_period 'Period') │ $(_t_or stats_sh_hdr_inbound 'Inbound') │ $(_t_or stats_sh_hdr_rate 'Rate')"
|
||||
local lbl_pkts; lbl_pkts="$(_t_or stats_sh_packets 'Packets')"
|
||||
local l1m; l1m="$(_t_or stats_sh_1min '1 min')"
|
||||
local l5m; l5m="$(_t_or stats_sh_5min '5 min')"
|
||||
local l60m; l60m="$(_t_or stats_sh_60min '60 min')"
|
||||
local l1d; l1d="$(_t_or stats_sh_1day '1 day')"
|
||||
local l7d; l7d="$(_t_or stats_sh_7days '7 days')"
|
||||
local l30d; l30d="$(_t_or stats_sh_30days '30 days')"
|
||||
local l365d; l365d="$(_t_or stats_sh_365days '365 days')"
|
||||
|
||||
# Display proxy stats
|
||||
{
|
||||
echo ""
|
||||
echo -e "${BLUE} ${lbl_proxy}:${NC}"
|
||||
echo -e "${BLUE} Proxy (telemt, порт 443):${NC}"
|
||||
echo -e "${BLUE} ─────────────────────────────────────────${NC}"
|
||||
echo -e "${BLUE} ${lbl_hdr}${NC}"
|
||||
echo -e "${BLUE} Период │ Входящий │ Скорость${NC}"
|
||||
echo -e "${BLUE} ─────────────────────────────────────────${NC}"
|
||||
printf " %-9s │ %14s │ %s\n" "$l1m" "$p1m" "$p1mr"
|
||||
printf " %-9s │ %14s │ %s\n" "$l5m" "$p5m" "$p5mr"
|
||||
printf " %-9s │ %14s │ %s\n" "$l60m" "$p60m" "$p60mr"
|
||||
printf " %-9s │ %14s │ %s\n" "$l1d" "$p1d" "$p1dr"
|
||||
printf " %-9s │ %14s │ %s\n" "$l7d" "$p7d" "$p7dr"
|
||||
printf " %-9s │ %14s │ %s\n" "$l30d" "$p30d" "$p30dr"
|
||||
printf " %-9s │ %14s │ %s\n" "$l365d" "$p365d" "$p365dr"
|
||||
printf " %-9s │ %14s │ %s\n" "1 мин" "$p1m" "$p1mr"
|
||||
printf " %-9s │ %14s │ %s\n" "5 мин" "$p5m" "$p5mr"
|
||||
printf " %-9s │ %14s │ %s\n" "60 мин" "$p60m" "$p60mr"
|
||||
printf " %-9s │ %14s │ %s\n" "1 день" "$p1d" "$p1dr"
|
||||
printf " %-9s │ %14s │ %s\n" "7 дней" "$p7d" "$p7dr"
|
||||
printf " %-9s │ %14s │ %s\n" "30 дней" "$p30d" "$p30dr"
|
||||
printf " %-9s │ %14s │ %s\n" "365 дней" "$p365d" "$p365dr"
|
||||
echo -e "${BLUE} ─────────────────────────────────────────${NC}"
|
||||
printf " %s: %d\n\n" "$lbl_pkts" "$proxy_pkts"
|
||||
printf " Пакетов: %d\n\n" "$proxy_pkts"
|
||||
|
||||
echo -e "${BLUE} ${lbl_site}:${NC}"
|
||||
echo -e "${BLUE} Сайт (nginx, порт 8443):${NC}"
|
||||
echo -e "${BLUE} ─────────────────────────────────────────${NC}"
|
||||
echo -e "${BLUE} ${lbl_hdr}${NC}"
|
||||
echo -e "${BLUE} Период │ Входящий │ Скорость${NC}"
|
||||
echo -e "${BLUE} ─────────────────────────────────────────${NC}"
|
||||
printf " %-9s │ %14s │ %s\n" "$l1m" "$s1m" "$s1mr"
|
||||
printf " %-9s │ %14s │ %s\n" "$l5m" "$s5m" "$s5mr"
|
||||
printf " %-9s │ %14s │ %s\n" "$l60m" "$s60m" "$s60mr"
|
||||
printf " %-9s │ %14s │ %s\n" "$l1d" "$s1d" "$s1dr"
|
||||
printf " %-9s │ %14s │ %s\n" "$l7d" "$s7d" "$s7dr"
|
||||
printf " %-9s │ %14s │ %s\n" "$l30d" "$s30d" "$s30dr"
|
||||
printf " %-9s │ %14s │ %s\n" "$l365d" "$s365d" "$s365dr"
|
||||
printf " %-9s │ %14s │ %s\n" "1 мин" "$s1m" "$s1mr"
|
||||
printf " %-9s │ %14s │ %s\n" "5 мин" "$s5m" "$s5mr"
|
||||
printf " %-9s │ %14s │ %s\n" "60 мин" "$s60m" "$s60mr"
|
||||
printf " %-9s │ %14s │ %s\n" "1 день" "$s1d" "$s1dr"
|
||||
printf " %-9s │ %14s │ %s\n" "7 дней" "$s7d" "$s7dr"
|
||||
printf " %-9s │ %14s │ %s\n" "30 дней" "$s30d" "$s30dr"
|
||||
printf " %-9s │ %14s │ %s\n" "365 дней" "$s365d" "$s365dr"
|
||||
echo -e "${BLUE} ─────────────────────────────────────────${NC}"
|
||||
printf " %s: %d\n" "$lbl_pkts" "$site_pkts"
|
||||
printf " Пакетов: %d\n" "$site_pkts"
|
||||
echo ""
|
||||
} >&2
|
||||
}
|
||||
|
||||
# Clean up history older than 365 days
|
||||
_stats_positive_int() {
|
||||
local value="${1:-0}"
|
||||
[[ "$value" =~ ^[0-9]+$ ]] && [[ "$value" -gt 0 ]] && echo "$value" || echo "$2"
|
||||
}
|
||||
|
||||
stats_should_cleanup() {
|
||||
local stamp="$1"
|
||||
local now last interval
|
||||
mkdir -p "$STATS_DIR" 2>/dev/null || true
|
||||
now=$(date +%s)
|
||||
interval=$(_stats_positive_int "$STATS_CLEANUP_INTERVAL" 3600)
|
||||
last=$(cat "$stamp" 2>/dev/null || echo 0)
|
||||
[[ "$last" =~ ^[0-9]+$ ]] || last=0
|
||||
if (( now - last < interval )); then
|
||||
return 1
|
||||
fi
|
||||
echo "$now" > "$stamp" 2>/dev/null || true
|
||||
return 0
|
||||
}
|
||||
|
||||
stats_retention_cutoffs() {
|
||||
local now retention_days minute_days
|
||||
now=$(date +%s)
|
||||
retention_days=$(_stats_positive_int "$STATS_RETENTION_DAYS" 365)
|
||||
minute_days=$(_stats_positive_int "$STATS_MINUTE_RETENTION_DAYS" 31)
|
||||
if (( minute_days > retention_days )); then
|
||||
minute_days="$retention_days"
|
||||
fi
|
||||
echo "$((now - retention_days * 86400)) $((now - minute_days * 86400))"
|
||||
}
|
||||
|
||||
# Keep history for at most one year. Recent points stay per-minute; older
|
||||
# points are compacted to one last cumulative snapshot per hour.
|
||||
stats_cleanup_history() {
|
||||
if [[ ! -f "$HISTORY_FILE" ]]; then
|
||||
return
|
||||
fi
|
||||
|
||||
local now=$(date +%s)
|
||||
local ts_365d=$((now - 31536000))
|
||||
local temp_file=$(mktemp)
|
||||
stats_should_cleanup "$STATS_CLEANUP_STAMP" || return 0
|
||||
|
||||
local retention_cutoff minute_cutoff temp_file
|
||||
read -r retention_cutoff minute_cutoff <<< "$(stats_retention_cutoffs)"
|
||||
temp_file=$(mktemp)
|
||||
|
||||
# Keep header + entries from last 365 days
|
||||
{
|
||||
head -1 "$HISTORY_FILE"
|
||||
awk -F, -v ts="$ts_365d" '$1 >= ts' "$HISTORY_FILE" | tail -n +2
|
||||
awk -F, -v keep="$retention_cutoff" -v minute="$minute_cutoff" '
|
||||
BEGIN { OFS="," }
|
||||
NR == 1 { next }
|
||||
$1 !~ /^[0-9]+$/ { next }
|
||||
$1 < keep { next }
|
||||
$1 >= minute { print $1, $2, $3; next }
|
||||
{
|
||||
bucket = int($1 / 3600)
|
||||
compact[bucket] = $1 OFS $2 OFS $3
|
||||
}
|
||||
END {
|
||||
for (bucket in compact) print compact[bucket]
|
||||
}
|
||||
' "$HISTORY_FILE" | sort -t, -k1,1n
|
||||
} > "$temp_file" 2>/dev/null
|
||||
|
||||
mv "$temp_file" "$HISTORY_FILE" 2>/dev/null
|
||||
}
|
||||
|
||||
stats_cleanup_user_history() {
|
||||
if [[ ! -f "$USER_HISTORY_FILE" ]]; then
|
||||
return
|
||||
fi
|
||||
|
||||
stats_should_cleanup "${STATS_CLEANUP_STAMP}.users" || return 0
|
||||
|
||||
local retention_cutoff minute_cutoff temp_file
|
||||
read -r retention_cutoff minute_cutoff <<< "$(stats_retention_cutoffs)"
|
||||
temp_file=$(mktemp)
|
||||
|
||||
{
|
||||
head -1 "$USER_HISTORY_FILE"
|
||||
awk -F, -v keep="$retention_cutoff" -v minute="$minute_cutoff" '
|
||||
BEGIN { OFS="," }
|
||||
NR == 1 { next }
|
||||
$1 !~ /^[0-9]+$/ { next }
|
||||
$1 < keep { next }
|
||||
$1 >= minute { print $1, $2, $3, $4, $5, $6; next }
|
||||
{
|
||||
bucket = $2 SUBSEP int($1 / 3600)
|
||||
compact[bucket] = $1 OFS $2 OFS $3 OFS $4 OFS $5 OFS $6
|
||||
}
|
||||
END {
|
||||
for (bucket in compact) print compact[bucket]
|
||||
}
|
||||
' "$USER_HISTORY_FILE" | sort -t, -k1,1n -k2,2
|
||||
} > "$temp_file" 2>/dev/null
|
||||
|
||||
mv "$temp_file" "$USER_HISTORY_FILE" 2>/dev/null
|
||||
}
|
||||
|
||||
# Toggle stats collection on/off
|
||||
toggle_stats() {
|
||||
local current_state="false"
|
||||
@@ -363,6 +512,14 @@ install_stats_collector() {
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! command -v iptables &>/dev/null; then
|
||||
log_info "Установка iptables для подсчёта трафика..."
|
||||
install_pkg "$(apt_pkg_for_cmd iptables)" || {
|
||||
echo "Не удалось установить iptables" >&2
|
||||
return 1
|
||||
}
|
||||
fi
|
||||
|
||||
# Get script directory (resolve symlinks)
|
||||
local script_dir=$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")
|
||||
local lib_dir=$(dirname "$script_dir")
|
||||
@@ -370,7 +527,7 @@ install_stats_collector() {
|
||||
# Create systemd service file
|
||||
cat > "$service_file" <<'EOF'
|
||||
[Unit]
|
||||
Description=GoTelegram Traffic Stats Collector
|
||||
Description=goTelegram Pro Traffic Stats Collector
|
||||
After=network.target
|
||||
Wants=network-online.target
|
||||
|
||||
@@ -390,7 +547,18 @@ EOF
|
||||
chmod 644 "$service_file"
|
||||
systemctl daemon-reload
|
||||
systemctl enable gotelegram-stats.service
|
||||
systemctl start gotelegram-stats.service
|
||||
systemctl restart gotelegram-stats.service
|
||||
|
||||
if [[ -f "$CONFIG_FILE" ]] && command -v jq &>/dev/null; then
|
||||
local tmp
|
||||
tmp=$(mktemp)
|
||||
if jq '.stats_enabled = true' "$CONFIG_FILE" > "$tmp" 2>/dev/null; then
|
||||
mv "$tmp" "$CONFIG_FILE"
|
||||
chmod 600 "$CONFIG_FILE" 2>/dev/null || true
|
||||
else
|
||||
rm -f "$tmp" 2>/dev/null
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "Сервис gotelegram-stats установлен и запущен" >&2
|
||||
}
|
||||
@@ -414,13 +582,13 @@ remove_stats_collector() {
|
||||
|
||||
# Clean up directories and files
|
||||
rm -rf "$STATS_DIR" 2>/dev/null
|
||||
rm -f "$HISTORY_FILE" 2>/dev/null
|
||||
rm -f "$HISTORY_FILE" "$USER_HISTORY_FILE" 2>/dev/null
|
||||
|
||||
echo "Сервис статистики удалён" >&2
|
||||
}
|
||||
|
||||
# Export functions for external use
|
||||
export -f stats_init stats_collect stats_read_current stats_calculate_rates
|
||||
export -f stats_init stats_collect stats_collect_users stats_active_users stats_read_current stats_calculate_rates
|
||||
export -f show_traffic_stats format_bytes format_rate toggle_stats
|
||||
export -f stats_cleanup_history install_stats_collector remove_stats_collector
|
||||
export -f stats_cleanup_history stats_cleanup_user_history stats_should_cleanup stats_retention_cutoffs install_stats_collector remove_stats_collector
|
||||
export -f json_get
|
||||
|
||||
130
lib/telemt.sh
Normal file → Executable file
130
lib/telemt.sh
Normal file → Executable file
@@ -1,5 +1,5 @@
|
||||
#!/bin/bash
|
||||
# GoTelegram v2.2 — Управление telemt binary
|
||||
# GoTelegram v2.5.0 — Управление telemt binary
|
||||
# Скачивание, обновление, запуск, остановка через systemd
|
||||
|
||||
TELEMT_GITHUB="telemt/telemt"
|
||||
@@ -19,55 +19,27 @@ get_latest_telemt_version() {
|
||||
}
|
||||
|
||||
get_telemt_download_url() {
|
||||
# 1) Сначала пробуем GitHub Releases API — он отдаёт точное имя ассета
|
||||
# последнего релиза (в т.ч. если в репо есть несколько архитектур,
|
||||
# pre-release и т.д.). Это наш предпочтительный путь.
|
||||
local resp url arch
|
||||
local arch
|
||||
arch=$(get_arch)
|
||||
local resp
|
||||
resp=$(curl -s --max-time 10 "$TELEMT_RELEASE_API" 2>/dev/null)
|
||||
if [ -n "$resp" ]; then
|
||||
url=$(echo "$resp" | jq -r --arg a "$arch" '
|
||||
.assets[]?.browser_download_url
|
||||
| select(test("linux"))
|
||||
| select(
|
||||
($a == "amd64" and (test("x86_64|amd64"))) or
|
||||
($a == "arm64" and (test("aarch64|arm64"))) or
|
||||
($a == "armv7" and (test("armv7"))) or
|
||||
(test($a))
|
||||
)
|
||||
| select(test("gnu"))
|
||||
' 2>/dev/null | head -1)
|
||||
if [ -n "$url" ] && [ "$url" != "null" ]; then
|
||||
echo "$url"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
if [ -z "$resp" ]; then return 1; fi
|
||||
|
||||
# 2) Fallback: API не ответил / отдал 403 (rate limit на shared-IP VPS),
|
||||
# отдал пустой JSON, или jq не нашёл подходящий ассет.
|
||||
# Берём прямой "magic redirect" CDN-URL — он не считается в API rate
|
||||
# limit и всегда указывает на последний релиз.
|
||||
local arch_name
|
||||
# URL format: telemt-x86_64-linux-gnu.tar.gz (arch BEFORE linux)
|
||||
local arch_pattern
|
||||
case "$arch" in
|
||||
amd64) arch_name="x86_64" ;;
|
||||
arm64) arch_name="aarch64" ;;
|
||||
armv7) arch_name="armv7" ;;
|
||||
*) arch_name="$arch" ;;
|
||||
amd64) arch_pattern="(amd64|x86_64)" ;;
|
||||
arm64) arch_pattern="(arm64|aarch64)" ;;
|
||||
armv7) arch_pattern="(armv7|arm)" ;;
|
||||
*) arch_pattern="${arch}" ;;
|
||||
esac
|
||||
echo "https://github.com/${TELEMT_GITHUB}/releases/latest/download/telemt-${arch_name}-linux-gnu.tar.gz"
|
||||
}
|
||||
|
||||
# Fallback URL using musl libc (some minimal distros / older glibc)
|
||||
get_telemt_download_url_musl() {
|
||||
local arch arch_name
|
||||
arch=$(get_arch)
|
||||
case "$arch" in
|
||||
amd64) arch_name="x86_64" ;;
|
||||
arm64) arch_name="aarch64" ;;
|
||||
armv7) arch_name="armv7" ;;
|
||||
*) arch_name="$arch" ;;
|
||||
esac
|
||||
echo "https://github.com/${TELEMT_GITHUB}/releases/latest/download/telemt-${arch_name}-linux-musl.tar.gz"
|
||||
echo "$resp" | jq -r ".assets[].browser_download_url" 2>/dev/null \
|
||||
| grep -iE "$arch_pattern" \
|
||||
| grep -i "linux" \
|
||||
| grep -v "sha256" \
|
||||
| grep "gnu" \
|
||||
| head -1
|
||||
}
|
||||
|
||||
# ── Установленная версия ─────────────────────────────────────────────────────
|
||||
@@ -97,35 +69,18 @@ download_telemt() {
|
||||
log_info "Скачивание: $url"
|
||||
|
||||
if ! curl -L -s --max-time 120 -o "$tmp_file" "$url"; then
|
||||
log_warning "Не удалось скачать gnu-сборку, пробую musl..."
|
||||
url=$(get_telemt_download_url_musl)
|
||||
log_info "Скачивание: $url"
|
||||
if ! curl -L -s --max-time 120 -o "$tmp_file" "$url"; then
|
||||
log_error "Ошибка скачивания telemt (gnu и musl)"
|
||||
rm -f "$tmp_file"
|
||||
return 1
|
||||
fi
|
||||
log_error "Ошибка скачивания telemt"
|
||||
rm -f "$tmp_file"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Проверяем что файл не пустой и не HTML
|
||||
local file_size
|
||||
file_size=$(stat -c%s "$tmp_file" 2>/dev/null || echo 0)
|
||||
if [ "$file_size" -lt 1000 ]; then
|
||||
# Try musl as fallback if gnu came back empty/error-html
|
||||
log_warning "Файл подозрительно мал ($file_size байт), пробую musl-сборку..."
|
||||
url=$(get_telemt_download_url_musl)
|
||||
log_info "Скачивание: $url"
|
||||
if ! curl -L -s --max-time 120 -o "$tmp_file" "$url"; then
|
||||
log_error "Ошибка скачивания telemt (musl fallback)"
|
||||
rm -f "$tmp_file"
|
||||
return 1
|
||||
fi
|
||||
file_size=$(stat -c%s "$tmp_file" 2>/dev/null || echo 0)
|
||||
if [ "$file_size" -lt 1000 ]; then
|
||||
log_error "Скачанный файл слишком маленький ($file_size байт) — возможна ошибка сети"
|
||||
rm -f "$tmp_file"
|
||||
return 1
|
||||
fi
|
||||
log_error "Скачанный файл слишком маленький ($file_size байт) — возможна ошибка сети"
|
||||
rm -f "$tmp_file"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Определяем тип файла и распаковываем
|
||||
@@ -204,7 +159,7 @@ install_telemt_service() {
|
||||
|
||||
cat > "/etc/systemd/system/${TELEMT_SERVICE}.service" << EOF
|
||||
[Unit]
|
||||
Description=GoTelegram MTProxy (telemt engine)
|
||||
Description=goTelegram Pro MTProxy (telemt engine)
|
||||
Documentation=https://github.com/telemt/telemt
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
@@ -240,18 +195,45 @@ EOF
|
||||
# previous Pro config (tls_domain=anten-ka.com) and was rejecting SNI=google.com
|
||||
# clients with unknown_sni_action=Drop even though the on-disk config said
|
||||
# tls_domain=google.com.
|
||||
wait_telemt_ready() {
|
||||
local timeout="${1:-90}"
|
||||
local port elapsed=0
|
||||
port=$(awk '
|
||||
/^\[server\]/ { in_server=1; next }
|
||||
/^\[/ && in_server { exit }
|
||||
in_server && $1 == "port" {
|
||||
sub(/^[^=]*=[[:space:]]*/, "")
|
||||
gsub(/[[:space:]]/, "")
|
||||
print
|
||||
exit
|
||||
}
|
||||
' "$TELEMT_CONFIG" 2>/dev/null)
|
||||
[[ "$port" =~ ^[0-9]+$ ]] || port=443
|
||||
|
||||
while [ "$elapsed" -lt "$timeout" ]; do
|
||||
if ! systemctl is-active --quiet "$TELEMT_SERVICE" 2>/dev/null; then
|
||||
return 1
|
||||
fi
|
||||
if ss -ltnp 2>/dev/null | grep -E ":${port}\b" | grep -q "telemt"; then
|
||||
return 0
|
||||
fi
|
||||
sleep 1
|
||||
elapsed=$((elapsed + 1))
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
start_telemt() {
|
||||
if systemctl is-active --quiet "$TELEMT_SERVICE" 2>/dev/null; then
|
||||
systemctl restart "$TELEMT_SERVICE" 2>/dev/null
|
||||
else
|
||||
systemctl start "$TELEMT_SERVICE" 2>/dev/null
|
||||
fi
|
||||
sleep 2
|
||||
if systemctl is-active --quiet "$TELEMT_SERVICE"; then
|
||||
if wait_telemt_ready 90; then
|
||||
log_success "telemt запущен"
|
||||
return 0
|
||||
else
|
||||
log_error "telemt не запустился"
|
||||
log_error "telemt не запустился или не открыл порт"
|
||||
journalctl -u "$TELEMT_SERVICE" --no-pager -n 10 2>/dev/null
|
||||
return 1
|
||||
fi
|
||||
@@ -268,12 +250,12 @@ stop_telemt() {
|
||||
|
||||
restart_telemt() {
|
||||
systemctl restart "$TELEMT_SERVICE" 2>/dev/null
|
||||
sleep 2
|
||||
if systemctl is-active --quiet "$TELEMT_SERVICE"; then
|
||||
if wait_telemt_ready 90; then
|
||||
log_success "telemt перезапущен"
|
||||
return 0
|
||||
else
|
||||
log_error "telemt не перезапустился"
|
||||
log_error "telemt не перезапустился или не открыл порт"
|
||||
journalctl -u "$TELEMT_SERVICE" --no-pager -n 10 2>/dev/null
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
211
lib/telemt_config.sh
Normal file → Executable file
211
lib/telemt_config.sh
Normal file → Executable file
@@ -1,5 +1,5 @@
|
||||
#!/bin/bash
|
||||
# GoTelegram v2.2 — Генерация TOML конфигурации для telemt
|
||||
# GoTelegram v2.5.0 — Генерация TOML конфигурации для telemt
|
||||
|
||||
# ── Популярные домены (не заблокированные в РФ) ──────────────────────────────
|
||||
QUICK_DOMAINS=(
|
||||
@@ -48,15 +48,38 @@ generate_telemt_toml() {
|
||||
# Сгенерировано: $(date -Iseconds)
|
||||
# Режим: ${mask_mode}
|
||||
|
||||
[general]
|
||||
use_middle_proxy = true
|
||||
log_level = "normal"
|
||||
|
||||
[general.modes]
|
||||
classic = false
|
||||
secure = false
|
||||
tls = true
|
||||
|
||||
[general.links]
|
||||
show = "*"
|
||||
public_port = ${port}
|
||||
|
||||
[server]
|
||||
port = ${port}
|
||||
listen_addr_ipv4 = "0.0.0.0"
|
||||
metrics_listen = "127.0.0.1:9090"
|
||||
metrics_whitelist = ["127.0.0.1/32", "::1/128"]
|
||||
|
||||
[server.api]
|
||||
enabled = true
|
||||
listen = "127.0.0.1:9091"
|
||||
whitelist = ["127.0.0.1/32", "::1/128"]
|
||||
minimal_runtime_enabled = false
|
||||
minimal_runtime_cache_ttl_ms = 1000
|
||||
|
||||
[censorship]
|
||||
tls_domain = "${mask_domain}"
|
||||
mask = true
|
||||
mask_port = ${mask_port}
|
||||
tls_emulation = $([ "$mask_mode" = "pro" ] && echo "false" || echo "true")
|
||||
unknown_sni_action = "mask"
|
||||
|
||||
[access.users]
|
||||
main = "${secret}"
|
||||
@@ -106,25 +129,157 @@ get_config_value() {
|
||||
case "$key" in
|
||||
secret)
|
||||
# [access.users] main = "..."
|
||||
grep -A5 '\[access.users\]' "$config" | grep -m1 '=' | sed 's/.*=\s*"\(.*\)"/\1/' | tr -d ' '
|
||||
awk '
|
||||
/^\[access\.users\]/ { in_users=1; next }
|
||||
/^\[/ && in_users { exit }
|
||||
in_users && /^[[:space:]]*[^#[:space:]][^=]*=/ {
|
||||
user_key=$1
|
||||
gsub(/"/, "", user_key)
|
||||
if (user_key != "main") next
|
||||
|
||||
value=$0
|
||||
sub(/^[^=]*=[[:space:]]*/, "", value)
|
||||
sub(/^"/, "", value)
|
||||
sub(/".*$/, "", value)
|
||||
gsub(/[[:space:]]/, "", value)
|
||||
if (value != "") {
|
||||
print value
|
||||
found=1
|
||||
}
|
||||
exit
|
||||
}
|
||||
END { exit found ? 0 : 1 }
|
||||
' "$config"
|
||||
;;
|
||||
port)
|
||||
# [server] port = 443
|
||||
grep -A5 '\[server\]' "$config" | grep 'port\s*=' | head -1 | sed 's/.*=\s*\([0-9]*\)/\1/' | tr -d ' '
|
||||
awk '
|
||||
/^\[server\]/ { in_server=1; next }
|
||||
/^\[/ && in_server { exit }
|
||||
in_server && $1 == "port" {
|
||||
sub(/^[^=]*=[[:space:]]*/, "")
|
||||
gsub(/[[:space:]]/, "")
|
||||
print
|
||||
exit
|
||||
}
|
||||
' "$config"
|
||||
;;
|
||||
mask_host|tls_domain)
|
||||
# [censorship] tls_domain = "..."
|
||||
grep -A10 '\[censorship\]' "$config" | grep 'tls_domain\s*=' | sed 's/.*=\s*"\(.*\)"/\1/'
|
||||
awk '
|
||||
/^\[censorship\]/ { in_cens=1; next }
|
||||
/^\[/ && in_cens { exit }
|
||||
in_cens && $1 == "tls_domain" {
|
||||
sub(/^[^=]*=[[:space:]]*"/, "")
|
||||
sub(/".*$/, "")
|
||||
print
|
||||
exit
|
||||
}
|
||||
' "$config"
|
||||
;;
|
||||
mask_port)
|
||||
grep -A10 '\[censorship\]' "$config" | grep 'mask_port\s*=' | sed 's/.*=\s*\([0-9]*\)/\1/' | tr -d ' '
|
||||
awk '
|
||||
/^\[censorship\]/ { in_cens=1; next }
|
||||
/^\[/ && in_cens { exit }
|
||||
in_cens && $1 == "mask_port" {
|
||||
sub(/^[^=]*=[[:space:]]*/, "")
|
||||
gsub(/[[:space:]]/, "")
|
||||
print
|
||||
exit
|
||||
}
|
||||
' "$config"
|
||||
;;
|
||||
*)
|
||||
grep "$key" "$config" | head -1 | sed 's/.*=\s*"\?\(.*\)"\?/\1/' | tr -d ' "'
|
||||
grep "$key" "$config" | head -1 | sed 's/^[^=]*=[[:space:]]*//; s/^"//; s/"$//' | tr -d ' '
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
get_telemt_users_block() {
|
||||
local config="${1:-$TELEMT_CONFIG}"
|
||||
[ -f "$config" ] || return 1
|
||||
awk '
|
||||
/^\[access\.users\]/ { in_users=1; next }
|
||||
/^\[/ && in_users { exit }
|
||||
in_users && /^[[:space:]]*[^#[:space:]][^=]*=/ { print }
|
||||
' "$config"
|
||||
}
|
||||
|
||||
first_telemt_user_secret() {
|
||||
local config="${1:-$TELEMT_CONFIG}"
|
||||
get_telemt_users_block "$config" | head -1 | sed 's/^[^=]*=[[:space:]]*//; s/^"//; s/".*$//' | tr -d ' '
|
||||
}
|
||||
|
||||
telemt_users_block_has_main() {
|
||||
local users_block="$1"
|
||||
printf '%s\n' "$users_block" | awk -F= '
|
||||
/^[[:space:]]*#/ || ! /=/ { next }
|
||||
{
|
||||
key=$1
|
||||
gsub(/^[[:space:]]+|[[:space:]]+$/, "", key)
|
||||
if (key ~ /^".*"$/ || key ~ /^\047.*\047$/) {
|
||||
key=substr(key, 2, length(key) - 2)
|
||||
}
|
||||
if (key == "main") {
|
||||
found=1
|
||||
exit
|
||||
}
|
||||
}
|
||||
END { exit found ? 0 : 1 }
|
||||
'
|
||||
}
|
||||
|
||||
replace_telemt_users_block() {
|
||||
local users_block="$1"
|
||||
local config="${2:-$TELEMT_CONFIG}"
|
||||
[ -f "$config" ] || return 1
|
||||
[ -n "$users_block" ] || return 0
|
||||
|
||||
local tmp
|
||||
tmp=$(mktemp) || return 1
|
||||
awk -v users="$users_block" '
|
||||
BEGIN { split(users, lines, "\n") }
|
||||
/^\[access\.users\]/ {
|
||||
found=1
|
||||
print
|
||||
for (i = 1; i in lines; i++) {
|
||||
if (lines[i] != "") print lines[i]
|
||||
}
|
||||
in_users=1
|
||||
next
|
||||
}
|
||||
/^\[/ && in_users { in_users=0 }
|
||||
in_users { next }
|
||||
{ print }
|
||||
END {
|
||||
if (!found) {
|
||||
print ""
|
||||
print "[access.users]"
|
||||
for (i = 1; i in lines; i++) {
|
||||
if (lines[i] != "") print lines[i]
|
||||
}
|
||||
}
|
||||
}
|
||||
' "$config" > "$tmp" && mv "$tmp" "$config"
|
||||
chmod 600 "$config"
|
||||
}
|
||||
|
||||
toml_bool_value() {
|
||||
local table="$1"
|
||||
local key="$2"
|
||||
local config="${3:-$TELEMT_CONFIG}"
|
||||
awk -v table="$table" -v key="$key" '
|
||||
$0 == "[" table "]" { in_table=1; next }
|
||||
/^\[/ && in_table { exit }
|
||||
in_table && $1 == key {
|
||||
sub(/^[^=]*=[[:space:]]*/, "")
|
||||
gsub(/[[:space:]]/, "")
|
||||
print
|
||||
exit
|
||||
}
|
||||
' "$config"
|
||||
}
|
||||
|
||||
# ── Валидация конфига ────────────────────────────────────────────────────────
|
||||
validate_telemt_config() {
|
||||
local config="${1:-$TELEMT_CONFIG}"
|
||||
@@ -285,26 +440,26 @@ show_proxy_info() {
|
||||
|
||||
local status_icon status_text
|
||||
case "$status" in
|
||||
running) status_icon="✅"; status_text="$(t info_status_running)" ;;
|
||||
stopped) status_icon="⏸️"; status_text="$(t info_status_stopped)" ;;
|
||||
*) status_icon="❌"; status_text="$(t info_status_not_installed)" ;;
|
||||
running) status_icon="✅"; status_text="Работает" ;;
|
||||
stopped) status_icon="⏸️"; status_text="Остановлен" ;;
|
||||
*) status_icon="❌"; status_text="Не установлен" ;;
|
||||
esac
|
||||
|
||||
echo ""
|
||||
echo -e " ${BOLD}${WHITE}${status_icon} $(t info_proxy_status): ${status_text}${NC}"
|
||||
echo -e " ${BOLD}${WHITE}${status_icon} Статус прокси: ${status_text}${NC}"
|
||||
echo -e " ${DIM}$(printf '─%.0s' {1..50})${NC}"
|
||||
echo -e " ${WHITE}$(t info_engine):${NC} telemt (Rust)"
|
||||
echo -e " ${WHITE}Ядро:${NC} telemt (Rust)"
|
||||
if [ "$mode" = "pro" ] && [ -n "$domain" ]; then
|
||||
echo -e " ${WHITE}$(t info_domain):${NC} ${CYAN}${domain}${NC}"
|
||||
echo -e " ${WHITE}Домен:${NC} ${CYAN}${domain}${NC}"
|
||||
else
|
||||
echo -e " ${WHITE}$(t info_ip):${NC} ${CYAN}${ip}${NC}"
|
||||
echo -e " ${WHITE}IP:${NC} ${CYAN}${ip}${NC}"
|
||||
fi
|
||||
echo -e " ${WHITE}$(t info_port):${NC} ${CYAN}${port}${NC}"
|
||||
echo -e " ${WHITE}$(t info_mode):${NC} ${CYAN}${mode}${NC}"
|
||||
echo -e " ${WHITE}$(t info_mask):${NC} ${CYAN}${mask_host}${NC}"
|
||||
echo -e " ${WHITE}$(t info_secret):${NC} ${CYAN}${secret:0:16}...${NC}"
|
||||
echo -e " ${WHITE}Порт:${NC} ${CYAN}${port}${NC}"
|
||||
echo -e " ${WHITE}Режим:${NC} ${CYAN}${mode}${NC}"
|
||||
echo -e " ${WHITE}Маскировка:${NC} ${CYAN}${mask_host}${NC}"
|
||||
echo -e " ${WHITE}Secret:${NC} ${CYAN}${secret:0:16}...${NC}"
|
||||
echo -e " ${DIM}$(printf '─%.0s' {1..50})${NC}"
|
||||
echo -e " ${WHITE}$(t info_link):${NC}"
|
||||
echo -e " ${WHITE}Ссылка:${NC}"
|
||||
echo -e " ${GREEN}${link}${NC}"
|
||||
echo ""
|
||||
|
||||
@@ -323,20 +478,20 @@ show_proxy_info_pro() {
|
||||
local link="tg://proxy?server=${domain}&port=443&secret=${faketls_secret}"
|
||||
|
||||
echo ""
|
||||
echo -e " ${BOLD}${WHITE}✅ $(t info_proxy_status): $(t info_status_running) (Pro)${NC}"
|
||||
echo -e " ${BOLD}${WHITE}✅ Pro-прокси настроен${NC}"
|
||||
echo -e " ${DIM}$(printf '─%.0s' {1..55})${NC}"
|
||||
echo -e " ${WHITE}$(t info_engine):${NC} telemt (Rust)"
|
||||
echo -e " ${WHITE}$(t info_domain):${NC} ${CYAN}${domain}${NC}"
|
||||
echo -e " ${WHITE}$(t info_port):${NC} ${CYAN}443${NC} (telemt)"
|
||||
echo -e " ${WHITE}$(t info_mode):${NC} ${MAGENTA}Pro (fake-TLS)${NC}"
|
||||
echo -e " ${WHITE}nginx:${NC} ${CYAN}127.0.0.1:8443${NC}"
|
||||
echo -e " ${WHITE}$(t info_secret):${NC} ${CYAN}${faketls_secret:0:20}...${NC}"
|
||||
echo -e " ${WHITE}Ядро:${NC} telemt (Rust)"
|
||||
echo -e " ${WHITE}Домен:${NC} ${CYAN}${domain}${NC}"
|
||||
echo -e " ${WHITE}Порт:${NC} ${CYAN}443${NC} (внешний, telemt)"
|
||||
echo -e " ${WHITE}Режим:${NC} ${MAGENTA}Pro (fake-TLS)${NC}"
|
||||
echo -e " ${WHITE}nginx:${NC} ${CYAN}127.0.0.1:8443${NC} (внутренний)"
|
||||
echo -e " ${WHITE}Secret:${NC} ${CYAN}${faketls_secret:0:20}...${NC}"
|
||||
echo -e " ${DIM}$(printf '─%.0s' {1..55})${NC}"
|
||||
echo -e " ${WHITE}$(t info_link):${NC}"
|
||||
echo -e " ${WHITE}Ссылка для Telegram:${NC}"
|
||||
echo -e " ${GREEN}${link}${NC}"
|
||||
echo ""
|
||||
echo -e " ${DIM}ISP sees: HTTPS → ${domain}:443${NC}"
|
||||
echo -e " ${DIM}Telegram client masquerades as TLS${NC}"
|
||||
echo -e " ${DIM}Провайдер видит: HTTPS-трафик к ${domain}:443${NC}"
|
||||
echo -e " ${DIM}Telegram-клиент маскирует соединение под TLS${NC}"
|
||||
echo ""
|
||||
|
||||
# QR если доступен
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/bin/bash
|
||||
# GoTelegram v2.4 — website templates catalog
|
||||
# GoTelegram v2.5.0 — website templates catalog
|
||||
# Pick from ~1800 templates, preview links, git sparse-checkout downloads,
|
||||
# + custom git URL templates (user-supplied public repos)
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/bin/bash
|
||||
# GoTelegram v2.2 — Управление сайтом (nginx + certbot + шаблоны)
|
||||
# GoTelegram v2.5.0 — Управление сайтом (nginx + certbot + шаблоны)
|
||||
|
||||
# ── Установка nginx ──────────────────────────────────────────────────────────
|
||||
install_nginx() {
|
||||
@@ -39,7 +39,7 @@ generate_nginx_config() {
|
||||
mkdir -p /etc/nginx/sites-available /etc/nginx/sites-enabled
|
||||
|
||||
cat > "$NGINX_SITE_CONF" << 'EONGINX'
|
||||
# GoTelegram v2.3 — nginx config
|
||||
# GoTelegram v2.5.0 — nginx config
|
||||
# Pro: nginx на 127.0.0.1:8443 (внутренний), telemt на 0.0.0.0:443 (внешний)
|
||||
# Обычный браузер → :443 → telemt → 127.0.0.1:8443 → nginx (сайт)
|
||||
|
||||
@@ -237,11 +237,15 @@ get_ssl_expiry() {
|
||||
# ── Деплой шаблона сайта ─────────────────────────────────────────────────────
|
||||
deploy_template_to_nginx() {
|
||||
local template_dir="$1"
|
||||
local template_id="${2:-}"
|
||||
local source_url=""
|
||||
|
||||
if [ ! -d "$template_dir" ] || [ ! -f "$template_dir/index.html" ]; then
|
||||
log_error "Шаблон не содержит index.html: $template_dir"
|
||||
return 1
|
||||
fi
|
||||
[ -z "$template_id" ] && template_id=$(basename "$template_dir")
|
||||
[ -f "$template_dir/.custom_git_source" ] && source_url=$(head -1 "$template_dir/.custom_git_source" 2>/dev/null || echo "")
|
||||
|
||||
# Бекапим старый сайт
|
||||
if [ -d "$WEBSITE_ROOT" ] && [ "$(ls -A "$WEBSITE_ROOT" 2>/dev/null)" ]; then
|
||||
@@ -251,7 +255,10 @@ deploy_template_to_nginx() {
|
||||
fi
|
||||
|
||||
mkdir -p "$WEBSITE_ROOT"
|
||||
cp -r "$template_dir"/* "$WEBSITE_ROOT/"
|
||||
cp -a "$template_dir/." "$WEBSITE_ROOT/"
|
||||
rm -f "$WEBSITE_ROOT/.custom_git_source" 2>/dev/null || true
|
||||
echo "$template_id" > "$WEBSITE_ROOT/.gotelegram_template_id" 2>/dev/null || true
|
||||
[ -n "$source_url" ] && echo "$source_url" > "$WEBSITE_ROOT/.gotelegram_template_source" 2>/dev/null || true
|
||||
chown -R www-data:www-data "$WEBSITE_ROOT" 2>/dev/null || chown -R nginx:nginx "$WEBSITE_ROOT" 2>/dev/null
|
||||
chmod -R 755 "$WEBSITE_ROOT"
|
||||
|
||||
@@ -334,7 +341,7 @@ remove_pro_mode() {
|
||||
# ── Смена шаблона ────────────────────────────────────────────────────────────
|
||||
switch_template() {
|
||||
local new_template_dir="$1"
|
||||
deploy_template_to_nginx "$new_template_dir"
|
||||
deploy_template_to_nginx "$new_template_dir" "$(basename "$new_template_dir")"
|
||||
# nginx не требует перезапуска — статика обновилась на месте
|
||||
log_success "Шаблон сайта обновлён"
|
||||
}
|
||||
|
||||
291
tests/test_admin_features.py
Normal file
291
tests/test_admin_features.py
Normal file
@@ -0,0 +1,291 @@
|
||||
import importlib.util
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import shlex
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
SERVER_PATH = ROOT / "admin-web" / "server.py"
|
||||
|
||||
|
||||
def load_server(tmpdir: Path):
|
||||
os.environ["GOTELEGRAM_BACKUP_DIR"] = str(tmpdir / "backups")
|
||||
os.environ["GOTELEGRAM_DIR"] = str(tmpdir / "gotelegram")
|
||||
os.environ["TELEMT_CONFIG"] = str(tmpdir / "etc" / "telemt" / "config.toml")
|
||||
os.environ["GOTELEGRAM_DISABLED_USERS"] = str(tmpdir / "gotelegram" / "disabled_users.json")
|
||||
module_name = "gotelegram_admin_server_test"
|
||||
sys.modules.pop(module_name, None)
|
||||
spec = importlib.util.spec_from_file_location(module_name, SERVER_PATH)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
assert spec.loader is not None
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
class AdminFeatureTests(unittest.TestCase):
|
||||
def test_backup_path_accepts_only_local_archives(self):
|
||||
with tempfile.TemporaryDirectory() as raw:
|
||||
tmpdir = Path(raw)
|
||||
server = load_server(tmpdir)
|
||||
server.BACKUP_DIR.mkdir(parents=True)
|
||||
good = server.BACKUP_DIR / "gotelegram_backup_20260425_120000.tar.gz"
|
||||
good.write_text("backup", encoding="utf-8")
|
||||
encrypted = server.BACKUP_DIR / "gotelegram_backup_20260425_120001.tar.gz.enc"
|
||||
encrypted.write_text("backup", encoding="utf-8")
|
||||
legacy = server.BACKUP_DIR / "backup_20260425_120002.tar.gz"
|
||||
legacy.write_text("backup", encoding="utf-8")
|
||||
|
||||
self.assertEqual(server.safe_backup_path(good.name), good.resolve())
|
||||
self.assertEqual(server.safe_backup_path(encrypted.name), encrypted.resolve())
|
||||
self.assertEqual(server.safe_backup_path(legacy.name), legacy.resolve())
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
server.safe_backup_path("../outside.tar.gz")
|
||||
with self.assertRaises(ValueError):
|
||||
server.safe_backup_path("gotelegram_backup_20260425_120000.tar.gz.sha256")
|
||||
with self.assertRaises(FileNotFoundError):
|
||||
server.safe_backup_path("missing.tar.gz")
|
||||
|
||||
def test_backup_schedule_calendar_rejects_unknown_values(self):
|
||||
with tempfile.TemporaryDirectory() as raw:
|
||||
server = load_server(Path(raw))
|
||||
self.assertEqual(server.backup_schedule_calendar("daily"), "*-*-* 03:20:00")
|
||||
self.assertEqual(server.backup_schedule_calendar("weekly"), "Sun 03:20:00")
|
||||
self.assertEqual(server.backup_schedule_calendar("monthly"), "*-*-01 03:20:00")
|
||||
self.assertIsNone(server.backup_schedule_calendar("off"))
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
server.backup_schedule_calendar("hourly")
|
||||
|
||||
def test_user_records_include_active_disabled_and_ip_limits(self):
|
||||
with tempfile.TemporaryDirectory() as raw:
|
||||
tmpdir = Path(raw)
|
||||
server = load_server(tmpdir)
|
||||
server.TELEMT_CONFIG.parent.mkdir(parents=True)
|
||||
server.TELEMT_CONFIG.write_text(
|
||||
"\n".join([
|
||||
"[access.users]",
|
||||
'main = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"',
|
||||
'client = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"',
|
||||
"",
|
||||
"[access.user_max_unique_ips]",
|
||||
"main = 0",
|
||||
"client = 2",
|
||||
"disabled = 1",
|
||||
"",
|
||||
]),
|
||||
encoding="utf-8",
|
||||
)
|
||||
server.DISABLED_USERS_FILE.parent.mkdir(parents=True)
|
||||
server.DISABLED_USERS_FILE.write_text(
|
||||
json.dumps({"users": {"disabled": "cccccccccccccccccccccccccccccccc"}}),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
records = server.read_user_records()
|
||||
|
||||
self.assertTrue(records["client"]["enabled"])
|
||||
self.assertEqual(records["client"]["max_unique_ips"], 2)
|
||||
self.assertFalse(records["disabled"]["enabled"])
|
||||
self.assertEqual(records["disabled"]["max_unique_ips"], 1)
|
||||
self.assertEqual(records["main"]["max_unique_ips"], 0)
|
||||
|
||||
def test_write_user_max_unique_ips_preserves_other_toml_sections(self):
|
||||
with tempfile.TemporaryDirectory() as raw:
|
||||
server = load_server(Path(raw))
|
||||
server.TELEMT_CONFIG.parent.mkdir(parents=True)
|
||||
server.TELEMT_CONFIG.write_text(
|
||||
"\n".join([
|
||||
"[server]",
|
||||
"port = 443",
|
||||
"",
|
||||
"[access.users]",
|
||||
'main = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"',
|
||||
'client = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"',
|
||||
"",
|
||||
"[access.user_max_unique_ips]",
|
||||
"client = 3",
|
||||
"old = 4",
|
||||
"",
|
||||
"[network]",
|
||||
'dns_overrides = ["example.com:8443:127.0.0.1"]',
|
||||
"",
|
||||
]),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
server.write_user_max_unique_ips({"main": 1, "client": 0, "new": 5})
|
||||
text = server.TELEMT_CONFIG.read_text(encoding="utf-8")
|
||||
|
||||
self.assertIn("[server]\nport = 443", text)
|
||||
self.assertIn("[network]\ndns_overrides", text)
|
||||
self.assertIn("[access.user_max_unique_ips]", text)
|
||||
self.assertIn('"main" = 1', text)
|
||||
self.assertIn('"new" = 5', text)
|
||||
self.assertNotIn("client = 3", text)
|
||||
self.assertNotIn("old = 4", text)
|
||||
|
||||
def test_key_card_traffic_uses_live_ip_counts_not_stale_history(self):
|
||||
with tempfile.TemporaryDirectory() as raw:
|
||||
server = load_server(Path(raw))
|
||||
stale_history = {
|
||||
"epoch": 1000,
|
||||
"total_octets": 100,
|
||||
"current_connections": 16,
|
||||
"active_unique_ips": 8,
|
||||
"recent_unique_ips": 5,
|
||||
}
|
||||
original = server.runtime_user_traffic
|
||||
server.runtime_user_traffic = lambda name, enabled=True: {
|
||||
"ok": True,
|
||||
"enabled": True,
|
||||
"total_octets": 200,
|
||||
"current_connections": 0,
|
||||
"active_unique_ips": 0,
|
||||
"recent_unique_ips": 0,
|
||||
}
|
||||
try:
|
||||
snapshot = server.current_user_traffic_snapshot("client", True, stale_history, now=2000)
|
||||
finally:
|
||||
server.runtime_user_traffic = original
|
||||
|
||||
self.assertEqual(snapshot["epoch"], 2000)
|
||||
self.assertEqual(snapshot["total_octets"], 200)
|
||||
self.assertEqual(snapshot["current_connections"], 0)
|
||||
self.assertEqual(snapshot["active_unique_ips"], 0)
|
||||
self.assertEqual(snapshot["recent_unique_ips"], 0)
|
||||
|
||||
def test_key_card_traffic_fallback_keeps_only_historical_total(self):
|
||||
with tempfile.TemporaryDirectory() as raw:
|
||||
server = load_server(Path(raw))
|
||||
stale_history = {
|
||||
"epoch": 1000,
|
||||
"total_octets": 100,
|
||||
"current_connections": 16,
|
||||
"active_unique_ips": 8,
|
||||
"recent_unique_ips": 5,
|
||||
}
|
||||
original = server.runtime_user_traffic
|
||||
server.runtime_user_traffic = lambda name, enabled=True: {"ok": False}
|
||||
try:
|
||||
snapshot = server.current_user_traffic_snapshot("client", True, stale_history, now=2000)
|
||||
finally:
|
||||
server.runtime_user_traffic = original
|
||||
|
||||
self.assertEqual(snapshot["epoch"], 1000)
|
||||
self.assertEqual(snapshot["total_octets"], 100)
|
||||
self.assertEqual(snapshot["current_connections"], 0)
|
||||
self.assertEqual(snapshot["active_unique_ips"], 0)
|
||||
self.assertEqual(snapshot["recent_unique_ips"], 0)
|
||||
|
||||
def test_keys_view_uses_card_layout_without_horizontal_table(self):
|
||||
app_js = (ROOT / "admin-web" / "static" / "app.js").read_text(encoding="utf-8")
|
||||
styles = (ROOT / "admin-web" / "static" / "styles.css").read_text(encoding="utf-8")
|
||||
index = (ROOT / "admin-web" / "static" / "index.html").read_text(encoding="utf-8")
|
||||
|
||||
self.assertNotIn('class="actions"', app_js)
|
||||
self.assertIn('class="key-card', app_js)
|
||||
self.assertIn('class="key-card-actions"', app_js)
|
||||
self.assertIn('class="action-buttons"', app_js)
|
||||
self.assertIn('class="key-name-button"', app_js)
|
||||
self.assertIn(".key-card .mini-actions,\n.key-card .action-buttons", styles)
|
||||
self.assertIn("grid-template-columns: 1fr", styles)
|
||||
self.assertIn("min-height: 44px", styles)
|
||||
self.assertNotIn("td.actions", styles)
|
||||
self.assertIn('class="keys-list" id="usersTable"', index)
|
||||
self.assertNotIn('class="keys-table"', index)
|
||||
|
||||
def test_topbar_has_five_second_auto_refresh_toggle(self):
|
||||
app_js = (ROOT / "admin-web" / "static" / "app.js").read_text(encoding="utf-8")
|
||||
styles = (ROOT / "admin-web" / "static" / "styles.css").read_text(encoding="utf-8")
|
||||
index = (ROOT / "admin-web" / "static" / "index.html").read_text(encoding="utf-8")
|
||||
|
||||
self.assertIn('id="autoRefreshToggle"', index)
|
||||
self.assertIn('data-i18n-title="autoRefresh"', index)
|
||||
self.assertIn("gotelegram-auto-refresh", app_js)
|
||||
self.assertIn("AUTO_REFRESH_MS = 5000", app_js)
|
||||
self.assertIn("setInterval", app_js)
|
||||
self.assertIn("clearInterval", app_js)
|
||||
self.assertIn(".auto-refresh-toggle", styles)
|
||||
|
||||
def test_get_config_value_secret_accepts_quoted_main_user(self):
|
||||
with tempfile.TemporaryDirectory() as raw:
|
||||
config = Path(raw) / "config.toml"
|
||||
config.write_text(
|
||||
"\n".join([
|
||||
"[access.users]",
|
||||
'"main" = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"',
|
||||
'"client" = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"',
|
||||
"",
|
||||
]),
|
||||
encoding="utf-8",
|
||||
)
|
||||
script = "\n".join([
|
||||
"set -e",
|
||||
f"source {shlex.quote(str(ROOT / 'lib' / 'telemt_config.sh'))}",
|
||||
f"get_config_value secret {shlex.quote(str(config))}",
|
||||
])
|
||||
|
||||
result = subprocess.run(
|
||||
["bash", "-lc", script],
|
||||
cwd=ROOT,
|
||||
text=True,
|
||||
capture_output=True,
|
||||
check=False,
|
||||
)
|
||||
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
self.assertEqual(result.stdout.strip(), "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")
|
||||
|
||||
def test_telemt_users_block_detects_quoted_main_user(self):
|
||||
script = "\n".join([
|
||||
"set -e",
|
||||
f"source {shlex.quote(str(ROOT / 'lib' / 'telemt_config.sh'))}",
|
||||
"block=$'\"main\" = \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"\\n\"client\" = \"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\"'",
|
||||
"telemt_users_block_has_main \"$block\"",
|
||||
])
|
||||
|
||||
result = subprocess.run(
|
||||
["bash", "-lc", script],
|
||||
cwd=ROOT,
|
||||
text=True,
|
||||
capture_output=True,
|
||||
check=False,
|
||||
)
|
||||
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
|
||||
def test_telemt_restart_requests_are_debounced(self):
|
||||
with tempfile.TemporaryDirectory() as raw:
|
||||
server = load_server(Path(raw))
|
||||
calls = []
|
||||
original_run = server.run
|
||||
original_status = server.service_status
|
||||
try:
|
||||
server._LAST_TELEMT_RESTART = 0.0
|
||||
server.TELEMT_RESTART_DEBOUNCE_SECONDS = 30.0
|
||||
server.service_status = lambda name: "running"
|
||||
|
||||
def fake_run(cmd, timeout=8):
|
||||
calls.append(cmd)
|
||||
return 0, "", ""
|
||||
|
||||
server.run = fake_run
|
||||
self.assertTrue(server.request_service_restart("telemt"))
|
||||
self.assertTrue(server.request_service_restart("telemt"))
|
||||
finally:
|
||||
server.run = original_run
|
||||
server.service_status = original_status
|
||||
|
||||
restart_calls = [cmd for cmd in calls if cmd[:3] == ["systemctl", "--no-block", "restart"]]
|
||||
self.assertEqual(len(restart_calls), 1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
102
tests/test_bot_features.py
Normal file
102
tests/test_bot_features.py
Normal file
@@ -0,0 +1,102 @@
|
||||
import json
|
||||
import re
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
BOT_PATH = ROOT / "gotelegram-bot" / "bot.py"
|
||||
CATALOG_PATH = ROOT / "templates_catalog.json"
|
||||
INSTALL_PATH = ROOT / "install.sh"
|
||||
|
||||
|
||||
class BotFeatureTests(unittest.TestCase):
|
||||
def test_catalog_contains_template_ids_that_break_raw_callback_data(self):
|
||||
catalog = json.loads(CATALOG_PATH.read_text(encoding="utf-8"))
|
||||
raw_lengths = [
|
||||
len(f"pro_tpl_{tpl['id']}".encode("utf-8"))
|
||||
for cat in catalog.get("categories", [])
|
||||
for tpl in cat.get("templates", [])
|
||||
]
|
||||
|
||||
self.assertTrue(any(length > 64 for length in raw_lengths))
|
||||
|
||||
def test_bot_uses_short_template_callback_keys(self):
|
||||
source = BOT_PATH.read_text(encoding="utf-8")
|
||||
|
||||
self.assertNotIn("callback_data=f\"pro_tpl_{tpl['id']}\"", source)
|
||||
self.assertNotIn('callback_data=f"pro_confirm_{tpl_id}"', source)
|
||||
self.assertIn("pro_template_map", source)
|
||||
self.assertIn("resolve_pro_template_id", source)
|
||||
|
||||
def test_template_callbacks_are_restart_safe_hashes(self):
|
||||
source = BOT_PATH.read_text(encoding="utf-8")
|
||||
category_body = re.search(
|
||||
r"async def cb_pro_category\(.*?(?=\n\n(?:async )?def |\n\n#)",
|
||||
source,
|
||||
flags=re.S,
|
||||
)
|
||||
resolve_body = re.search(
|
||||
r"def resolve_pro_template_id\(.*?(?=\n\n(?:async )?def |\n\n#)",
|
||||
source,
|
||||
flags=re.S,
|
||||
)
|
||||
self.assertIsNotNone(category_body)
|
||||
self.assertIsNotNone(resolve_body)
|
||||
|
||||
self.assertIn('pro_template_key_for_id(context, tpl["id"])', category_body.group(0))
|
||||
self.assertNotIn("enumerate(templates)", category_body.group(0))
|
||||
self.assertNotIn("mapping.clear()", category_body.group(0))
|
||||
self.assertIn("load_json(TEMPLATES_CATALOG)", resolve_body.group(0))
|
||||
self.assertIn("hashlib.sha1", resolve_body.group(0))
|
||||
|
||||
def test_telemt_version_checks_systemd_path_fallbacks(self):
|
||||
source = BOT_PATH.read_text(encoding="utf-8")
|
||||
version_body = re.search(
|
||||
r"async def get_telemt_version\(\).*?(?=\n\n(?:async )?def |\n\n#)",
|
||||
source,
|
||||
flags=re.S,
|
||||
)
|
||||
self.assertIsNotNone(version_body)
|
||||
body = version_body.group(0)
|
||||
|
||||
self.assertIn('"--version"', body)
|
||||
self.assertIn('"-V"', body)
|
||||
self.assertIn('"/usr/local/bin/telemt"', body)
|
||||
self.assertIn("for command in", body)
|
||||
self.assertIn("for args in", body)
|
||||
self.assertNotIn('"-v"', body)
|
||||
|
||||
def test_telemt_update_menu_reuses_version_helper(self):
|
||||
source = BOT_PATH.read_text(encoding="utf-8")
|
||||
update_body = re.search(
|
||||
r"async def cb_menu_update\(.*?(?=\n\n(?:async )?def |\n\n#)",
|
||||
source,
|
||||
flags=re.S,
|
||||
)
|
||||
self.assertIsNotNone(update_body)
|
||||
body = update_body.group(0)
|
||||
|
||||
self.assertIn("await get_telemt_version()", body)
|
||||
self.assertNotIn('sh("telemt", "--version")', body)
|
||||
|
||||
def test_installer_auto_updates_existing_bot_files(self):
|
||||
source = INSTALL_PATH.read_text(encoding="utf-8")
|
||||
auto_body = re.search(
|
||||
r"auto_update_bot_if_possible\(\).*?(?=\n\n[A-Za-z0-9_]+\(\) |\n\n#)",
|
||||
source,
|
||||
flags=re.S,
|
||||
)
|
||||
self.assertIsNotNone(auto_body)
|
||||
|
||||
auto_text = auto_body.group(0)
|
||||
self.assertIn('bot_service_status', auto_text)
|
||||
self.assertIn('bot_install', auto_text)
|
||||
self.assertIn('cmp -s "$SCRIPT_DIR/gotelegram-bot/bot.py" "$BOT_DIR/bot.py"', auto_text)
|
||||
self.assertIn('cmp -s "$SCRIPT_DIR/gotelegram-bot/i18n.py" "$BOT_DIR/i18n.py"', auto_text)
|
||||
self.assertIn('cmp -s "$SCRIPT_DIR/gotelegram-bot/requirements.txt" "$BOT_DIR/requirements.txt"', auto_text)
|
||||
self.assertIn('auto_migrate_legacy_state || true\n auto_update_bot_if_possible || true\n auto_install_admin_web_if_possible || true', source)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user