41 Commits

Author SHA1 Message Date
Виталий Литвинов
83467ba06e Align docs and bootstrap with main beta alpha branches 2026-04-26 11:52:57 +03:00
Виталий Литвинов
4dc286908e Document Telegram bot auto-update flow 2026-04-26 11:04:11 +03:00
Виталий Литвинов
7bb88f2c24 Auto-update existing Telegram bot during upgrade 2026-04-26 10:51:30 +03:00
Виталий Литвинов
ffea9f5d77 Default bootstrap installs from alfa 2026-04-25 23:20:45 +03:00
Виталий Литвинов
5714714d3c Add repository hygiene ignore rules 2026-04-25 22:59:41 +03:00
Виталий Литвинов
6840d309af Fix Telegram bot template callbacks and telemt version checks 2026-04-25 19:18:54 +03:00
Виталий Литвинов
2f3607e1e6 v2.5.0: harden telemt user reloads 2026-04-25 17:50:06 +03:00
Виталий Литвинов
5a6bc9f614 v2.5.0: read quoted main proxy secret 2026-04-25 16:45:57 +03:00
Виталий Литвинов
cae552ec87 v2.5.0: add admin auto refresh toggle 2026-04-25 16:38:19 +03:00
Виталий Литвинов
98e4be8831 v2.5.0: use live IP counts in key cards 2026-04-25 16:22:17 +03:00
Виталий Литвинов
d62991857f v2.5.0: replace keys table with responsive cards 2026-04-25 15:52:17 +03:00
Виталий Литвинов
7b1a79bba9 v2.5.0: align key table buttons 2026-04-25 15:44:36 +03:00
Виталий Литвинов
b58bc9e1ba v2.5.0: make key stats selection explicit 2026-04-25 15:35:13 +03:00
Виталий Литвинов
1e27d6ba71 v2.5.0: make key table responsive 2026-04-25 15:27:58 +03:00
Виталий Литвинов
a143c01a9a v2.5.0: keep admin scroll clear 2026-04-25 15:24:34 +03:00
Виталий Литвинов
507a2979e5 v2.5.0: align key controls and add IP limits 2026-04-25 15:19:28 +03:00
Виталий Литвинов
bd3fc1af18 v2.5.0: select key traffic by row click 2026-04-25 14:56:52 +03:00
Виталий Литвинов
817ea9ab9f v2.5.0: clarify encrypted restore in admin 2026-04-25 14:51:10 +03:00
Виталий Литвинов
d492e5eb69 v2.5.0: make backup names collision safe 2026-04-25 14:47:34 +03:00
Виталий Литвинов
b2ab0dca57 v2.5.0: add QR import and backup scheduling 2026-04-25 14:39:56 +03:00
Виталий Литвинов
c7540a97f7 v2.5.0: compact traffic history retention 2026-04-25 14:15:28 +03:00
Виталий Литвинов
63b564f70f v2.5.0: add shared 443 and per-user traffic 2026-04-25 14:07:47 +03:00
Виталий Литвинов
c1b5ffc5a7 v2.5.0: clarify 443 route map assets 2026-04-25 13:29:05 +03:00
Виталий Литвинов
7eaeef8b49 v2.5.0: show routed services behind port 443 2026-04-25 13:25:32 +03:00
Виталий Литвинов
5225811b3c v2.5.0: harden admin key and traffic flows 2026-04-25 12:28:33 +03:00
Виталий Литвинов
d74b05ccf8 v2.5.0: refine admin traffic and port status 2026-04-25 12:01:31 +03:00
Виталий Литвинов
d8ec62eb07 v2.5.0: update service branding 2026-04-25 09:58:23 +03:00
Виталий Литвинов
e54778c08c v2.5.0: add key disable switches and pro UI polish 2026-04-25 09:44:53 +03:00
Виталий Литвинов
6b89c3ea81 v2.5.0: redact admin overview secret 2026-04-24 23:08:02 +03:00
Виталий Литвинов
a393bd79ff v2.5.0: limit language api response 2026-04-24 23:03:06 +03:00
Виталий Литвинов
3b075a7ed7 v2.5.0: improve admin domain language logs 2026-04-24 22:59:21 +03:00
Виталий Литвинов
9c74a0d00f v2.5.0: guard web admin log loading race 2026-04-24 22:44:47 +03:00
Виталий Литвинов
3ab0f9d5c7 v2.5.0: bust web admin asset cache 2026-04-24 22:40:31 +03:00
Виталий Литвинов
dc7452930a v2.5.0: disable web admin static cache 2026-04-24 22:37:52 +03:00
Виталий Литвинов
d9e4831e44 v2.5.0: redesign local admin and repair stats 2026-04-24 22:30:09 +03:00
Виталий Литвинов
008143a617 v2.5.0: remove local web admin token gate 2026-04-24 22:00:08 +03:00
Виталий Литвинов
8804319e19 v2.5.0: wait for telemt readiness in web admin 2026-04-24 21:41:21 +03:00
Виталий Литвинов
fe6d91c3a5 v2.5.0: wait for telemt listener readiness 2026-04-24 21:39:02 +03:00
Виталий Литвинов
20103ccac8 v2.5.0: add local web admin dashboard 2026-04-24 19:19:12 +03:00
Виталий Литвинов
ed9073f28f v2.5.0: add legacy state migration 2026-04-24 18:58:52 +03:00
Codex
7afeb59261 v2.5.0: maintenance and bot user management 2026-04-24 18:50:43 +03:00
29 changed files with 8683 additions and 281 deletions

51
.gitignore vendored Normal file
View 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

View File

@@ -1,8 +1,8 @@
# GoTelegram Pro — техническая документация для ИИ-агентов
# goTelegram Pro — техническая документация для ИИ-агентов
**Версия:** 2.4.3
**Версия:** 2.5.0
**Репозиторий:** `anten-ka/gotelegram_pro`
**Активная ветка:** `alfa-test` (ветка `test` заморожена и содержит stable для конечных пользователей)
**Активная ветка:** `beta` для публичной проверки; `alpha` — личная экспериментальная ветка владельца; `main` — продакшен.
**Целевая ОС:** 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`.
@@ -43,18 +43,17 @@ tls_domain = google.com (или любой из QUICK_DOMAINS)
```
anten-ka/gotelegram_pro
├── testfrozen stable, пользователи ставятся отсюда через bootstrap.sh
── alfa-test ← активная разработка, пуши туда
├── mainпродакшен, максимально стабильная версия
── beta ← публичная бета для пользователей, готовых к риску и поиску багов
└── alpha ← личная alpha ветка владельца для самых ранних изменений
```
**Правило коммитов (из auto-memory):** все новые изменения идут ТОЛЬКО в `alfa-test`. `test` не трогаем без явной команды пользователя. Когда пользователь в диалоге скажет «влей в stable» — тогда мёржим alfa-test → test.
**Правило коммитов:** новые изменения сначала проверяются в `alpha`, затем после ручной проверки идут в `beta`. В `main` попадает только проверенный beta-код после отдельного решения о релизе. Перед любым переносом между ветками создавай rollback tag/release для текущего состояния ветки.
**Инструменты для коммитов (специфика окружения):**
- `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`.
**Инструменты для коммитов:**
- Основной путь — обычный `git commit` + `git push` в нужную ветку после локальной проверки.
- Если окружение не может пушить напрямую, используй GitHub REST API с PAT из переменной окружения `GOTELEGRAM_PAT`.
- Workflow через GitHub API: `POST git/blobs` для каждого файла → `POST git/trees` (с `base_tree` от текущего HEAD) → `POST git/commits` (parents=[текущий HEAD]) → `PATCH git/refs/heads/alpha` и/или `PATCH git/refs/heads/beta` (sha=новый commit) → при необходимости `POST /releases`.
- **Важно про `base_tree`:** при частичном обновлении (1-2 файла) ОБЯЗАТЕЛЬНО передавать `base_tree` — иначе дерево получится только из переданных файлов, и все остальные файлы пропадут из коммита. `base_tree` можно опускать только при коммите с полным набором файлов.
---
@@ -99,7 +98,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 +108,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 +171,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 +196,7 @@ dns_overrides = ["anten-ka.com:8443:127.0.0.1"]
Генерируется функцией `install_telemt_service()` в `lib/telemt.sh`:
```ini
[Unit]
Description=GoTelegram MTProxy (telemt engine)
Description=goTelegram Pro MTProxy (telemt engine)
After=network-online.target
Wants=network-online.target
@@ -324,7 +324,7 @@ Fallback: если по известным правилам index.html не на
## 10. История багов (не наступать на те же грабли)
Все решены в текущем HEAD (`alfa-test`). Перечисление для того, чтобы новый агент не «чинил» то, что уже починено, и понимал контекст.
Все решены в текущем HEAD (`beta`/`alpha`). Перечисление для того, чтобы новый агент не «чинил» то, что уже починено, и понимал контекст.
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 +370,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 beta+)** при повторном 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 +392,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 +430,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 +502,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 = "beta"
API = f"https://api.github.com/repos/{REPO}"
headers = {
"Authorization": f"Bearer {TOKEN}",
@@ -596,6 +652,8 @@ with socket.create_connection(("95.163.176.222", 443), timeout=5) as s:
## 17. Changelog
- **2.5.0 (2026-04-24)** — крупный maintenance pass в ветках `beta`/`alpha`: единая версия `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 +707,7 @@ with socket.create_connection(("95.163.176.222", 443), timeout=5) as s:
## 20. Контрольные точки и инварианты
Перед любым пушем в `alfa-test`:
Перед любым пушем в `alpha`/`beta`/`main`:
1. `bash -n install.sh lib/*.sh` — синтаксис bash ОК.
2. Все новые `$()`-вызываемые функции пишут UI через `>&2`.
3. Все пути к lib/ идут через `$SCRIPT_DIR/lib/...`, а `SCRIPT_DIR` — через `readlink -f`.
@@ -659,8 +717,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. Подождать пока нужная ветка обновится (GitHub API мгновенно, raw кеш ~30 сек).
2. На VPS: повторить bootstrap из ветки `beta` с `GOTELEGRAM_PAT` или явно передать `GOTELEGRAM_BRANCH=alpha/main`; скрипт сам скачает файлы, обновит установленный бот/админку и покажет меню.
3. `telemt_status` → running. `journalctl -u telemt` → нет ошибок. Ссылка открывается в Telegram-клиенте.
---

View File

@@ -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/beta/bootstrap.sh")
```
Если нужно поставить строго зафиксированный prerelease, укажи ветку/тег явно:
```bash
export GOTELEGRAM_PAT="YOUR_PAT" GOTELEGRAM_BRANCH="v2.5.0-beta.1"; bash <(curl -sL -H "Authorization: token $GOTELEGRAM_PAT" "https://raw.githubusercontent.com/anten-ka/gotelegram_pro/v2.5.0-beta.1/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` (активная разработка).
- Ветки: `main` (продакшен, максимально стабильная версия), `beta` (публичная бета для пользователей, которые готовы искать баги), `alpha` (личная ветка владельца для самых ранних изменений).
---
## 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

File diff suppressed because it is too large Load Diff

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
View 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

File diff suppressed because it is too large Load Diff

View File

@@ -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:-beta}"
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

View File

@@ -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

View File

@@ -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

View File

@@ -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:

View File

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

View File

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

View 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
@@ -294,6 +484,9 @@ install_lite_mode() {
local port
port=$(select_port)
[ $? -ne 0 ] && return
if [ "$port" = "443" ]; then
warn_3xui_443_conflict || true
fi
# Generate secret
local secret
@@ -327,7 +520,7 @@ install_lite_mode() {
# Start
start_telemt || return
# Save GoTelegram config
# Save goTelegram Pro config
save_gotelegram_config "telemt" "lite" "$port" "$secret" "$domain" "" ""
# Credits
@@ -342,6 +535,8 @@ install_lite_mode() {
install_pro_mode() {
log_step "$(t install_pro_step)"
warn_3xui_443_conflict || true
# Enter domain
echo ""
echo -ne " ${WHITE}$(t install_enter_domain)${NC} "
@@ -536,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)
@@ -558,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)"
@@ -601,6 +812,7 @@ menu_website() {
template_dir=$(interactive_template_selection)
[ $? -ne 0 ] && return
switch_template "$template_dir"
update_current_template_id "$template_dir"
;;
esac
}
@@ -658,6 +870,7 @@ menu_remove() {
systemctl daemon-reload
rm -rf "$BOT_DIR"
fi
remove_admin_web
log_success "$(t remove_all_done)"
;;
esac
@@ -667,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"
@@ -677,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)
@@ -906,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]
@@ -925,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)
@@ -1094,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}"
@@ -1108,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 ""
}
@@ -1137,19 +1456,24 @@ mark_promo_shown() {
date +%s > "$GOTELEGRAM_DIR/.promo_last_shown"
}
_promo_qr() {
local label="$1" url="$2"
[ -n "$url" ] || return 0
echo -e " ${DIM}${label}${NC}"
qrencode -t UTF8 -m 1 "$url" 2>/dev/null | while IFS= read -r qr_line; do
echo " $qr_line"
done
}
# ── Promo with QR + delay (on install + once per day) ───────────────────
# QR показываем ТОЛЬКО для чаевых/донатов. Для хостеров оставлены только
# текстовые ссылки и промокоды (см. _promo_block) — QR-коды хостеров
# визуально конкурировали с чаевыми и перегружали экран.
show_promo_with_qr() {
_promo_block
# QR только для чаевых
if command -v qrencode &>/dev/null; then
echo -e " ${DIM}$(t promo_qr_tips)${NC}"
qrencode -t UTF8 -m 1 "https://pay.cloudtips.ru/p/7410814f" 2>/dev/null | while IFS= read -r qr_line; do
echo " $qr_line"
done
_promo_qr "$(t promo_qr_host1)" "https://vk.cc/ct29NQ"
_promo_qr "$(t promo_qr_host2)" "https://vk.cc/cUxAhj"
_promo_qr "$(t promo_qr_tips)" "https://pay.cloudtips.ru/p/7410814f"
_promo_qr "$(t promo_qr_youtube)" "${GOTELEGRAM_YOUTUBE_LINK:-}"
fi
mark_promo_shown
@@ -1469,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
@@ -1487,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

View File

@@ -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}"

View File

@@ -1,5 +1,5 @@
#!/bin/bash
# GoTelegram v2.4 — backup and restore (i18n-aware)
# goTelegram Pro v2.5.0 — backup and restore (i18n-aware)
# ── Создание бекапа ──────────────────────────────────────────────────────────
create_backup() {
@@ -7,10 +7,24 @@ create_backup() {
local output_dir="${2:-$BACKUP_DIR}"
local timestamp
timestamp=$(date +%Y%m%d_%H%M%S)
local backup_name="gotelegram_backup_${timestamp}"
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 'Собираю конфигурацию...')"
@@ -20,10 +34,16 @@ create_backup() {
cp "$TELEMT_CONFIG" "$tmp_dir/config.toml"
fi
# GoTelegram конфиг
# goTelegram Pro конфиг
if [ -f "$GOTELEGRAM_CONFIG" ]; then
cp "$GOTELEGRAM_CONFIG" "$tmp_dir/gotelegram.json"
fi
if [ -f "$GOTELEGRAM_DIR/disabled_users.json" ]; then
cp "$GOTELEGRAM_DIR/disabled_users.json" "$tmp_dir/disabled_users.json" 2>/dev/null
fi
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
@@ -35,23 +55,61 @@ create_backup() {
cp "$NGINX_SITE_CONF" "$tmp_dir/nginx.conf"
fi
# SSL сертификаты
# 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/certs"
cp "/etc/letsencrypt/live/$domain/fullchain.pem" "$tmp_dir/certs/" 2>/dev/null
cp "/etc/letsencrypt/live/$domain/privkey.pem" "$tmp_dir/certs/" 2>/dev/null
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
# Шаблон сайта (если есть)
if [ -d "$WEBSITE_ROOT" ] && [ -f "$WEBSITE_ROOT/index.html" ]; then
mkdir -p "$tmp_dir/site"
cp -r "$WEBSITE_ROOT"/* "$tmp_dir/site/"
cp -a "$WEBSITE_ROOT/." "$tmp_dir/site/"
log_dim "$(_t_or backup_site_included 'Шаблон сайта включён')"
fi
# 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"
[ -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
# 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
# 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
# Метаданные
local ip mode engine lang port domain
ip=$(get_server_ip)
@@ -65,7 +123,7 @@ create_backup() {
cat > "$tmp_dir/metadata.json" << EOMETA
{
"backup_version": "1.1",
"backup_version": "1.6",
"gotelegram_version": "$GOTELEGRAM_VERSION",
"created_at": "$(date -Iseconds)",
"hostname": "$(hostname)",
@@ -132,6 +190,7 @@ EOMETA
restore_backup() {
local backup_file="$1"
local password="$2"
local assume_yes="$3"
if [ ! -f "$backup_file" ]; then
if type tf &>/dev/null; then
@@ -177,6 +236,18 @@ restore_backup() {
backup_dir=$(find "$tmp_dir" -maxdepth 1 -type d -name "gotelegram_backup_*" | head -1)
[ -z "$backup_dir" ] && backup_dir="$tmp_dir"
# 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
local bk_version bk_mode bk_ip bk_lang bk_date
@@ -192,7 +263,7 @@ restore_backup() {
echo ""
fi
if ! confirm "$(_t_or backup_confirm_restore 'Восстановить конфигурацию? Текущие настройки будут перезаписаны.')"; then
if [ "$assume_yes" != "yes" ] && ! confirm "$(_t_or backup_confirm_restore 'Восстановить конфигурацию? Текущие настройки будут перезаписаны.')"; then
rm -rf "$tmp_dir"
return 0
fi
@@ -209,12 +280,29 @@ restore_backup() {
log_success "$(_t_or backup_restored_telemt 'telemt конфиг восстановлен')"
fi
# Восстанавливаем GoTelegram конфиг
# Восстанавливаем goTelegram Pro конфиг
if [ -f "$backup_dir/gotelegram.json" ]; then
mkdir -p "$GOTELEGRAM_DIR"
cp "$backup_dir/gotelegram.json" "$GOTELEGRAM_CONFIG"
log_success "$(_t_or backup_restored_gotelegram 'GoTelegram конфиг восстановлен')"
fi
if [ -f "$backup_dir/disabled_users.json" ]; then
mkdir -p "$GOTELEGRAM_DIR"
cp "$backup_dir/disabled_users.json" "$GOTELEGRAM_DIR/disabled_users.json"
chmod 600 "$GOTELEGRAM_DIR/disabled_users.json" 2>/dev/null || true
fi
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
@@ -231,8 +319,14 @@ restore_backup() {
log_success "$(_t_or backup_restored_nginx 'nginx конфиг восстановлен')"
fi
# Восстанавливаем SSL
if [ -d "$backup_dir/certs" ]; then
# Восстанавливаем 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
@@ -246,16 +340,60 @@ restore_backup() {
# Восстанавливаем шаблон сайта
if [ -d "$backup_dir/site" ]; then
mkdir -p "$WEBSITE_ROOT"
cp -r "$backup_dir/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
# Восстанавливаем 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
# Восстанавливаем состояние бота
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
# Очистка
rm -rf "$tmp_dir"
@@ -278,7 +416,7 @@ list_backups() {
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 date_str name
@@ -297,11 +435,11 @@ list_backups() {
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
@@ -312,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 ""
@@ -351,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

View 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.6"
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,6 +469,53 @@ check_port() {
return 1 # свободен
}
detect_3xui() {
if systemctl list-unit-files 2>/dev/null | grep -Eq '^(x-ui|3x-ui)\.service'; then
return 0
fi
[ -d /etc/x-ui ] || [ -d /usr/local/x-ui ] || [ -f /etc/x-ui/x-ui.db ]
}
detect_3xui_443_listener() {
ss -ltnp 2>/dev/null | grep -E '(:|])443[[:space:]]' | grep -Eiq '(xray|x-ui|3x-ui)'
}
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
goTelegram Pro detected that 3x-ui/Xray already owns TCP/443. Two independent
processes cannot bind the same IP:port at the same time. A safe shared setup
needs one front TLS/SNI dispatcher on 443 and internal backends, for example:
- dispatcher: 0.0.0.0:443 (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
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.
goTelegram Pro can generate the dispatcher with:
source /opt/gotelegram/lib/shared443.sh
shared443_enable <gotelegram-domain> <xray-sni-domain> 127.0.0.1:9443
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() {
local min_mb="${1:-500}"
local avail_mb

View File

@@ -1,5 +1,5 @@
#!/bin/bash
# GoTelegram v2.4 — i18n engine
# GoTelegram v2.5.0 — i18n engine
# Internationalization support: EN (English) / RU (Русский)
#
# Usage:

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

View File

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

248
lib/shared443.sh Normal file
View 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

View File

@@ -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
@@ -277,25 +361,103 @@ show_traffic_stats() {
} >&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"
@@ -350,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")
@@ -357,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
@@ -377,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
}
@@ -401,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

View File

@@ -1,5 +1,5 @@
#!/bin/bash
# GoTelegram v2.2 — Управление telemt binary
# GoTelegram v2.5.0 — Управление telemt binary
# Скачивание, обновление, запуск, остановка через systemd
TELEMT_GITHUB="telemt/telemt"
@@ -159,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
@@ -195,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
@@ -223,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
}

View 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}"

View File

@@ -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)

View File

@@ -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 "Шаблон сайта обновлён"
}

View 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
View 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()