mirror of
https://github.com/anten-ka/gotelegram_pro.git
synced 2026-05-19 16:46:03 +00:00
Compare commits
28 Commits
v2.3.1
...
rollback-a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4fec096676 | ||
|
|
7075ff8696 | ||
|
|
0e38c2b5b6 | ||
|
|
9d9d12e150 | ||
|
|
8f249c35e5 | ||
|
|
bb2502e1fc | ||
|
|
b10ea54ce9 | ||
|
|
2f3045bcc0 | ||
|
|
eb5175ccab | ||
|
|
3403975636 | ||
|
|
3919f201f5 | ||
|
|
4b63b79184 | ||
|
|
e9af6e969f | ||
|
|
724eeb92d9 | ||
|
|
fc28a1a099 | ||
|
|
7b53566dad | ||
|
|
6b206a1697 | ||
|
|
6dc0000013 | ||
|
|
663a5a2aae | ||
|
|
32f204c871 | ||
|
|
3495ab5b0f | ||
|
|
194fb32fec | ||
|
|
45e5cbabea | ||
|
|
694f18c22f | ||
|
|
0d087831d8 | ||
|
|
9c084f37ec | ||
|
|
63b2fc3717 | ||
|
|
fedc8f77fe |
29
.gitignore
vendored
29
.gitignore
vendored
@@ -1,29 +0,0 @@
|
||||
# Environment
|
||||
.env
|
||||
*.env.local
|
||||
|
||||
# Python
|
||||
__pycache__/
|
||||
*.pyc
|
||||
venv/
|
||||
.venv/
|
||||
|
||||
# Backups (contain secrets)
|
||||
backups/
|
||||
*.tar.gz
|
||||
*.tar.gz.enc
|
||||
*.sha256
|
||||
|
||||
# Temp
|
||||
/tmp/
|
||||
*.tmp
|
||||
*.swp
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
*.code-workspace
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
668
DOCS_AI.md
Normal file
668
DOCS_AI.md
Normal file
@@ -0,0 +1,668 @@
|
||||
# GoTelegram Pro — техническая документация для ИИ-агентов
|
||||
|
||||
**Версия:** 2.4.3
|
||||
**Репозиторий:** `anten-ka/gotelegram_pro`
|
||||
**Активная ветка:** `alfa-test` (ветка `test` заморожена и содержит stable для конечных пользователей)
|
||||
**Целевая ОС:** Ubuntu 20.04+, Debian 11+
|
||||
|
||||
Этот документ описывает устройство проекта с максимальным количеством нюансов — все ловушки, на которые мы уже наступали, все причины «почему именно так», формат всех внешних интерфейсов, и checklist действий для типовых задач. Цель: чтобы новый агент мог продолжить работу без повторения ошибок и без регрессий.
|
||||
|
||||
---
|
||||
|
||||
## 1. Общая картина
|
||||
|
||||
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`.
|
||||
3. **Telegram-бота** (`gotelegram-bot/bot.py`, python-telegram-bot v21+) — управление прокси со смартфона: статус/ссылка/QR/перезапуск/бекап/смена режима/смена домена/смена шаблона/переключение языка per-user (RU/EN).
|
||||
|
||||
Архитектура stealth-режима:
|
||||
```
|
||||
Клиент Telegram ──┐
|
||||
├─► anten-ka.com:443 (telemt, 0.0.0.0:443)
|
||||
Обычный браузер ───┘ │
|
||||
│ dns_overrides:
|
||||
▼ "anten-ka.com:8443:127.0.0.1"
|
||||
127.0.0.1:8443 (nginx)
|
||||
│
|
||||
▼
|
||||
/var/www/gotelegram-site/ (HTML шаблон)
|
||||
```
|
||||
|
||||
Lite-режим (без домена):
|
||||
```
|
||||
Клиент Telegram ─► IP:443 (telemt) ─► Telegram DC
|
||||
Fake-TLS эмулируется (tls_emulation=true), mask-трафик падает никуда
|
||||
tls_domain = google.com (или любой из QUICK_DOMAINS)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Репозиторий и ветки
|
||||
|
||||
```
|
||||
anten-ka/gotelegram_pro
|
||||
├── test ← frozen stable, пользователи ставятся отсюда через bootstrap.sh
|
||||
└── alfa-test ← активная разработка, пуши туда
|
||||
```
|
||||
|
||||
**Правило коммитов (из auto-memory):** все новые изменения идут ТОЛЬКО в `alfa-test`. `test` не трогаем без явной команды пользователя. Когда пользователь в диалоге скажет «влей в stable» — тогда мёржим alfa-test → test.
|
||||
|
||||
**Инструменты для коммитов (специфика окружения):**
|
||||
- `git push` через Windows Git CLI **не работает** — credential helper вешает процесс.
|
||||
- Linux sandbox **не имеет доступа к github.com** — прокси возвращает 403 на raw.githubusercontent.com и api.github.com.
|
||||
- **Единственный работающий путь:** Python-скрипт через GitHub REST API, запускаемый через Desktop Commander с `shell="cmd.exe"` (НЕ powershell, иначе не захватывается stdout).
|
||||
- PAT токен — см. CLAUDE.md.
|
||||
- Workflow: `POST git/blobs` для каждого файла → `POST git/trees` (с `base_tree` от текущего HEAD) → `POST git/commits` (parents=[текущий HEAD]) → `PATCH git/refs/heads/alfa-test` (sha=новый commit) → при необходимости `PATCH git/refs/tags/vX.Y.Z`.
|
||||
- **Важно про `base_tree`:** при частичном обновлении (1-2 файла) ОБЯЗАТЕЛЬНО передавать `base_tree` — иначе дерево получится только из переданных файлов, и все остальные файлы пропадут из коммита. `base_tree` можно опускать только при коммите с полным набором файлов.
|
||||
|
||||
---
|
||||
|
||||
## 3. Карта файлов
|
||||
|
||||
```
|
||||
install.sh главная точка входа, CLI-меню из 14 пунктов
|
||||
install_gotelegram_bot.sh legacy-установщик бота (функционал продублирован в install.sh)
|
||||
bootstrap.sh установщик приватного репо через raw.githubusercontent.com с PAT
|
||||
templates_catalog.json каталог 1801 шаблонов, 18 категорий, 4 источника (~460KB)
|
||||
DOCS_HUMAN.md документация для пользователя (этот каталог)
|
||||
DOCS_AI.md этот файл
|
||||
|
||||
lib/common.sh цвета, log_*, confirm, select_option, _valid_ip, config_get,
|
||||
save_gotelegram_config, get_server_ip, run_with_spinner,
|
||||
version, GOTELEGRAM_VERSION, пути
|
||||
lib/telemt.sh download_telemt, install_telemt_full, start/stop/restart_telemt,
|
||||
update_telemt, remove_telemt, telemt_status, telemt_logs,
|
||||
install_telemt_service (systemd юнит)
|
||||
lib/telemt_config.sh generate_telemt_toml (TOML v3), generate_proxy_link,
|
||||
get_config_value, validate_telemt_config, QUICK_DOMAINS[],
|
||||
select_quick_domain, select_port, show_proxy_info[_pro]
|
||||
lib/templates_catalog.sh download_template, select_category, select_template,
|
||||
show_template_preview, поддержка 5 источников
|
||||
lib/website.sh nginx + certbot + деплой шаблона
|
||||
lib/backup.sh create_backup, restore_backup, list_backups
|
||||
lib/i18n.sh t(), tf(), switch_language(), загрузка lang/ru.sh и lang/en.sh
|
||||
lib/lang/ru.sh 328 i18n ключей на русском
|
||||
lib/lang/en.sh 328 i18n ключей на английском
|
||||
lib/stats.sh телеметрия (опциональная)
|
||||
|
||||
gotelegram-bot/bot.py Telegram-бот (python-telegram-bot v21+)
|
||||
gotelegram-bot/config.example.env
|
||||
gotelegram-bot/requirements.txt
|
||||
gotelegram-bot/README.md
|
||||
gotelegram-bot/locales/*.json 99 ключей i18n для бота с per-user persistence
|
||||
```
|
||||
|
||||
**Где что лежит на VPS после установки:**
|
||||
| Что | Путь |
|
||||
| --- | --- |
|
||||
| Репо-скрипты | `/opt/gotelegram/` |
|
||||
| Symlink запуска | `/usr/local/bin/gotelegram` → `/opt/gotelegram/install.sh` |
|
||||
| Конфиг GoTelegram (JSON) | `/opt/gotelegram/config.json` |
|
||||
| Конфиг telemt | `/etc/telemt/config.toml` |
|
||||
| Бинарник telemt | `/usr/local/bin/telemt` |
|
||||
| Systemd юнит telemt | `/etc/systemd/system/telemt.service` |
|
||||
| Bot | `/opt/gotelegram-bot/` |
|
||||
| Systemd юнит бота | `/etc/systemd/system/gotelegram-bot.service` |
|
||||
| Сайт | `/var/www/gotelegram-site/` |
|
||||
| nginx конфиг | `/etc/nginx/sites-available/gotelegram` |
|
||||
| nginx enabled | `/etc/nginx/sites-enabled/gotelegram` |
|
||||
| Бекапы | `/opt/gotelegram/backups/` |
|
||||
| Лог GoTelegram | `/var/log/gotelegram.log` |
|
||||
| Логи telemt | `journalctl -u telemt` |
|
||||
| Логи бота | `journalctl -u gotelegram-bot` |
|
||||
|
||||
---
|
||||
|
||||
## 4. Формат telemt v3 TOML (КРИТИЧЕСКИ ВАЖНО)
|
||||
|
||||
telemt v3.3.39 использует собственный TOML-формат, **несовместимый с mtg и mtproto-proxy**. Попытка скормить ему старый формат (`[security]`, `[[users]]`, `[listen] bind_to`) приводит к тому, что telemt молча игнорирует секции и запускается с дефолтами → клиенты отваливаются с ошибкой SNI.
|
||||
|
||||
### Минимальный рабочий конфиг для Lite-режима
|
||||
|
||||
```toml
|
||||
[server]
|
||||
port = 443
|
||||
listen_addr_ipv4 = "0.0.0.0"
|
||||
|
||||
[censorship]
|
||||
tls_domain = "google.com"
|
||||
mask = true
|
||||
mask_port = 443
|
||||
tls_emulation = true # true → telemt сам эмулирует TLS handshake от имени tls_domain
|
||||
|
||||
[access.users]
|
||||
main = "HEX_SECRET_32" # 16 байт = 32 hex символа, БЕЗ префикса ee
|
||||
```
|
||||
|
||||
### Минимальный рабочий конфиг для Pro-режима (stealth)
|
||||
|
||||
```toml
|
||||
[server]
|
||||
port = 443
|
||||
listen_addr_ipv4 = "0.0.0.0"
|
||||
|
||||
[censorship]
|
||||
tls_domain = "anten-ka.com"
|
||||
mask = true
|
||||
mask_port = 8443
|
||||
tls_emulation = false # false → mask-трафик идёт на mask_port, nginx имеет свой Let's Encrypt cert
|
||||
|
||||
[access.users]
|
||||
main = "HEX_SECRET_32"
|
||||
|
||||
[network]
|
||||
dns_overrides = ["anten-ka.com:8443:127.0.0.1"]
|
||||
# формат host:port:ip — именно три поля через двоеточие
|
||||
```
|
||||
|
||||
### Поля по секциям (полный набор, который telemt принимает в дефолтном gen-config)
|
||||
|
||||
- `[general]` — modes, telemetry, links (опционально, можно не трогать).
|
||||
- `[general.modes]` — включение/отключение режимов прокси.
|
||||
- `[general.telemetry]` — отправка статистики (по умолчанию выключено).
|
||||
- `[general.links]` — формат публикуемых ссылок.
|
||||
- `[server]` — `port`, `listen_addr_ipv4`, `listen_addr_ipv6`.
|
||||
- `[server.api]` — HTTP-админка (не используем).
|
||||
- `[server.conntrack_control]` — ограничения на conntrack.
|
||||
- `[timeouts]` — таймауты handshake, idle и т.д.
|
||||
- `[network]` — `stun_servers`, `dns_overrides`.
|
||||
- `[censorship]` — `tls_domain`, `mask`, `mask_port`, `tls_emulation`, `unknown_sni_action` (значения: `Drop`, `Proxy`).
|
||||
- `[censorship.tls_fetch]` — параметры получения реального cert от tls_domain (используется только если `tls_emulation=false` И нет dns_overrides).
|
||||
- `[access]` — rate limits, доступ.
|
||||
- `[access.users]` — таблица `name = "secret"` (секрет — 32 hex).
|
||||
|
||||
### Почему `unknown_sni_action = Drop` нас кусал
|
||||
|
||||
В Pro-режиме при старом конфиге с `tls_domain = anten-ka.com` telemt дропает всех клиентов, которые приходят с другим SNI. Если потом перегенерировать конфиг в Lite (`tls_domain = google.com`), но НЕ перезапустить telemt полноценно (`systemctl start` no-op на живом сервисе), то у telemt в памяти остаётся старое `anten-ka.com`, и клиенты с SNI=google.com дропаются. Симптом пользователя: «ключ Lite не работает, telemt живой». Фикс — в `start_telemt()`, см. раздел 10, баг #23.
|
||||
|
||||
### Fake-TLS секрет (ee-формат)
|
||||
|
||||
Что присылается клиенту в ссылке `tg://proxy?...`:
|
||||
|
||||
```
|
||||
секрет_в_ссылке = "ee" + <hex_secret_32chars> + <hex(tls_domain)>
|
||||
```
|
||||
|
||||
Пример для secret=`2de6920b1e17ccd440933ba0600f578f6` (длинное — 32 hex) и domain=`google.com`:
|
||||
- hex(google.com) = `676f6f676c652e636f6d`
|
||||
- итог: `ee2de6920b1e17ccd440933ba0600f578f676f6f676c652e636f6d`
|
||||
|
||||
В конфиге telemt (`[access.users] main = ...`) секрет хранится **без** префикса `ee` и **без** hex-домена. telemt сам склеивает при handshake. Функция `generate_proxy_link()` в `lib/telemt_config.sh` делает эту конкатенацию при формировании ссылки.
|
||||
|
||||
### Systemd юнит
|
||||
|
||||
Генерируется функцией `install_telemt_service()` в `lib/telemt.sh`:
|
||||
```ini
|
||||
[Unit]
|
||||
Description=GoTelegram MTProxy (telemt engine)
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=root
|
||||
ExecStart=/usr/local/bin/telemt run /etc/telemt/config.toml
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
LimitNOFILE=65535
|
||||
NoNewPrivileges=true
|
||||
ProtectSystem=full
|
||||
ProtectHome=true
|
||||
PrivateTmp=true
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
- `ProtectSystem=full` (НЕ `strict`!) — telemt пишет cache-файлы, при `strict` он падает.
|
||||
- `User=root` — telemt нужен для bind на 443. Можно заменить на `telemt` + `AmbientCapabilities=CAP_NET_BIND_SERVICE`, но это усложнение ради эстетики.
|
||||
|
||||
---
|
||||
|
||||
## 5. Жизненный цикл установки (Lite)
|
||||
|
||||
Функция `install_lite_mode` в `install.sh`. Порядок:
|
||||
|
||||
1. `select_quick_domain` (lib/telemt_config.sh) — интерактивный выбор `tls_domain` из списка QUICK_DOMAINS[] (20 доменов: google.com, microsoft.com, cloudflare.com, apple.com, amazon.com, github.com …). Функция **ОБЯЗАТЕЛЬНО** выводит UI через `>&2`, потому что она вызывается через `$()` — см. раздел 10, баг #14.
|
||||
2. `select_port` — порт (443 по умолчанию, проверяется занятость через `check_port`).
|
||||
3. Генерация секрета: `openssl rand -hex 16` → 32 hex символа.
|
||||
4. `install_telemt_full` → `download_telemt` (скачивает бинарник из telemt/telemt GitHub Releases с фильтром `telemt-x86_64-linux-gnu.tar.gz`) → `install_telemt_service` → `enable_telemt`.
|
||||
5. `generate_telemt_toml "$secret" "$port" "lite" "$domain" "$port"` → пишет `/etc/telemt/config.toml`.
|
||||
6. `save_gotelegram_config` → пишет `/opt/gotelegram/config.json` с полями `mode=lite`, `domain=""`, `mask_host=$domain`, `secret`, `port`.
|
||||
7. `start_telemt` → **restart** если уже active (см. баг #23), иначе `start`. Проверяет `systemctl is-active` через 2 сек.
|
||||
8. `show_proxy_info` — выводит IP/port/secret/tls_domain и `tg://proxy?...` ссылку, QR через qrencode если есть.
|
||||
|
||||
## 6. Жизненный цикл установки (Pro / stealth)
|
||||
|
||||
Функция `install_pro_mode` в `install.sh`. Добавляет к Lite-флоу:
|
||||
|
||||
1. Запрос домена (`read_user_input domain`), `validate_domain` — проверка формата.
|
||||
2. `dig +short $domain` → проверка DNS = IP VPS. Если не совпало — предупреждение, но продолжаем (пользователь может знать лучше).
|
||||
3. `apt install nginx certbot python3-certbot-nginx` (если ещё нет).
|
||||
4. `interactive_template_selection` → `select_category` → `select_template` → `show_template_preview` → возвращает `tpl_id`. Все UI через `>&2` — иначе ID замусоривается.
|
||||
5. `download_template "$tpl_id"` → клонирует репо шаблона в temp, проверяет `index.html`, копирует в `/var/www/gotelegram-site/`. Для StartBootstrap проверяет `dist/index.html` — см. баг #21.
|
||||
6. Запись временного nginx-конфига на порту 80 (для challenge), reload nginx.
|
||||
7. `certbot --nginx -d $domain` → получает Let's Encrypt сертификат.
|
||||
8. Переписывание nginx-конфига: listen `127.0.0.1:8443 ssl`, root `/var/www/gotelegram-site`, используем cert от certbot.
|
||||
9. Генерация `telemt/config.toml` в режиме `pro` с `tls_emulation=false`, `mask_port=8443`, `dns_overrides = ["$domain:8443:127.0.0.1"]`.
|
||||
10. `save_gotelegram_config mode=pro domain=$domain ...`.
|
||||
11. `start_telemt` (restart-safe) → `show_proxy_info_pro` выводит ссылку через доменное имя.
|
||||
|
||||
---
|
||||
|
||||
## 7. Правила subshell-capture (железно)
|
||||
|
||||
Это **самое частое место ошибок** в проекте. Любая функция, которая вызывается через `$(...)` для получения значения, должна писать ВСЕ логи и UI в stderr (`>&2`). Единственный разрешённый вывод в stdout — это финальный `echo "$result"` с возвращаемым значением.
|
||||
|
||||
Что обязательно `>&2`:
|
||||
- **Все** `log_info/success/warning/error/step/dim` в `lib/common.sh` (это уже сделано).
|
||||
- `confirm`, `select_option` в `lib/common.sh` (сделано).
|
||||
- `select_quick_domain`, `select_port` в `lib/telemt_config.sh` (сделано).
|
||||
- `select_category`, `select_template`, `show_template_preview` в `lib/templates_catalog.sh` (сделано, баг #18 был о том, что 4 строки в show_template_preview выводили без >&2).
|
||||
- Любой `echo`/`printf` внутри `download_template` при ошибочном пути — см. баг #22 (`ls -la` без `>&2` замусоривал вывод).
|
||||
- Любая новая интерактивная функция, которую добавляешь.
|
||||
|
||||
Чек-лист когда пишешь новую функцию:
|
||||
1. Она вызывается через `$()`? → все echo/printf/log_* через `>&2`, кроме финального.
|
||||
2. Возвращает строку через echo → именно **одна** финальная строка, без лишних переводов строки.
|
||||
3. Ошибочный путь (early return) → НЕ писать в stdout, только в stderr, `return 1`.
|
||||
|
||||
---
|
||||
|
||||
## 8. SCRIPT_DIR и symlink
|
||||
|
||||
`install.sh` запускается через symlink `/usr/local/bin/gotelegram → /opt/gotelegram/install.sh`. Если наивно использовать `dirname "${BASH_SOURCE[0]}"`, получим `/usr/local/bin`, а там нет каталога `lib/`. Правильный паттерн:
|
||||
|
||||
```bash
|
||||
SCRIPT_PATH="$(readlink -f "${BASH_SOURCE[0]}")"
|
||||
SCRIPT_DIR="$(dirname "$SCRIPT_PATH")"
|
||||
```
|
||||
|
||||
`readlink -f` резолвит symlink до реального файла → `SCRIPT_DIR=/opt/gotelegram`, и `source "$SCRIPT_DIR/lib/common.sh"` работает. Это зафиксировано в баге #19.
|
||||
|
||||
---
|
||||
|
||||
## 9. Каталог шаблонов
|
||||
|
||||
Файл `templates_catalog.json`, ~460KB, ~18000 строк. Формат:
|
||||
|
||||
```json
|
||||
{
|
||||
"version": "2.4.1",
|
||||
"sources": ["html5up", "startbootstrap", "themewagon", "dawidolko"],
|
||||
"categories": ["landing", "portfolio", "agency", "saas", "restaurant", ...],
|
||||
"templates": [
|
||||
{
|
||||
"id": "h5up_massively",
|
||||
"name": "Massively",
|
||||
"source": "html5up",
|
||||
"repo": "https://github.com/html5up-inc/massively.git",
|
||||
"category": "personal",
|
||||
"preview": "https://html5up.net/massively"
|
||||
},
|
||||
...
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Источники и как они скачиваются (`download_template` в `lib/templates_catalog.sh`):
|
||||
|
||||
| Источник | Префикс id | Метод | Где index.html |
|
||||
| --- | --- | --- | --- |
|
||||
| html5up | `h5up_*` | `git clone --depth 1 $repo` | в корне |
|
||||
| startbootstrap | `sb_*` | `git clone --depth 1` → **ищем `dist/index.html`** → копируем `dist/*` | в `dist/` |
|
||||
| themewagon | `tw_*` | `git clone --depth 1 $repo` (каждый шаблон в отдельном репо) | в корне |
|
||||
| dawidolko | `dw_*` / `colorlib_*` | sparse checkout одного большого репо | в подпапке |
|
||||
|
||||
Fallback: если по известным правилам index.html не найден, `download_template` делает `find -name "index.html"` по всему клону и берёт первый. Это спасает для нестандартных структур.
|
||||
|
||||
**ColorlibHQ отброшен** — оказались WordPress-темы (PHP), index.html пустой. Не пытаться включать обратно.
|
||||
|
||||
---
|
||||
|
||||
## 10. История багов (не наступать на те же грабли)
|
||||
|
||||
Все решены в текущем HEAD (`alfa-test`). Перечисление для того, чтобы новый агент не «чинил» то, что уже починено, и понимал контекст.
|
||||
|
||||
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`.
|
||||
3. `bot.py QR cleanup` — `os.remove(qr_file)` в `finally` блоке.
|
||||
4. `common.sh _valid_ip` — проверка октетов 0-255, раньше пропускал 999.
|
||||
5. `common.sh os-release injection` — блокирует `;`, backticks, `$(`, `${` при парсинге `/etc/os-release`.
|
||||
6. `common.sh config_get` — return codes: `0=ok`, `2=файл отсутствует`, `3=невалидный JSON`.
|
||||
7. `common.sh run_with_spinner` — stderr в temp-файл, показ первых 3 строк при ошибке.
|
||||
8. `install.sh` — проверка пустого домена **перед** `validate_domain` (валидатор падал на пустой строке).
|
||||
9. `install.sh mode_choice` — санитизация `${mode_choice:-}` чтобы `set -u` не ронял скрипт.
|
||||
10. `backup.sh` — инициализация `final_file`/`tar_file`/`backup_file` перед ветвлением (иначе unset var).
|
||||
11. `bot.py XSS` — `html.escape(preview_url, quote=True)` с двойными кавычками.
|
||||
12. `common.sh` — версия была устаревшая, обновлена (сейчас 2.4.1).
|
||||
13. `install.sh` — добавлен пункт 12 меню (Telegram-бот) с подменю установка/статус/логи/настройки/удаление.
|
||||
14. **КРИТИЧЕСКИЙ subshell capture** — интерактивные функции (`select_quick_domain`, `select_port`, `select_category`, `select_template`, `show_template_preview`) выводили UI в stdout. Вызов через `$()` → меню уходило в переменную. Фикс: весь UI через `>&2`.
|
||||
15. **КРИТИЧЕСКИЙ download_telemt extract** — `find -newer` не находил извлечённый файл (таймстамп архива старше). Фикс: извлечение в отдельную директорию `$extract_dir` + проверка размера файла + fallback через `file` для определения ELF.
|
||||
16. `common.sh log_*` — все `log_info/success/warning/error/step/dim` выводят в stderr (`>&2`).
|
||||
17. `common.sh confirm/select_option` — UI через `>&2`.
|
||||
18. **КРИТИЧЕСКИЙ show_template_preview stdout leak** — 4 строки (блок «Спасибо авторам» + разделители) выводили без `>&2`. Цепочка `interactive_template_selection → select_template → show_template_preview` → мусор подмешивался к `tpl_id` → `download_template` получал мусорный ID → «шаблон не содержит index.html». Фикс: `>&2` ко всем 4 echo.
|
||||
19. **symlink SCRIPT_DIR** — `readlink -f "${BASH_SOURCE[0]}"` для резолва symlink `/usr/local/bin/gotelegram`.
|
||||
20. **bootstrap.sh** — скачивание приватного репо через `raw.githubusercontent.com/.../bootstrap.sh?token=...`, создание symlink, запуск install.sh.
|
||||
21. **КРИТИЧЕСКИЙ StartBootstrap dist/** — sb_* шаблоны хранят production в `dist/`. Фикс: клонируем в `$sb_tmp`, проверяем `dist/index.html`, копируем `dist/*` в `$clone_dir`, + universal fallback через `find`.
|
||||
22. **templates_catalog.sh ls stdout leak** — `ls -la` в блоке ошибки `download_template` шёл в stdout. Фикс: `>&2`.
|
||||
23. **КРИТИЧЕСКИЙ start_telemt stale config (v2.4.1)** — `systemctl start` это no-op для уже активного сервиса. После переустановки Lite поверх Pro конфиг на диске менялся, но telemt держал в памяти старый `tls_domain=anten-ka.com` и дропал клиентов с SNI=google.com. Фикс: `start_telemt` теперь делает `restart` если сервис уже активен. См. `lib/telemt.sh:189-213`.
|
||||
24. **КРИТИЧЕСКИЙ safe_edit_message TypeError (v2.4.2)** — в iter2 аудите субагент нашёл: `cb_pro_confirm` в success-пути вызывал `safe_edit_message(query, text, ..., disable_web_page_preview=True)`, но сигнатура обёртки этот kwarg не принимала. Runtime TypeError прямо в хэппи-пути смены шаблона из бота. Фикс: добавлен `disable_web_page_preview: Optional[bool] = None` + условный форвард через `**kwargs`.
|
||||
25. **template_id field name mismatch (v2.4.2)** — `save_gotelegram_config` всегда писал ключ `template_id`, но `bot.py` читал `config.get('template')` и писал `config['template']` в `handle_text_message`. Результат: статус шаблона в боте никогда не отображался, а поле в JSON появлялось фантомно и не использовалось. Фикс: везде только `template_id`. Исторические чтения совместимы через `config.get("template_id") or config.get("template")` для legacy конфигов.
|
||||
26. **jq 1.5 compat в bot_update_config_field (v2.4.2)** — использовалось `jq '... | .updated_at = (now | todate)'`, но фильтр `now|todate` только с jq 1.6. На Debian 10 jq 1.5 падал с syntax error, config не обновлялся. Фикс: генерим timestamp через `date -Iseconds` в shell и передаём через `jq --arg t`.
|
||||
27. **Отсутствующая валидация tpl_id/domain (v2.4.2)** — `tpl_id` из `callback_data` и `domain` из текстового ввода передавались напрямую в subprocess как `--template=$x`. Defense-in-depth: в bot.py добавлены `_TPL_ID_RE = ^[A-Za-z0-9_-]{1,64}$` и `_DOMAIN_RE`, в install.sh — `validate_domain` перед `generate_telemt_toml`. Малформный ввод отвергается early.
|
||||
28. **КРИТИЧЕСКИЙ race в bot_action_dispatch (v2.4.3)** — iter3-тест с 3 параллельными `change-lite-domain` дал `{"status":"error","code":"no_secret"}` на одном из вызовов. Причина: `bot_update_config_field` делает `jq ... > tmp && mv tmp config.json`; когда параллельный процесс заходил в `get_config_value secret` в момент между `>` и `mv`, он видел пустой/частичный файл. `asyncio.Lock` в боте ловил только внутри-процессные гонки, но не CLI-level. Фикс: `flock -w 30 9 < /var/lock/gotelegram-bot-action.lock` вокруг всего диспатчера. Новый error code: `lock_timeout` (`EX_TEMPFAIL`=75).
|
||||
|
||||
---
|
||||
|
||||
## 11. Telegram-бот
|
||||
|
||||
Файл `gotelegram-bot/bot.py`, python-telegram-bot v21+, systemd сервис `gotelegram-bot.service`.
|
||||
|
||||
Ключевые моменты:
|
||||
- **Admin ID** — бот отвечает только пользователю с `ADMIN_TG_ID` из `.env`. Всё остальное игнорируется.
|
||||
- **safe_edit_message** — обёртка над `query.edit_message_text` + `query.edit_message_caption`, глотает `BadRequest: message is not modified`. Начиная с v2.4.2 принимает опциональный `disable_web_page_preview` — не забудь прокинуть его если показываешь ссылку-превью (раньше вызов с этим kwarg ловил TypeError в runtime). Используй её всегда вместо прямого вызова.
|
||||
- **Language per-user** — файл `locales/users.json` хранит `{user_id: "ru"/"en"}`. При каждом сообщении бот читает язык пользователя и подставляет строку через `t(key, lang)`.
|
||||
- **QR-коды** — генерятся в `/tmp/gotelegram_qr_*.png`, отправляются как `InputFile`, удаляются в `finally`.
|
||||
- **Шаблон preview в HTML** — URL экранируется через `html.escape(url, quote=True)` (баг #11).
|
||||
- **Системные действия (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`.
|
||||
|
||||
### 11.1 Non-interactive action bridge (install.sh ↔ bot)
|
||||
|
||||
Бот общается с CLI через жёсткий JSON-протокол. Единая точка входа — `bot_action_dispatch` в `install.sh`.
|
||||
|
||||
```bash
|
||||
/opt/gotelegram/install.sh --action=<name> [--template=ID|--domain=HOST] --json
|
||||
```
|
||||
|
||||
**Доступные action'ы:**
|
||||
- `change-template --template=<tpl_id>` — только в Pro режиме. Скачивает шаблон, деплоит в nginx, обновляет `config.json.template_id`.
|
||||
- `change-lite-domain --domain=<host>` — только в Lite режиме. Регенерит telemt TOML с новым `tls_domain`, валидирует, рестартит telemt, обновляет `config.json.{domain,mask_host}`.
|
||||
|
||||
**Формат ответа (stdout, последняя строка):**
|
||||
```json
|
||||
{"status":"success","message":"...","<extra>":"..."}
|
||||
{"status":"error","message":"...","code":"<machine_code>"}
|
||||
```
|
||||
|
||||
Коды ошибок: `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`. Это защищает от гонок при:
|
||||
1. Одновременных callback'ах внутри бота (asyncio.Lock уже ловит это, но flock — defense-in-depth).
|
||||
2. Параллельных CLI-вызовах (бот + ручной SSH, или два бот-процесса — теоретически).
|
||||
|
||||
Если таймаут лока (30 с), диспетчер возвращает `exit 75` (`EX_TEMPFAIL`) и JSON `{"status":"error","code":"lock_timeout"}`. `flock` идёт из `util-linux` — добавлен в `critical` зависимости в `ensure_deps`.
|
||||
|
||||
**История:** до v2.4.2 коллбэки были stub'ами («сделай через CLI»). В v2.4.2 подключили реальные action'ы. В iter3-тестировании на VPS обнаружилась гонка: `bot_update_config_field` делает `jq ... > tmp && mv tmp config.json`, и параллельный процесс мог прочитать `config.json` в промежутке, увидев пустоту → ошибка `no_secret`. v2.4.3 починил flock'ом.
|
||||
|
||||
---
|
||||
|
||||
## 12. i18n (bash CLI)
|
||||
|
||||
`lib/i18n.sh` экспортирует функции:
|
||||
|
||||
```bash
|
||||
t "key" # вернуть строку на текущем языке
|
||||
tf "key" arg1 arg2 # t + printf-интерполяция
|
||||
switch_language ru|en
|
||||
```
|
||||
|
||||
Под капотом — ассоциативный массив `I18N`, ключи в `lib/lang/ru.sh` и `lib/lang/en.sh`. ~328 ключей.
|
||||
|
||||
Добавление нового ключа:
|
||||
1. Придумай стабильный key (snake_case, без пробелов): `menu_install`, `error_port_busy`.
|
||||
2. Добавь строку в `lib/lang/ru.sh`: `I18N[menu_install]="Установить / обновить"`.
|
||||
3. Добавь строку в `lib/lang/en.sh`: `I18N[menu_install]="Install / update"`.
|
||||
4. В коде вызови `t menu_install`.
|
||||
5. Если есть интерполяция — используй `%s`/`%d`: `I18N[info_port]="Порт %s свободен"` + `tf info_port "$port"`.
|
||||
|
||||
Выбор языка сохраняется в `$GOTELEGRAM_CONFIG` → `config.json` → ключ `lang`. Первый запуск — интерактивный выбор.
|
||||
|
||||
---
|
||||
|
||||
## 13. Бекапы
|
||||
|
||||
`lib/backup.sh`. Собирает в `.tar.gz`:
|
||||
- `/etc/telemt/config.toml`
|
||||
- `/opt/gotelegram/config.json`
|
||||
- `/var/www/gotelegram-site/` (если есть)
|
||||
- `/etc/letsencrypt/live/<domain>/` + `/etc/letsencrypt/archive/<domain>/` (если Pro)
|
||||
- `/etc/nginx/sites-available/gotelegram` (если есть)
|
||||
|
||||
Складывает в `/opt/gotelegram/backups/backup_YYYY-MM-DD_HH-MM-SS.tar.gz`.
|
||||
|
||||
`restore_backup` разворачивает архив обратно, перезапускает telemt и nginx.
|
||||
|
||||
---
|
||||
|
||||
## 14. Checklist: как обновить один файл и запушить
|
||||
|
||||
1. Прочитай текущее состояние файла: `Read` для локальной версии + `git_get_contents` (raw.githubusercontent.com) если нужно убедиться что на GitHub то же самое.
|
||||
2. Применяй правки через `Edit` в локальном каталоге `/sessions/.../gotelegram-v2/`.
|
||||
3. Напиши `C:\Temp\push_<описание>.py`:
|
||||
```python
|
||||
import os, base64, json, urllib.request, ssl
|
||||
TOKEN = "github_pat_..."
|
||||
REPO = "anten-ka/gotelegram_pro"
|
||||
BRANCH = "alfa-test"
|
||||
API = f"https://api.github.com/repos/{REPO}"
|
||||
headers = {
|
||||
"Authorization": f"Bearer {TOKEN}",
|
||||
"Accept": "application/vnd.github+json",
|
||||
"X-GitHub-Api-Version": "2022-11-28",
|
||||
"User-Agent": "gotelegram-push",
|
||||
}
|
||||
def req(method, path, body=None):
|
||||
data = json.dumps(body).encode() if body else None
|
||||
r = urllib.request.Request(API + path, data=data, headers={**headers, "Content-Type": "application/json"}, method=method)
|
||||
return json.loads(urllib.request.urlopen(r).read())
|
||||
|
||||
# 1. current ref
|
||||
ref = req("GET", f"/git/refs/heads/{BRANCH}")
|
||||
parent_sha = ref["object"]["sha"]
|
||||
commit = req("GET", f"/git/commits/{parent_sha}")
|
||||
base_tree = commit["tree"]["sha"]
|
||||
|
||||
# 2. blobs
|
||||
files = {
|
||||
"lib/common.sh": open("C:/.../gotelegram-v2/lib/common.sh","rb").read(),
|
||||
"DOCS_AI.md": open("C:/.../gotelegram-v2/DOCS_AI.md","rb").read(),
|
||||
}
|
||||
tree_items = []
|
||||
for path, content in files.items():
|
||||
blob = req("POST", "/git/blobs", {"content": base64.b64encode(content).decode(), "encoding": "base64"})
|
||||
tree_items.append({"path": path, "mode": "100644", "type": "blob", "sha": blob["sha"]})
|
||||
|
||||
# 3. tree (С base_tree — обязательно при частичном апдейте)
|
||||
tree = req("POST", "/git/trees", {"base_tree": base_tree, "tree": tree_items})
|
||||
|
||||
# 4. commit
|
||||
new_commit = req("POST", "/git/commits", {
|
||||
"message": "v2.4.1: docs + start_telemt restart-safe",
|
||||
"tree": tree["sha"],
|
||||
"parents": [parent_sha],
|
||||
})
|
||||
|
||||
# 5. patch ref
|
||||
req("PATCH", f"/git/refs/heads/{BRANCH}", {"sha": new_commit["sha"], "force": False})
|
||||
print("pushed:", new_commit["sha"])
|
||||
```
|
||||
4. Запускай через Desktop Commander `start_process` с `shell="cmd.exe"`:
|
||||
```
|
||||
cmd /c "python C:\Temp\push_docs.py"
|
||||
```
|
||||
НЕ через PowerShell — там stdout не захватывается в нашем окружении.
|
||||
5. Проверь результат: `GET /commits/<sha>` или открой ветку в браузере вручную.
|
||||
|
||||
**Частые грабли:**
|
||||
- Забыл `base_tree` → все остальные файлы исчезли из коммита. ВСЕГДА передавай `base_tree` кроме случая «чистый коммит со всеми файлами».
|
||||
- `cp1251` в cmd ломает юникод → пиши в файл через Python с `encoding='utf-8'`, не выводи кириллицу в stdout.
|
||||
- GitHub API кеширует raw-ответы по path → при проверке обновления используй `?ref=<commit_sha>`, не ветку.
|
||||
|
||||
---
|
||||
|
||||
## 15. Checklist: тестирование на VPS
|
||||
|
||||
VPS: `95.163.176.222`, root, пароль в CLAUDE.md.
|
||||
|
||||
**Путь из Linux sandbox:** `ssh`/`sshpass` нет, `pip install paramiko` падает (прокси 403). Идём через Windows Python, где paramiko уже установлен.
|
||||
|
||||
**Хелперы:**
|
||||
- `C:\Temp\ssh_cmd.py` — однократная команда, читает из `C:\Temp\ssh_input.txt`, пишет в `C:\Temp\ssh_output.txt`.
|
||||
- `C:\Temp\ssh_a1.py` / `ssh_a2.py` / `ssh_a3.py` — для параллельных агентов (разные output-файлы).
|
||||
|
||||
**Базовый шаблон ssh_cmd.py:**
|
||||
```python
|
||||
import sys, paramiko
|
||||
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
|
||||
ssh = paramiko.SSHClient()
|
||||
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
ssh.connect("95.163.176.222", username="root", password="...", timeout=20)
|
||||
cmd = open("C:/Temp/ssh_input.txt", "r", encoding="utf-8").read()
|
||||
stdin, stdout, stderr = ssh.exec_command(cmd, timeout=60)
|
||||
out = stdout.read().decode(errors="replace")
|
||||
err = stderr.read().decode(errors="replace")
|
||||
open("C:/Temp/ssh_output.txt", "w", encoding="utf-8").write(f"STDOUT:\n{out}\nSTDERR:\n{err}")
|
||||
ssh.close()
|
||||
```
|
||||
|
||||
**Заливка одного файла через SFTP:**
|
||||
```python
|
||||
sftp = ssh.open_sftp()
|
||||
sftp.put("C:/.../lib/telemt.sh", "/opt/gotelegram/lib/telemt.sh")
|
||||
sftp.close()
|
||||
```
|
||||
|
||||
**CRLF:** файлы из GitHub API приходят с `\r\n` → ОБЯЗАТЕЛЬНО `sed -i 's/\r$//' /opt/gotelegram/install.sh /opt/gotelegram/lib/*.sh` перед запуском, иначе bash падает с `bad interpreter`.
|
||||
|
||||
**chmod:** `chmod +x /opt/gotelegram/install.sh /opt/gotelegram/install_gotelegram_bot.sh` — GitHub раздаёт как 644.
|
||||
|
||||
**Быстрая проверка live-состояния:**
|
||||
```bash
|
||||
systemctl is-active telemt nginx gotelegram-bot
|
||||
telemt --version
|
||||
cat /etc/telemt/config.toml | grep tls_domain
|
||||
journalctl -u telemt --no-pager -n 30
|
||||
curl -sk -o /dev/null -w "%{http_code}\n" https://127.0.0.1:8443
|
||||
```
|
||||
|
||||
**Проверка что Lite-ключ реально работает через Fake-TLS:**
|
||||
```python
|
||||
import ssl, socket
|
||||
ctx = ssl.create_default_context()
|
||||
ctx.check_hostname = False
|
||||
ctx.verify_mode = ssl.CERT_NONE
|
||||
with socket.create_connection(("95.163.176.222", 443), timeout=5) as s:
|
||||
with ctx.wrap_socket(s, server_hostname="google.com") as ss:
|
||||
print(ss.version(), ss.getpeercert(True)[:40])
|
||||
```
|
||||
Ожидаемо: `TLSv1.3`, сертификат с CN=`*.google.com` (его telemt эмулирует через tls_fetch).
|
||||
|
||||
---
|
||||
|
||||
## 16. Меню (install.sh)
|
||||
|
||||
```
|
||||
── Прокси ──
|
||||
1) menu_install (Lite/Pro выбор, domain, template, certbot)
|
||||
2) menu_status (telemt_status + show_proxy_info)
|
||||
3) menu_link (ссылка + QR)
|
||||
4) menu_share (текст «для друзей»)
|
||||
5) menu_restart (restart_telemt)
|
||||
6) menu_logs (telemt_logs 40)
|
||||
7) menu_change_mode (lite↔pro, смена шаблона, смена домена маскировки)
|
||||
|
||||
── Управление ──
|
||||
8) interactive_backup (create_backup)
|
||||
9) interactive_restore (select_backup + restore_backup)
|
||||
10) update_telemt (check_telemt_update → download → restart)
|
||||
11) menu_website (nginx restart, certbot renew)
|
||||
|
||||
── Бот и прочее ──
|
||||
12) menu_bot (install_bot / start / stop / logs / change_token / remove)
|
||||
13) menu_remove (только прокси / только бот / всё)
|
||||
14) menu_promo (подарочные ссылки — маркетинг)
|
||||
|
||||
0) exit
|
||||
```
|
||||
|
||||
Диспатчер в `install.sh` (`bot_action_dispatch`) принимает `--action=` для автоматизации из бота. Полный контракт описан в разделе 11.1.
|
||||
|
||||
---
|
||||
|
||||
## 17. Changelog
|
||||
|
||||
- **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` (этот файл).
|
||||
- **2.4.0** — i18n EN/RU для CLI (328 ключей) и бота (99 ключей JSON с per-user persistence). Возможность загрузить свой шаблон сайта из произвольного git-репозитория (валидация URL, таймауты, лимит размера клона).
|
||||
- **2.3.x** — каталог шаблонов расширен до 1801, 4 источника, 18 категорий. Поддержка StartBootstrap `dist/` структуры.
|
||||
- **2.2.1** — критические фиксы `$()` capture (все UI через `>&2`), StartBootstrap dist, symlink SCRIPT_DIR через `readlink -f`, XSS в HTML-превью бота, OS-release injection.
|
||||
- **2.2** — переход с mtg на telemt v3, новый TOML-формат конфига, stealth-архитектура.
|
||||
|
||||
---
|
||||
|
||||
## 18. Быстрый справочник: «хочу сделать X»
|
||||
|
||||
**Добавить новый пункт меню:**
|
||||
1. `install.sh`: добавь `menu_new_thing()`, впиши в диспатчер `case` в главном цикле.
|
||||
2. Добавь i18n ключи `menu_new_thing` в ru.sh и en.sh.
|
||||
3. Если функция интерактивная + возвращает значение → ВСЁ UI через `>&2`.
|
||||
|
||||
**Добавить новый домен в QUICK_DOMAINS:**
|
||||
- `lib/telemt_config.sh` → массив `QUICK_DOMAINS=(...)`. Убедись что домен не заблокирован ни в одной из популярных стран и действительно отвечает на 443 с валидным сертификатом (telemt при `tls_emulation=true` вынимает реальный cert через tls_fetch).
|
||||
|
||||
**Сменить версию:**
|
||||
- `lib/common.sh:6` → `GOTELEGRAM_VERSION="X.Y.Z"`.
|
||||
- `DOCS_HUMAN.md` / `DOCS_AI.md` → раздел Changelog + версия в шапке.
|
||||
- Тэг (опционально): `PATCH /git/refs/tags/vX.Y.Z` на новый commit sha.
|
||||
|
||||
**Добавить новый шаблон в каталог:**
|
||||
- Найди git-репо со статическим HTML (index.html в корне или в `dist/`).
|
||||
- Придумай id: `<source_prefix>_<name>` (например `h5up_future`, `sb_portfolio_x`).
|
||||
- Добавь запись в `templates_catalog.json` с `id`, `name`, `source`, `repo`, `category`, `preview`.
|
||||
- Убедись что `download_template` знает префикс источника (`case "$tpl_id" in h5up_*) ... sb_*) ... esac`).
|
||||
|
||||
**Отладить «ключ не работает»:**
|
||||
1. `systemctl is-active telemt` → живой?
|
||||
2. `cat /etc/telemt/config.toml` → какой `tls_domain`? Какой `port`? Какой `secret`?
|
||||
3. `journalctl -u telemt --no-pager -n 50` → есть `unknown_sni_action=Drop`? `port in use`? `failed to bind`?
|
||||
4. Сравни `tls_domain` в конфиге и hex-домен в конце ссылки клиента (`hex(domain) === суффикс секрета после ee+32hex`).
|
||||
5. Если telemt жив но дропает — **restart** (не start). Это баг #23.
|
||||
6. Если порт занят — `ss -ltnp | grep :443` → убей конкурента.
|
||||
7. Если Pro и не открывается сайт в браузере — `curl -k https://127.0.0.1:8443` (nginx жив?), `dig +short $domain` (DNS правильный?).
|
||||
|
||||
---
|
||||
|
||||
## 19. Где НЕ копаться
|
||||
|
||||
- `install_gotelegram_bot.sh` — legacy, функционал дублирован в install.sh пункте 12. Можно удалить после того как убедимся что никто им не пользуется.
|
||||
- `lib/stats.sh` — опциональная телеметрия, не критичная для работы.
|
||||
- `ColorlibHQ` в каталоге — wordpress-темы, отброшены. Не возвращать.
|
||||
- Старый формат конфига mtg (`[security]`, `[[users]]`, `bind_to`) — telemt v3 его игнорирует. Не пытаться «починить» совместимость.
|
||||
|
||||
---
|
||||
|
||||
## 20. Контрольные точки и инварианты
|
||||
|
||||
Перед любым пушем в `alfa-test`:
|
||||
1. `bash -n install.sh lib/*.sh` — синтаксис bash ОК.
|
||||
2. Все новые `$()`-вызываемые функции пишут UI через `>&2`.
|
||||
3. Все пути к lib/ идут через `$SCRIPT_DIR/lib/...`, а `SCRIPT_DIR` — через `readlink -f`.
|
||||
4. `GOTELEGRAM_VERSION` обновлена если изменения меняют поведение.
|
||||
5. Changelog в `DOCS_HUMAN.md` и `DOCS_AI.md` дополнен.
|
||||
6. Если бекап-формат изменился — прописать в `restore_backup` обратную совместимость.
|
||||
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`.
|
||||
3. `telemt_status` → running. `journalctl -u telemt` → нет ошибок. Ссылка открывается в Telegram-клиенте.
|
||||
|
||||
---
|
||||
|
||||
**Если в чём-то сомневаешься — открой `CLAUDE.md` в корне `MT-proxy/`. Там суммированы все ранее пройденные грабли и рабочие паттерны под Windows + Desktop Commander + paramiko.**
|
||||
225
DOCS_HUMAN.md
Normal file
225
DOCS_HUMAN.md
Normal file
@@ -0,0 +1,225 @@
|
||||
# GoTelegram Pro — руководство пользователя
|
||||
|
||||
**Версия:** 2.4.3
|
||||
**Репозиторий:** `anten-ka/gotelegram_pro`
|
||||
**Для кого:** владельцы VPS, которым нужен надёжный MTProxy для Telegram с маскировкой под обычный HTTPS-сайт.
|
||||
|
||||
---
|
||||
|
||||
## 1. Что это такое
|
||||
|
||||
GoTelegram Pro — это готовый менеджер прокси-сервера MTProxy для Telegram. Он делает три вещи, которые иначе пришлось бы собирать вручную:
|
||||
|
||||
1. Ставит и настраивает ядро **telemt** (это современный Rust-порт mtproto-proxy с fake-TLS маскировкой).
|
||||
2. Запускает рядом обычный HTTPS-сайт на настоящем домене, так что провайдеру со стороны всё выглядит как посещение безобидного лендинга — а на самом деле в том же соединении ходит Telegram-трафик. Это называется «stealth» или «Pro-режим».
|
||||
3. Даёт Telegram-бота, через которого можно управлять прокси со смартфона: ссылка, статус, перезапуск, бекап, смена маскировочного домена, выбор шаблона сайта.
|
||||
|
||||
Всё управляется одним меню из 14 пунктов (`gotelegram` в терминале) — не нужно лазить по конфигам.
|
||||
|
||||
---
|
||||
|
||||
## 2. Быстрый старт
|
||||
|
||||
На чистом Ubuntu/Debian VPS под root:
|
||||
|
||||
```bash
|
||||
bash <(curl -sL "https://raw.githubusercontent.com/anten-ka/gotelegram_pro/test/bootstrap.sh?token=YOUR_PAT")
|
||||
```
|
||||
|
||||
`bootstrap.sh` скачает все файлы из приватного репозитория, создаст симлинк `/usr/local/bin/gotelegram` и запустит главное меню. Через минуту команда `gotelegram` уже будет работать откуда угодно.
|
||||
|
||||
Дальше в меню:
|
||||
|
||||
- **1) Установить / обновить** — ставит прокси, спрашивает режим (Lite или Pro) и домен маскировки.
|
||||
- **2) Статус** — показывает, жив ли telemt, IP, порт, маскировку и готовую `tg://proxy?...` ссылку.
|
||||
- **3) Ссылка** — тот же ключ отдельно, вместе с QR-кодом.
|
||||
|
||||
Дальше можно открыть Telegram → Настройки → Данные и память → Прокси → добавить по ссылке. Готово.
|
||||
|
||||
---
|
||||
|
||||
## 3. Lite vs Pro — что выбрать
|
||||
|
||||
### Lite (быстрый, без домена)
|
||||
|
||||
- Работает сразу без какого-либо домена.
|
||||
- Ссылка выглядит как IP-адрес: `tg://proxy?server=95.163.176.222&port=443&secret=ee...`
|
||||
- Провайдеру видно: кто-то сходил на IP:443, TLS с SNI=google.com (или другой популярный домен, который ты выберешь из списка).
|
||||
- Минус: IP-адрес виден, и если провайдер блокирует по списку known-bad-IP, ключ перестанет работать.
|
||||
- Подходит, если домена пока нет, а пользоваться нужно уже сейчас.
|
||||
|
||||
### Pro (стелс, со своим доменом)
|
||||
|
||||
- Нужен настоящий домен, который указывает A-записью на IP VPS.
|
||||
- Провайдеру видно: HTTPS-трафик к `твой-домен.com:443` — выглядит как обычный сайт.
|
||||
- По этому же домену снаружи открывается реальный HTML-сайт (любой из 1800+ шаблонов в каталоге).
|
||||
- Внутри: telemt слушает 443, nginx слушает 127.0.0.1:8443, маскировочный трафик telemt проксирует на nginx через `dns_overrides`, так что сайт реально открывается в браузере. SSL — настоящий Let's Encrypt.
|
||||
- Ссылка: `tg://proxy?server=твой-домен.com&port=443&secret=ee...`
|
||||
- Плюс: выглядит идентично обычному сайту, провайдер не может отличить.
|
||||
- Минус: нужен домен и корректно настроенный DNS (плюс несколько минут ожидания сертификата).
|
||||
|
||||
**Короткое правило:** есть домен — ставь Pro. Нет — начни с Lite и потом переключишься через пункт меню «Сменить режим».
|
||||
|
||||
---
|
||||
|
||||
## 4. Меню целиком
|
||||
|
||||
```
|
||||
── Прокси ──
|
||||
1) Установить / обновить — первый раз или апгрейд
|
||||
2) Статус — IP, порт, маскировка, живость
|
||||
3) Ссылка — tg://proxy?... + QR
|
||||
4) Поделиться — текстовое сообщение «для друзей»
|
||||
5) Перезапуск — systemctl restart telemt
|
||||
6) Логи — последние 40 строк telemt
|
||||
7) Сменить режим / шаблон — Lite↔Pro, сменить сайт-шаблон
|
||||
|
||||
── Управление ──
|
||||
8) Бекап — tar.gz всех конфигов и ключей
|
||||
9) Восстановить — откат из бекапа
|
||||
10) Обновить telemt — скачать свежий бинарник
|
||||
11) Сайт (SSL) — ручная перегенерация сертификата
|
||||
|
||||
── Бот и прочее ──
|
||||
12) Telegram-бот — установить / настроить бота
|
||||
13) Удалить всё — снести прокси / бота / вообще всё
|
||||
14) Промо — подарочные ссылки
|
||||
|
||||
0) Выход
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Telegram-бот
|
||||
|
||||
Пункт меню **12) Telegram-бот** разворачивает отдельный Python-сервис (`python-telegram-bot` v21+), который:
|
||||
|
||||
- Показывает статус прокси, ссылку и QR.
|
||||
- Умеет перезапускать telemt прямо из чата.
|
||||
- Делает бекап и восстанавливает.
|
||||
- Переключает режим (Lite ↔ Pro).
|
||||
- Меняет маскировочный домен.
|
||||
- Меняет сайт-шаблон из каталога на 1800+ готовых HTML-шаблонов (4 источника: html5up, startbootstrap, ThemeWagon, dawidolko).
|
||||
- Поддерживает **2 языка**: русский и английский. Каждый пользователь выбирает свой, настройка сохраняется.
|
||||
|
||||
Чтобы запустить бота, нужны только два параметра — токен от `@BotFather` и твой Telegram ID (чтобы никто кроме тебя бота не использовал). Меню подсказывает где что ввести.
|
||||
|
||||
---
|
||||
|
||||
## 6. Язык интерфейса
|
||||
|
||||
CLI и бот переведены на русский и английский. Выбор языка идёт:
|
||||
|
||||
- В CLI — при первом запуске спрашивает один раз и запоминает в `config.json`.
|
||||
- В боте — отдельная кнопка «🇬🇧/🇷🇺», переключение на лету, сохранение per-user.
|
||||
|
||||
Вся логика интерфейса, ошибок, подсказок, меню — переведена.
|
||||
|
||||
---
|
||||
|
||||
## 7. Бекап и восстановление
|
||||
|
||||
Пункт 8 делает один файл `.tar.gz` со всем, что нужно: `/etc/telemt/config.toml`, `/opt/gotelegram/config.json`, данные nginx-сайта и сертификаты. По умолчанию в `/opt/gotelegram/backups/`.
|
||||
|
||||
Пункт 9 принимает такой архив и восстанавливает всё обратно (конфиг, сервис, ссылка — всё то же, что было).
|
||||
|
||||
Хорошая практика: делать бекап каждый раз перед пунктами 1 (переустановка) и 10 (обновление telemt).
|
||||
|
||||
---
|
||||
|
||||
## 8. Обновление
|
||||
|
||||
Два типа обновлений:
|
||||
|
||||
- **Обновление ядра telemt** (пункт 10) — тянет свежий бинарник с GitHub Releases `telemt/telemt`, сохраняет старый в `.bak`, перезапускает сервис. Конфиг остаётся как был.
|
||||
- **Обновление самого GoTelegram** (пункт 1) — переустанавливает скрипты и lib/. Файл `config.json` не трогается, ключ и домен не меняются.
|
||||
|
||||
Bootstrap.sh умеет сам обновлять всё, если запустить его повторно.
|
||||
|
||||
---
|
||||
|
||||
## 9. Удаление
|
||||
|
||||
Пункт **13) Удалить всё** даёт выбор:
|
||||
|
||||
- Удалить только прокси (оставить бота).
|
||||
- Удалить только бота.
|
||||
- Удалить всё целиком, включая `/opt/gotelegram`, `/etc/telemt`, symlink `gotelegram`, systemd юниты, nginx-конфиг, бекапы и сайт из `/var/www/gotelegram-site`.
|
||||
|
||||
После «удалить всё» VPS возвращается к состоянию до установки (кроме скачанных пакетов типа `jq`, `nginx`, `certbot` — они остаются).
|
||||
|
||||
---
|
||||
|
||||
## 10. Требования к VPS
|
||||
|
||||
- **ОС:** Ubuntu 20.04+ или Debian 11+ (протестировано на Ubuntu 22.04).
|
||||
- **RAM:** 512 МБ минимум, 1 ГБ комфортно (telemt сам по себе ест мало, но рядом nginx + бот).
|
||||
- **Диск:** 2 ГБ (в основном под каталог шаблонов и бекапы).
|
||||
- **Права:** root или sudo.
|
||||
- **Порты:** 443 должен быть свободен (ни apache, ни nginx, ни ничего другого не должно на нём висеть). Если занят — скрипт предупредит.
|
||||
- **Для Pro-режима:** домен с настроенным A-record на IP VPS. DNS должен отвечать ДО установки, иначе Let's Encrypt не выдаст сертификат.
|
||||
|
||||
---
|
||||
|
||||
## 11. Частые вопросы
|
||||
|
||||
**Q: Ключ перестал работать, telemt живой.**
|
||||
A: 95% случаев — это когда после переустановки telemt не перечитал свежий конфиг (было исправлено в 2.4.1, см. changelog). Перезапусти вручную: `systemctl restart telemt`. Если не помогло — смотри логи (пункт 6 меню) и проверь `/etc/telemt/config.toml` на предмет правильного `tls_domain`.
|
||||
|
||||
**Q: Pro-режим не получил сертификат.**
|
||||
A: Проверь, что домен резолвится в правильный IP: `dig +short твой-домен.com`. Должен быть IP VPS. Если DNS правильный — проверь, что 80 и 443 никем не заняты кроме telemt (certbot на момент выдачи сертификата временно занимает 80). Попробуй пункт 11 (ручная перегенерация SSL).
|
||||
|
||||
**Q: Как сменить домен маскировки в Lite-режиме?**
|
||||
A: Пункт 7 → сменить режим/шаблон. Можно также просто переустановить (пункт 1) — текущий конфиг сохранится в бекапе.
|
||||
|
||||
**Q: Бот не реагирует.**
|
||||
A: Посмотри логи бота в пункте 12 → «Логи бота». Чаще всего — неверный токен или неверный admin ID в `.env`.
|
||||
|
||||
**Q: Могу ли я поставить несколько прокси на одном VPS?**
|
||||
A: На одном IP на порту 443 — нет, telemt один. На разных портах — можно, но скрипт этого не поддерживает из коробки, нужно руками.
|
||||
|
||||
**Q: Это легально?**
|
||||
A: Сам MTProxy — да, это публичная технология из исходников Telegram. Запуск прокси, чтобы твои друзья могли пользоваться Telegram там, где он заблокирован — в большинстве юрисдикций легально. Проверь локальные законы.
|
||||
|
||||
---
|
||||
|
||||
## 12. Где что лежит
|
||||
|
||||
| Что | Где |
|
||||
| --- | --- |
|
||||
| Скрипты (`install.sh`, `lib/`) | `/opt/gotelegram/` |
|
||||
| Симлинк запуска | `/usr/local/bin/gotelegram` |
|
||||
| Конфиг telemt | `/etc/telemt/config.toml` |
|
||||
| Конфиг GoTelegram (JSON) | `/opt/gotelegram/config.json` |
|
||||
| Бинарник telemt | `/usr/local/bin/telemt` |
|
||||
| Systemd юнит | `/etc/systemd/system/telemt.service` |
|
||||
| Бот | `/opt/gotelegram-bot/` |
|
||||
| Systemd юнит бота | `/etc/systemd/system/gotelegram-bot.service` |
|
||||
| Сайт (Pro-режим) | `/var/www/gotelegram-site/` |
|
||||
| nginx site | `/etc/nginx/sites-available/gotelegram` |
|
||||
| Бекапы | `/opt/gotelegram/backups/` |
|
||||
| Лог GoTelegram | `/var/log/gotelegram.log` |
|
||||
| Логи telemt | `journalctl -u telemt` |
|
||||
| Логи бота | `journalctl -u gotelegram-bot` |
|
||||
|
||||
---
|
||||
|
||||
## 13. Контакты и развитие
|
||||
|
||||
- Баги и пожелания — issues в репозитории `anten-ka/gotelegram_pro`.
|
||||
- Владелец: Vitalii (`anten-ka`).
|
||||
- Ветки: `test` (заморожена, stable для пользователей), `alfa-test` (активная разработка).
|
||||
|
||||
---
|
||||
|
||||
## Changelog (коротко)
|
||||
|
||||
- **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`).
|
||||
- **2.4.0** — i18n EN/RU для CLI (328 ключей) и бота (99 ключей JSON с per-user persistence). Возможность загрузить свой шаблон сайта из произвольного git-репо (с валидацией URL, таймаутами, лимитом размера клона).
|
||||
- **2.3.x** — каталог шаблонов расширен до 1801, 4 источника, 18 категорий. Поддержка StartBootstrap `dist/` структуры.
|
||||
- **2.2.1** — критические фиксы `$()` capture (все UI-вывод через `>&2`), StartBootstrap dist-структура, symlink SCRIPT_DIR через `readlink -f`, XSS в HTML-превью бота, OS-release injection.
|
||||
- **2.2** — переход с mtg на telemt v3, новый TOML-формат конфига, stealth-архитектура.
|
||||
|
||||
Полный changelog — в commit-истории `anten-ka/gotelegram_pro`.
|
||||
42
bootstrap.sh
42
bootstrap.sh
@@ -5,10 +5,12 @@
|
||||
set -euo pipefail
|
||||
|
||||
REPO="anten-ka/gotelegram_pro"
|
||||
BRANCH="test"
|
||||
PAT="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"
|
||||
BRANCH="${GOTELEGRAM_BRANCH:-test}"
|
||||
PAT="${GOTELEGRAM_PAT:-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}"
|
||||
INSTALL_DIR="/opt/gotelegram"
|
||||
API="https://api.github.com/repos/${REPO}/contents"
|
||||
# Use raw.githubusercontent.com (CDN) — faster and avoids Contents API caching
|
||||
# issues that occasionally return 404 for recently added files on non-default branches.
|
||||
RAW="https://raw.githubusercontent.com/${REPO}/${BRANCH}"
|
||||
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
@@ -47,17 +49,20 @@ download_file() {
|
||||
dir=$(dirname "$local_path")
|
||||
mkdir -p "$dir"
|
||||
|
||||
local http_code
|
||||
http_code=$(curl -sL -w "%{http_code}" -o "$local_path" \
|
||||
-H "Authorization: token ${PAT}" \
|
||||
-H "Accept: application/vnd.github.raw" \
|
||||
"${API}/${remote_path}?ref=${BRANCH}")
|
||||
# Retry up to 3 times with short backoff to tolerate transient CDN hiccups
|
||||
local attempt http_code
|
||||
for attempt in 1 2 3; do
|
||||
http_code=$(curl -sL -w "%{http_code}" -o "$local_path" \
|
||||
-H "Authorization: token ${PAT}" \
|
||||
"${RAW}/${remote_path}")
|
||||
if [ "$http_code" = "200" ]; then
|
||||
return 0
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
|
||||
if [ "$http_code" != "200" ]; then
|
||||
echo -e " ${RED}✗${NC} Ошибка загрузки ${remote_path} (HTTP ${http_code})"
|
||||
return 1
|
||||
fi
|
||||
return 0
|
||||
echo -e " ${RED}✗${NC} Ошибка загрузки ${remote_path} (HTTP ${http_code})"
|
||||
return 1
|
||||
}
|
||||
|
||||
# File list
|
||||
@@ -72,14 +77,20 @@ FILES=(
|
||||
"lib/website.sh"
|
||||
"lib/templates_catalog.sh"
|
||||
"lib/stats.sh"
|
||||
"lib/i18n.sh"
|
||||
"lib/lang/en.sh"
|
||||
"lib/lang/ru.sh"
|
||||
"gotelegram-bot/bot.py"
|
||||
"gotelegram-bot/i18n.py"
|
||||
"gotelegram-bot/lang/en.json"
|
||||
"gotelegram-bot/lang/ru.json"
|
||||
"gotelegram-bot/config.example.env"
|
||||
"gotelegram-bot/requirements.txt"
|
||||
"gotelegram-bot/README.md"
|
||||
)
|
||||
|
||||
echo -e " ${CYAN}↻${NC} Загрузка файлов в ${INSTALL_DIR}..."
|
||||
mkdir -p "${INSTALL_DIR}/lib" "${INSTALL_DIR}/gotelegram-bot"
|
||||
mkdir -p "${INSTALL_DIR}/lib/lang" "${INSTALL_DIR}/gotelegram-bot/lang"
|
||||
|
||||
failed=0
|
||||
for f in "${FILES[@]}"; do
|
||||
@@ -101,7 +112,8 @@ fi
|
||||
echo -e " ${CYAN}↻${NC} Настройка прав..."
|
||||
chmod +x "${INSTALL_DIR}/install.sh" "${INSTALL_DIR}/install_gotelegram_bot.sh"
|
||||
chmod +x "${INSTALL_DIR}"/lib/*.sh
|
||||
sed -i 's/\r$//' "${INSTALL_DIR}/install.sh" "${INSTALL_DIR}/install_gotelegram_bot.sh" "${INSTALL_DIR}"/lib/*.sh 2>/dev/null
|
||||
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
|
||||
|
||||
# Create symlink
|
||||
ln -sf "${INSTALL_DIR}/install.sh" /usr/local/bin/gotelegram
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
142
gotelegram-bot/i18n.py
Normal file
142
gotelegram-bot/i18n.py
Normal file
@@ -0,0 +1,142 @@
|
||||
"""
|
||||
GoTelegram v2.4 Bot — i18n module
|
||||
Provides per-user language preferences and a simple t()/tf() API.
|
||||
|
||||
Usage:
|
||||
from i18n import t, tf, set_user_lang, get_user_lang, get_language_name
|
||||
|
||||
msg = t(user_id, "menu_status")
|
||||
msg = tf(user_id, "backup_created_fmt", filename)
|
||||
|
||||
Language files live next to this module in lang/<code>.json.
|
||||
Per-user choices are persisted to USER_LANG_FILE (one JSON dict: user_id -> code).
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Dict, Optional
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# ── Paths ─────────────────────────────────────────────────────────────────
|
||||
_MODULE_DIR = Path(__file__).resolve().parent
|
||||
LANG_DIR = _MODULE_DIR / "lang"
|
||||
USER_LANG_FILE = Path("/opt/gotelegram-bot/user_langs.json")
|
||||
|
||||
# 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"
|
||||
|
||||
LANG_NAMES = {
|
||||
"en": "English",
|
||||
"ru": "Русский",
|
||||
}
|
||||
|
||||
# ── Caches ────────────────────────────────────────────────────────────────
|
||||
_LANG_CACHE: Dict[str, Dict[str, str]] = {}
|
||||
_USER_LANGS: Dict[int, str] = {}
|
||||
_USER_LANGS_LOADED = False
|
||||
|
||||
|
||||
def _load_lang_file(code: str) -> Dict[str, str]:
|
||||
"""Load lang/<code>.json into the cache and return it."""
|
||||
if code in _LANG_CACHE:
|
||||
return _LANG_CACHE[code]
|
||||
path = LANG_DIR / f"{code}.json"
|
||||
try:
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
if not isinstance(data, dict):
|
||||
raise ValueError("lang file must contain a top-level object")
|
||||
_LANG_CACHE[code] = data
|
||||
return data
|
||||
except FileNotFoundError:
|
||||
logger.warning("lang file not found: %s", path)
|
||||
except Exception as e:
|
||||
logger.warning("failed to load %s: %s", path, e)
|
||||
_LANG_CACHE[code] = {}
|
||||
return _LANG_CACHE[code]
|
||||
|
||||
|
||||
def _load_user_langs() -> None:
|
||||
"""Load per-user language preferences from USER_LANG_FILE."""
|
||||
global _USER_LANGS, _USER_LANGS_LOADED
|
||||
_USER_LANGS_LOADED = True
|
||||
try:
|
||||
if USER_LANG_FILE.exists():
|
||||
with open(USER_LANG_FILE, "r", encoding="utf-8") as f:
|
||||
raw = json.load(f)
|
||||
if isinstance(raw, dict):
|
||||
_USER_LANGS = {
|
||||
int(k): v for k, v in raw.items()
|
||||
if isinstance(v, str) and v in SUPPORTED_LANGS
|
||||
}
|
||||
except Exception as e:
|
||||
logger.warning("failed to load user_langs: %s", e)
|
||||
_USER_LANGS = {}
|
||||
|
||||
|
||||
def _save_user_langs() -> None:
|
||||
"""Persist per-user language preferences."""
|
||||
try:
|
||||
USER_LANG_FILE.parent.mkdir(parents=True, exist_ok=True)
|
||||
with open(USER_LANG_FILE, "w", encoding="utf-8") as f:
|
||||
json.dump(
|
||||
{str(k): v for k, v in _USER_LANGS.items()},
|
||||
f, ensure_ascii=False, indent=2,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning("failed to save user_langs: %s", e)
|
||||
|
||||
|
||||
# ── Public API ────────────────────────────────────────────────────────────
|
||||
|
||||
def get_user_lang(user_id: Optional[int]) -> str:
|
||||
"""Return the language code for the given user (or DEFAULT_LANG)."""
|
||||
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)
|
||||
|
||||
|
||||
def set_user_lang(user_id: int, code: str) -> bool:
|
||||
"""Set the per-user language preference and persist it."""
|
||||
if not _USER_LANGS_LOADED:
|
||||
_load_user_langs()
|
||||
code = (code or "").strip().lower()
|
||||
if code not in SUPPORTED_LANGS:
|
||||
return False
|
||||
_USER_LANGS[int(user_id)] = code
|
||||
_save_user_langs()
|
||||
return True
|
||||
|
||||
|
||||
def get_language_name(code: str) -> str:
|
||||
return LANG_NAMES.get(code, code)
|
||||
|
||||
|
||||
def t(user_id: Optional[int], key: str, default: Optional[str] = None) -> str:
|
||||
"""Translate key for the given user. Falls back to English, then default/key."""
|
||||
code = get_user_lang(user_id)
|
||||
table = _load_lang_file(code)
|
||||
if key in table:
|
||||
return table[key]
|
||||
if code != "en":
|
||||
en_table = _load_lang_file("en")
|
||||
if key in en_table:
|
||||
return en_table[key]
|
||||
return default if default is not None else key
|
||||
|
||||
|
||||
def tf(user_id: Optional[int], key: str, *args, default: Optional[str] = None) -> str:
|
||||
"""Format a translated string with positional args using %-formatting."""
|
||||
template = t(user_id, key, default=default)
|
||||
try:
|
||||
return template % args if args else template
|
||||
except (TypeError, ValueError):
|
||||
return template
|
||||
132
gotelegram-bot/lang/en.json
Normal file
132
gotelegram-bot/lang/en.json
Normal file
@@ -0,0 +1,132 @@
|
||||
{
|
||||
"lang_english": "English",
|
||||
"lang_russian": "Русский",
|
||||
"lang_title": "🌐 Language",
|
||||
"lang_current": "Current language: %s",
|
||||
"lang_saved": "Language saved: %s",
|
||||
"lang_choose": "Choose your language:",
|
||||
|
||||
"welcome_title": "GoTelegram v%s",
|
||||
"welcome_subtitle": "🤖 MTProxy Management Bot",
|
||||
"welcome_powered": "Powered by telemt engine",
|
||||
"welcome_prompt": "Select an action from the menu below:",
|
||||
|
||||
"waiting_admin_title": "👋 Hi, %s!",
|
||||
"waiting_admin_body": "The bot is not configured yet.\nYour Telegram ID: <code>%s</code>\n\nAssign you as administrator?",
|
||||
"btn_yes": "✅ Yes",
|
||||
"btn_no": "❌ No",
|
||||
|
||||
"access_denied": "⛔ Access denied.\nYour ID: <code>%s</code>",
|
||||
"help_title": "GoTelegram 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",
|
||||
"menu_status": "📊 Status",
|
||||
"menu_link": "🔗 Link",
|
||||
"menu_share": "📤 Share",
|
||||
"menu_restart": "🔄 Restart",
|
||||
"menu_logs": "📋 Logs",
|
||||
"menu_change": "⚡ Change Mode/Template",
|
||||
"menu_backup": "💾 Backup",
|
||||
"menu_restore": "↩️ Restore",
|
||||
"menu_update": "📡 Update telemt",
|
||||
"menu_website": "🌐 Website/SSL",
|
||||
"menu_promo": "🎁 Promo",
|
||||
"menu_stats": "📊 Traffic Stats",
|
||||
"menu_remove": "🗑️ Remove",
|
||||
"menu_admins": "👤 Admins",
|
||||
"menu_credits": "ℹ️ Credits",
|
||||
"menu_language": "🌐 Language",
|
||||
"menu_close": "❌ Close",
|
||||
"btn_back": "⬅️ Back",
|
||||
"btn_refresh": "🔄 Refresh",
|
||||
"btn_cancel": "❌ Cancel",
|
||||
"btn_confirm": "✅ Confirm",
|
||||
|
||||
"status_checking": "⏳ Checking status...",
|
||||
"status_title": "📊 Current Status",
|
||||
"status_service": "Service",
|
||||
"status_running": "✅ Running",
|
||||
"status_stopped": "❌ Stopped",
|
||||
"status_telemt": "Telemt",
|
||||
"status_mode": "Mode",
|
||||
"status_template": "Template",
|
||||
"status_domain": "Domain",
|
||||
"status_port": "Port",
|
||||
"status_listen_port": "Listen Port",
|
||||
"status_tls_domain": "TLS Domain",
|
||||
|
||||
"logs_failed": "Failed to retrieve logs",
|
||||
"link_fetching": "⏳ Fetching link...",
|
||||
"link_unavailable": "Link unavailable (proxy not installed?)",
|
||||
"share_title": "📤 Share Proxy",
|
||||
"share_body": "Send this link to your client:",
|
||||
|
||||
"restart_title": "🔄 Restart",
|
||||
"restart_progress": "⏳ Restarting telemt...",
|
||||
"restart_ok": "✅ telemt restarted",
|
||||
"restart_fail": "❌ Restart failed",
|
||||
|
||||
"install_title": "⚙️ Install / Update",
|
||||
"install_pick_mode": "Select installation mode:",
|
||||
"install_mode_lite": "🚀 Lite (quick, no site)",
|
||||
"install_mode_pro": "🎨 Pro (stealth + website)",
|
||||
|
||||
"backup_title": "💾 Backup",
|
||||
"backup_creating": "⏳ Creating backup...",
|
||||
"backup_created_fmt": "✅ Backup created: %s",
|
||||
"backup_failed": "❌ Backup creation failed",
|
||||
"backup_list_title": "Available backups:",
|
||||
"backup_none": "No backups yet",
|
||||
"backup_restore_title": "↩️ Restore backup",
|
||||
"backup_restoring": "⏳ Restoring...",
|
||||
"backup_restored": "✅ Backup restored",
|
||||
|
||||
"update_title": "📡 Update telemt",
|
||||
"update_progress": "⏳ Updating telemt binary...",
|
||||
"update_ok": "✅ telemt updated",
|
||||
"update_fail": "❌ Update failed",
|
||||
|
||||
"website_title": "🌐 Website / SSL",
|
||||
"ssl_renew_progress": "⏳ Renewing SSL...",
|
||||
"ssl_renewed": "✅ SSL renewed",
|
||||
"ssl_renew_fail": "❌ SSL renew failed",
|
||||
"ssl_status_title": "🔒 SSL Status",
|
||||
|
||||
"remove_title": "🗑️ Remove",
|
||||
"remove_warn": "⚠️ This will stop and remove telemt, nginx site and configs. Continue?",
|
||||
"remove_progress": "⏳ Removing...",
|
||||
"remove_done": "✅ Removed",
|
||||
|
||||
"admins_title": "👤 Administrators",
|
||||
"admins_list": "Current admin IDs:",
|
||||
"admins_empty": "No admins configured",
|
||||
|
||||
"promo_title": "🎁 Promo",
|
||||
"credits_title": "ℹ️ Credits",
|
||||
|
||||
"cg_title": "🔗 Custom Git Template",
|
||||
"cg_ask_url": "Send me the HTTPS git URL of a static site repository.\nOptionally append <code>@branch</code>.",
|
||||
"cg_cloning": "⏳ Cloning %s ...",
|
||||
"cg_invalid": "❌ Invalid URL. Only HTTPS git URLs are allowed.",
|
||||
"cg_timeout": "❌ Clone timeout (repository too large or slow)",
|
||||
"cg_too_big": "❌ Repository too large (>100MB)",
|
||||
"cg_no_index": "❌ No index.html found in repository",
|
||||
"cg_ok_fmt": "✅ Custom template downloaded: %s",
|
||||
|
||||
"stats_title": "Statistics",
|
||||
"stats_unavailable": "Data unavailable. Make sure stats module is enabled.",
|
||||
"stats_traffic_title": "Traffic statistics",
|
||||
"stats_proxy_label": "Proxy (telemt)",
|
||||
"stats_site_label": "Site (nginx)",
|
||||
"stats_hdr_period": "Period",
|
||||
"stats_hdr_traffic": "Traffic",
|
||||
"stats_hdr_rate": "Rate",
|
||||
"stats_1min": "1 min",
|
||||
"stats_5min": "5 min",
|
||||
"stats_60min": "60 min",
|
||||
"stats_1day": "1 day",
|
||||
"stats_7days": "7 days",
|
||||
"stats_30days": "30 days",
|
||||
"stats_365days": "365 days"
|
||||
}
|
||||
132
gotelegram-bot/lang/ru.json
Normal file
132
gotelegram-bot/lang/ru.json
Normal file
@@ -0,0 +1,132 @@
|
||||
{
|
||||
"lang_english": "English",
|
||||
"lang_russian": "Русский",
|
||||
"lang_title": "🌐 Язык",
|
||||
"lang_current": "Текущий язык: %s",
|
||||
"lang_saved": "Язык сохранён: %s",
|
||||
"lang_choose": "Выберите язык:",
|
||||
|
||||
"welcome_title": "GoTelegram v%s",
|
||||
"welcome_subtitle": "🤖 Бот управления MTProxy",
|
||||
"welcome_powered": "На базе движка telemt",
|
||||
"welcome_prompt": "Выберите действие в меню ниже:",
|
||||
|
||||
"waiting_admin_title": "👋 Привет, %s!",
|
||||
"waiting_admin_body": "Бот ещё не настроен.\nВаш Telegram ID: <code>%s</code>\n\nНазначить вас администратором?",
|
||||
"btn_yes": "✅ Да",
|
||||
"btn_no": "❌ Нет",
|
||||
|
||||
"access_denied": "⛔ Доступ запрещён.\nВаш ID: <code>%s</code>",
|
||||
"help_title": "GoTelegram Bot — Команды",
|
||||
"help_lines": "/start — Главное меню\n/help — Эта справка\n/status — Быстрый статус\n/logs — Последние логи\n/lang — Сменить язык\n/addadmin ID — Добавить админа\n/deladmin ID — Удалить админа\n\nИспользуйте кнопки меню для остальных операций.",
|
||||
|
||||
"menu_install": "⚙️ Установить",
|
||||
"menu_status": "📊 Статус",
|
||||
"menu_link": "🔗 Ссылка",
|
||||
"menu_share": "📤 Поделиться",
|
||||
"menu_restart": "🔄 Перезапуск",
|
||||
"menu_logs": "📋 Логи",
|
||||
"menu_change": "⚡ Сменить режим/шаблон",
|
||||
"menu_backup": "💾 Бекап",
|
||||
"menu_restore": "↩️ Восстановить",
|
||||
"menu_update": "📡 Обновить telemt",
|
||||
"menu_website": "🌐 Сайт/SSL",
|
||||
"menu_promo": "🎁 Промо",
|
||||
"menu_stats": "📊 Трафик",
|
||||
"menu_remove": "🗑️ Удалить",
|
||||
"menu_admins": "👤 Админы",
|
||||
"menu_credits": "ℹ️ О проекте",
|
||||
"menu_language": "🌐 Язык",
|
||||
"menu_close": "❌ Закрыть",
|
||||
"btn_back": "⬅️ Назад",
|
||||
"btn_refresh": "🔄 Обновить",
|
||||
"btn_cancel": "❌ Отмена",
|
||||
"btn_confirm": "✅ Подтвердить",
|
||||
|
||||
"status_checking": "⏳ Проверяю статус...",
|
||||
"status_title": "📊 Текущий статус",
|
||||
"status_service": "Сервис",
|
||||
"status_running": "✅ Работает",
|
||||
"status_stopped": "❌ Остановлен",
|
||||
"status_telemt": "Telemt",
|
||||
"status_mode": "Режим",
|
||||
"status_template": "Шаблон",
|
||||
"status_domain": "Домен",
|
||||
"status_port": "Порт",
|
||||
"status_listen_port": "Порт прослушивания",
|
||||
"status_tls_domain": "TLS домен",
|
||||
|
||||
"logs_failed": "Не удалось получить логи",
|
||||
"link_fetching": "⏳ Получаю ссылку...",
|
||||
"link_unavailable": "Ссылка недоступна (прокси не установлен?)",
|
||||
"share_title": "📤 Поделиться прокси",
|
||||
"share_body": "Отправьте эту ссылку клиенту:",
|
||||
|
||||
"restart_title": "🔄 Перезапуск",
|
||||
"restart_progress": "⏳ Перезапускаю telemt...",
|
||||
"restart_ok": "✅ telemt перезапущен",
|
||||
"restart_fail": "❌ Ошибка перезапуска",
|
||||
|
||||
"install_title": "⚙️ Установка / Обновление",
|
||||
"install_pick_mode": "Выберите режим установки:",
|
||||
"install_mode_lite": "🚀 Lite (быстро, без сайта)",
|
||||
"install_mode_pro": "🎨 Pro (stealth + сайт)",
|
||||
|
||||
"backup_title": "💾 Бекап",
|
||||
"backup_creating": "⏳ Создаю бекап...",
|
||||
"backup_created_fmt": "✅ Бекап создан: %s",
|
||||
"backup_failed": "❌ Не удалось создать бекап",
|
||||
"backup_list_title": "Доступные бекапы:",
|
||||
"backup_none": "Бекапов пока нет",
|
||||
"backup_restore_title": "↩️ Восстановление бекапа",
|
||||
"backup_restoring": "⏳ Восстанавливаю...",
|
||||
"backup_restored": "✅ Бекап восстановлен",
|
||||
|
||||
"update_title": "📡 Обновление telemt",
|
||||
"update_progress": "⏳ Обновляю telemt...",
|
||||
"update_ok": "✅ telemt обновлён",
|
||||
"update_fail": "❌ Ошибка обновления",
|
||||
|
||||
"website_title": "🌐 Сайт / SSL",
|
||||
"ssl_renew_progress": "⏳ Обновляю SSL...",
|
||||
"ssl_renewed": "✅ SSL обновлён",
|
||||
"ssl_renew_fail": "❌ Ошибка обновления SSL",
|
||||
"ssl_status_title": "🔒 Статус SSL",
|
||||
|
||||
"remove_title": "🗑️ Удаление",
|
||||
"remove_warn": "⚠️ Это остановит и удалит telemt, сайт nginx и конфиги. Продолжить?",
|
||||
"remove_progress": "⏳ Удаляю...",
|
||||
"remove_done": "✅ Удалено",
|
||||
|
||||
"admins_title": "👤 Администраторы",
|
||||
"admins_list": "Текущие ID админов:",
|
||||
"admins_empty": "Админы не настроены",
|
||||
|
||||
"promo_title": "🎁 Промо",
|
||||
"credits_title": "ℹ️ О проекте",
|
||||
|
||||
"cg_title": "🔗 Свой git-шаблон",
|
||||
"cg_ask_url": "Отправьте HTTPS git-URL репозитория со статическим сайтом.\nПри желании добавьте <code>@branch</code>.",
|
||||
"cg_cloning": "⏳ Клонирую %s ...",
|
||||
"cg_invalid": "❌ Неверный URL. Разрешены только HTTPS git-URL.",
|
||||
"cg_timeout": "❌ Таймаут клонирования (репозиторий слишком большой или медленный)",
|
||||
"cg_too_big": "❌ Репозиторий слишком большой (>100МБ)",
|
||||
"cg_no_index": "❌ В репозитории не найден index.html",
|
||||
"cg_ok_fmt": "✅ Свой шаблон загружен: %s",
|
||||
|
||||
"stats_title": "Статистика",
|
||||
"stats_unavailable": "Данные недоступны. Убедитесь что модуль статистики включён.",
|
||||
"stats_traffic_title": "Статистика трафика",
|
||||
"stats_proxy_label": "Proxy (telemt)",
|
||||
"stats_site_label": "Сайт (nginx)",
|
||||
"stats_hdr_period": "Период",
|
||||
"stats_hdr_traffic": "Трафик",
|
||||
"stats_hdr_rate": "Скорость",
|
||||
"stats_1min": "1 мин",
|
||||
"stats_5min": "5 мин",
|
||||
"stats_60min": "60 мин",
|
||||
"stats_1day": "1 день",
|
||||
"stats_7days": "7 дней",
|
||||
"stats_30days": "30 дней",
|
||||
"stats_365days": "365 дней"
|
||||
}
|
||||
1241
install.sh
Executable file → Normal file
1241
install.sh
Executable file → Normal file
File diff suppressed because it is too large
Load Diff
553
lib/backup.sh
Executable file → Normal file
553
lib/backup.sh
Executable file → Normal file
@@ -1,143 +1,281 @@
|
||||
#!/bin/bash
|
||||
# GoTelegram v2.2 — Бекап и восстановление конфигурации
|
||||
# GoTelegram v2.4.9 — Unified Backup Format (UBF) v2.0
|
||||
#
|
||||
# UBF v2.0 layout (inside the tarball):
|
||||
# gotelegram_backup_YYYYMMDD_HHMMSS_<shortid>/
|
||||
# ├── metadata.json # backup_id, versions, fingerprint, ...
|
||||
# ├── secrets.json # raw_secret, faketls_secret, proxy_link, bot_token
|
||||
# ├── telemt/config.toml
|
||||
# ├── gotelegram/config.json
|
||||
# ├── gotelegram/.language
|
||||
# ├── nginx/site.conf
|
||||
# ├── letsencrypt/
|
||||
# │ ├── live/<domain>/{fullchain,privkey,chain,cert}.pem
|
||||
# │ └── renewal/<domain>.conf
|
||||
# ├── site/ (nginx document root)
|
||||
# └── bot/.env
|
||||
#
|
||||
# Backup ID format: GT-YYMMDD-<last6hex-of-raw-secret>
|
||||
# Archive name: gotelegram_backup_YYYYMMDD_HHMMSS_<shortid>.tar.gz[.enc]
|
||||
#
|
||||
# Encryption: AES-256-CBC + PBKDF2 (optional, password-based)
|
||||
# Integrity: SHA-256 sidecar file (<archive>.sha256)
|
||||
#
|
||||
# Restore path automatically detects v1.1 (legacy) vs v2.0 layouts by reading
|
||||
# metadata.json.backup_version. When restoring a v1.1 archive the script
|
||||
# immediately writes a fresh v2.0 backup alongside the old one, so subsequent
|
||||
# reinstalls can benefit from the new format.
|
||||
|
||||
# ── Создание бекапа ──────────────────────────────────────────────────────────
|
||||
# ── Utility: generate a backup ID from a raw secret ─────────────────────────
|
||||
# Format: GT-YYMMDD-<last6hex>. Deterministic per-day per-key; easy to read.
|
||||
generate_backup_id() {
|
||||
local raw_secret="$1"
|
||||
local date_part
|
||||
date_part=$(date +%y%m%d)
|
||||
local last6="000000"
|
||||
if [ -n "$raw_secret" ] && [ ${#raw_secret} -ge 6 ]; then
|
||||
last6="${raw_secret: -6}"
|
||||
last6=$(echo "$last6" | tr 'A-F' 'a-f')
|
||||
fi
|
||||
echo "GT-${date_part}-${last6}"
|
||||
}
|
||||
|
||||
# ── Utility: SHA-256 fingerprint of a raw secret ────────────────────────────
|
||||
secret_fingerprint() {
|
||||
local raw_secret="$1"
|
||||
[ -z "$raw_secret" ] && { echo ""; return; }
|
||||
printf '%s' "$raw_secret" | openssl dgst -sha256 2>/dev/null | awk '{print $NF}'
|
||||
}
|
||||
|
||||
# ── Utility: hex-encode an ASCII string (for fake-TLS secret) ───────────────
|
||||
_hex_encode() {
|
||||
printf '%s' "$1" | xxd -p | tr -d '\n'
|
||||
}
|
||||
|
||||
# ── Создание бекапа (UBF v2.0) ──────────────────────────────────────────────
|
||||
create_backup() {
|
||||
local password="$1"
|
||||
local password="${1:-}"
|
||||
local output_dir="${2:-$BACKUP_DIR}"
|
||||
|
||||
# Pull current config (so backup_id can include the real secret)
|
||||
local raw_secret domain mode engine port lang tpl_id mask_host
|
||||
raw_secret=$(config_get secret 2>/dev/null || echo "")
|
||||
domain=$(config_get domain 2>/dev/null || echo "")
|
||||
mode=$(config_get mode 2>/dev/null || echo "unknown")
|
||||
engine=$(config_get engine 2>/dev/null || echo "telemt")
|
||||
port=$(config_get port 2>/dev/null || echo "443")
|
||||
lang=$(type get_language &>/dev/null && get_language 2>/dev/null || echo "en")
|
||||
tpl_id=$(config_get template_id 2>/dev/null || echo "")
|
||||
mask_host=$(config_get mask_host 2>/dev/null || echo "")
|
||||
|
||||
# Sanitise port
|
||||
[[ "$port" =~ ^[0-9]+$ ]] || port=443
|
||||
|
||||
# Backup id / short id (last 6 of secret, or random if unknown)
|
||||
local backup_id short_id
|
||||
backup_id=$(generate_backup_id "$raw_secret")
|
||||
short_id="${backup_id##*-}"
|
||||
|
||||
local timestamp
|
||||
timestamp=$(date +%Y%m%d_%H%M%S)
|
||||
local backup_name="gotelegram_backup_${timestamp}"
|
||||
local backup_name="gotelegram_backup_${timestamp}_${short_id}"
|
||||
local tmp_dir="/tmp/${backup_name}"
|
||||
|
||||
mkdir -p "$tmp_dir" "$output_dir"
|
||||
|
||||
# Собираем файлы
|
||||
log_info "Собираю конфигурацию..."
|
||||
log_info "$(_t_or backup_collecting 'Собираю конфигурацию...')"
|
||||
|
||||
# telemt конфиг
|
||||
# ── telemt ──
|
||||
if [ -f "$TELEMT_CONFIG" ]; then
|
||||
cp "$TELEMT_CONFIG" "$tmp_dir/config.toml"
|
||||
mkdir -p "$tmp_dir/telemt"
|
||||
cp "$TELEMT_CONFIG" "$tmp_dir/telemt/config.toml"
|
||||
fi
|
||||
|
||||
# GoTelegram конфиг
|
||||
# ── gotelegram ──
|
||||
mkdir -p "$tmp_dir/gotelegram"
|
||||
if [ -f "$GOTELEGRAM_CONFIG" ]; then
|
||||
cp "$GOTELEGRAM_CONFIG" "$tmp_dir/gotelegram.json"
|
||||
cp "$GOTELEGRAM_CONFIG" "$tmp_dir/gotelegram/config.json"
|
||||
fi
|
||||
if [ -f "$GOTELEGRAM_DIR/.language" ]; then
|
||||
cp "$GOTELEGRAM_DIR/.language" "$tmp_dir/gotelegram/.language"
|
||||
fi
|
||||
|
||||
# nginx конфиг (stealth mode)
|
||||
# ── nginx ──
|
||||
if [ -f "$NGINX_SITE_CONF" ]; then
|
||||
cp "$NGINX_SITE_CONF" "$tmp_dir/nginx.conf"
|
||||
mkdir -p "$tmp_dir/nginx"
|
||||
cp "$NGINX_SITE_CONF" "$tmp_dir/nginx/site.conf"
|
||||
fi
|
||||
|
||||
# SSL сертификаты
|
||||
local domain
|
||||
domain=$(config_get domain 2>/dev/null)
|
||||
# ── Let's Encrypt (full tree: live/<d>/*.pem + renewal/<d>.conf) ──
|
||||
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
|
||||
log_dim "SSL сертификаты включены"
|
||||
mkdir -p "$tmp_dir/letsencrypt/live/$domain"
|
||||
# Follow symlinks — letsencrypt's live/ tree is symlinks into archive/
|
||||
cp -L "/etc/letsencrypt/live/$domain/"*.pem "$tmp_dir/letsencrypt/live/$domain/" 2>/dev/null
|
||||
if [ -f "/etc/letsencrypt/renewal/${domain}.conf" ]; then
|
||||
mkdir -p "$tmp_dir/letsencrypt/renewal"
|
||||
cp "/etc/letsencrypt/renewal/${domain}.conf" "$tmp_dir/letsencrypt/renewal/"
|
||||
fi
|
||||
log_dim "$(_t_or backup_ssl_included 'SSL-сертификаты включены (+ chain + renewal)')"
|
||||
fi
|
||||
|
||||
# Шаблон сайта (если есть)
|
||||
# ── Website template ──
|
||||
if [ -d "$WEBSITE_ROOT" ] && [ -f "$WEBSITE_ROOT/index.html" ]; then
|
||||
mkdir -p "$tmp_dir/site"
|
||||
cp -r "$WEBSITE_ROOT"/* "$tmp_dir/site/"
|
||||
log_dim "Шаблон сайта включён"
|
||||
cp -r "$WEBSITE_ROOT"/* "$tmp_dir/site/" 2>/dev/null
|
||||
log_dim "$(_t_or backup_site_included 'Шаблон сайта включён')"
|
||||
fi
|
||||
|
||||
# Метаданные
|
||||
local ip mode engine
|
||||
# ── Telegram bot ──
|
||||
if [ -f "$BOT_DIR/.env" ]; then
|
||||
mkdir -p "$tmp_dir/bot"
|
||||
cp "$BOT_DIR/.env" "$tmp_dir/bot/.env"
|
||||
chmod 600 "$tmp_dir/bot/.env" 2>/dev/null
|
||||
log_dim "$(_t_or backup_bot_included 'Конфиг Telegram-бота включён')"
|
||||
fi
|
||||
|
||||
# ── secrets.json ──
|
||||
local faketls_secret="" proxy_link="" bot_token=""
|
||||
if [ -n "$raw_secret" ] && [ "$mode" = "pro" ] && [ -n "$domain" ]; then
|
||||
faketls_secret="ee${raw_secret}$(_hex_encode "$domain")"
|
||||
fi
|
||||
if type generate_proxy_link &>/dev/null; then
|
||||
if [ "$mode" = "pro" ] && [ -n "$domain" ]; then
|
||||
proxy_link=$(generate_proxy_link "$domain" "$port" "$raw_secret" "$domain" 2>/dev/null || echo "")
|
||||
elif [ -n "$raw_secret" ]; then
|
||||
local ip
|
||||
ip=$(get_server_ip)
|
||||
proxy_link=$(generate_proxy_link "$ip" "$port" "$raw_secret" "$mask_host" 2>/dev/null || echo "")
|
||||
fi
|
||||
fi
|
||||
if [ -f "$BOT_DIR/.env" ]; then
|
||||
bot_token=$(grep -E '^BOT_TOKEN=' "$BOT_DIR/.env" 2>/dev/null | head -1 | cut -d= -f2-)
|
||||
bot_token="${bot_token%\"}"
|
||||
bot_token="${bot_token#\"}"
|
||||
fi
|
||||
|
||||
cat > "$tmp_dir/secrets.json" << EOSEC
|
||||
{
|
||||
"version": "1",
|
||||
"raw_secret": "${raw_secret}",
|
||||
"faketls_secret": "${faketls_secret}",
|
||||
"proxy_link": "${proxy_link}",
|
||||
"bot_token": "${bot_token}",
|
||||
"exported_at": "$(date -Iseconds)"
|
||||
}
|
||||
EOSEC
|
||||
chmod 600 "$tmp_dir/secrets.json"
|
||||
|
||||
# ── metadata.json v2.0 ──
|
||||
local ip fingerprint
|
||||
ip=$(get_server_ip)
|
||||
mode=$(config_get mode 2>/dev/null || echo "unknown")
|
||||
engine=$(config_get engine 2>/dev/null || echo "telemt")
|
||||
fingerprint=$(secret_fingerprint "$raw_secret")
|
||||
|
||||
cat > "$tmp_dir/metadata.json" << EOMETA
|
||||
{
|
||||
"backup_version": "1.0",
|
||||
"backup_version": "2.0",
|
||||
"backup_id": "${backup_id}",
|
||||
"gotelegram_version": "$GOTELEGRAM_VERSION",
|
||||
"created_at": "$(date -Iseconds)",
|
||||
"hostname": "$(hostname)",
|
||||
"ip": "$ip",
|
||||
"engine": "$engine",
|
||||
"mode": "$mode",
|
||||
"port": $(config_get port 2>/dev/null || echo "443"),
|
||||
"domain": "$(config_get domain 2>/dev/null)"
|
||||
"language": "$lang",
|
||||
"port": $port,
|
||||
"domain": "$domain",
|
||||
"template_id": "$tpl_id",
|
||||
"mask_host": "$mask_host",
|
||||
"secret_fingerprint_sha256": "$fingerprint",
|
||||
"has_secrets": true,
|
||||
"has_letsencrypt": $([ -d "$tmp_dir/letsencrypt" ] && echo true || echo false),
|
||||
"has_site": $([ -d "$tmp_dir/site" ] && echo true || echo false),
|
||||
"has_bot": $([ -d "$tmp_dir/bot" ] && echo true || echo false)
|
||||
}
|
||||
EOMETA
|
||||
|
||||
# Архивируем
|
||||
# ── Archive ──
|
||||
local tar_file="/tmp/${backup_name}.tar.gz"
|
||||
if ! tar czf "$tar_file" -C /tmp "$backup_name" 2>/dev/null; then
|
||||
log_error "Ошибка создания архива"
|
||||
rm -rf "$tmp_dir"
|
||||
rm -f "$tar_file"
|
||||
log_error "$(_t_or backup_archive_err 'Ошибка создания архива')"
|
||||
rm -rf "$tmp_dir"; rm -f "$tar_file"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [ ! -f "$tar_file" ]; then
|
||||
log_error "Архив не создан"
|
||||
log_error "$(_t_or backup_archive_missing 'Архив не создан')"
|
||||
rm -rf "$tmp_dir"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Шифруем если задан пароль
|
||||
# ── Encrypt (optional) ──
|
||||
local final_file=""
|
||||
if [ -n "$password" ]; then
|
||||
final_file="${output_dir}/${backup_name}.tar.gz.enc"
|
||||
openssl enc -aes-256-cbc -salt -pbkdf2 -in "$tar_file" -out "$final_file" -pass "pass:${password}" 2>/dev/null
|
||||
if [ $? -ne 0 ]; then
|
||||
log_error "Ошибка шифрования"
|
||||
rm -f "$tar_file"
|
||||
rm -rf "$tmp_dir"
|
||||
if ! openssl enc -aes-256-cbc -salt -pbkdf2 -in "$tar_file" -out "$final_file" -pass "pass:${password}" 2>/dev/null; then
|
||||
log_error "$(_t_or backup_encrypt_err 'Ошибка шифрования')"
|
||||
rm -f "$tar_file"; rm -rf "$tmp_dir"
|
||||
return 1
|
||||
fi
|
||||
rm -f "$tar_file"
|
||||
log_success "Бекап зашифрован (AES-256-CBC)"
|
||||
log_success "$(_t_or backup_encrypted 'Бекап зашифрован (AES-256-CBC)')"
|
||||
else
|
||||
final_file="${output_dir}/${backup_name}.tar.gz"
|
||||
mv "$tar_file" "$final_file"
|
||||
fi
|
||||
|
||||
# SHA256 подпись
|
||||
# SHA-256 sidecar
|
||||
sha256sum "$final_file" > "${final_file}.sha256" 2>/dev/null
|
||||
|
||||
# Очистка
|
||||
# Cleanup
|
||||
rm -rf "$tmp_dir"
|
||||
|
||||
local size
|
||||
size=$(du -h "$final_file" | cut -f1)
|
||||
log_success "Бекап создан: $final_file ($size)"
|
||||
|
||||
echo "" >&2
|
||||
echo -e " ${BOLD}${GREEN}✓ $(_t_or backup_created 'Бекап создан')${NC}" >&2
|
||||
echo -e " ${DIM}$(printf '─%.0s' {1..60})${NC}" >&2
|
||||
echo -e " ${WHITE}$(_t_or backup_id_label 'Backup ID'):${NC} ${BOLD}${CYAN}${backup_id}${NC}" >&2
|
||||
echo -e " ${WHITE}$(_t_or backup_file_label 'Файл'):${NC} ${final_file}" >&2
|
||||
echo -e " ${WHITE}$(_t_or backup_size_label 'Размер'):${NC} ${size}" >&2
|
||||
if [ -n "$raw_secret" ]; then
|
||||
echo -e " ${WHITE}$(_t_or backup_key_label 'Ключ в бекапе (fingerprint)'):${NC} ${DIM}${fingerprint:0:32}...${NC}" >&2
|
||||
fi
|
||||
echo -e " ${DIM}$(printf '─%.0s' {1..60})${NC}" >&2
|
||||
echo "" >&2
|
||||
|
||||
# Only the final file path on stdout (callers capture it)
|
||||
echo "$final_file"
|
||||
return 0
|
||||
}
|
||||
|
||||
# ── Восстановление из бекапа ────────────────────────────────────────────────
|
||||
# ── Восстановление из бекапа (auto-detect v1.1 vs v2.0) ─────────────────────
|
||||
restore_backup() {
|
||||
local backup_file="$1"
|
||||
local password="$2"
|
||||
local password="${2:-}"
|
||||
|
||||
if [ ! -f "$backup_file" ]; then
|
||||
log_error "Файл не найден: $backup_file"
|
||||
if type tf &>/dev/null; then
|
||||
log_error "$(tf backup_file_not_found_fmt "$backup_file")"
|
||||
else
|
||||
log_error "Файл не найден: $backup_file"
|
||||
fi
|
||||
return 1
|
||||
fi
|
||||
|
||||
local tmp_dir="/tmp/gotelegram_restore_$$"
|
||||
mkdir -p "$tmp_dir"
|
||||
|
||||
# Расшифровываем если нужно
|
||||
# ── Decrypt if needed ──
|
||||
local tar_file=""
|
||||
if echo "$backup_file" | grep -q '\.enc$'; then
|
||||
if [ -z "$password" ]; then
|
||||
echo -ne " Введите пароль от бекапа: "
|
||||
echo -ne " $(_t_or backup_enter_pass 'Введите пароль от бекапа'): " >&2
|
||||
read -rs password
|
||||
echo ""
|
||||
echo "" >&2
|
||||
fi
|
||||
tar_file="/tmp/gotelegram_restore_$$.tar.gz"
|
||||
openssl enc -aes-256-cbc -d -pbkdf2 -in "$backup_file" -out "$tar_file" -pass "pass:${password}" 2>/dev/null
|
||||
if [ $? -ne 0 ]; then
|
||||
log_error "Неверный пароль или повреждённый файл"
|
||||
if ! openssl enc -aes-256-cbc -d -pbkdf2 -in "$backup_file" -out "$tar_file" -pass "pass:${password}" 2>/dev/null; then
|
||||
log_error "$(_t_or backup_bad_pass 'Неверный пароль или повреждённый файл')"
|
||||
rm -rf "$tmp_dir" "$tar_file"
|
||||
return 1
|
||||
fi
|
||||
@@ -145,124 +283,185 @@ restore_backup() {
|
||||
tar_file="$backup_file"
|
||||
fi
|
||||
|
||||
# Распаковываем
|
||||
tar xzf "$tar_file" -C "$tmp_dir" 2>/dev/null
|
||||
if [ $? -ne 0 ]; then
|
||||
log_error "Ошибка распаковки архива"
|
||||
# ── Extract ──
|
||||
if ! tar xzf "$tar_file" -C "$tmp_dir" 2>/dev/null; then
|
||||
log_error "$(_t_or backup_extract_err 'Ошибка распаковки архива')"
|
||||
rm -rf "$tmp_dir"
|
||||
[ "$tar_file" != "$backup_file" ] && rm -f "$tar_file"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Находим папку бекапа
|
||||
# Find the single top-level dir inside the archive
|
||||
local backup_dir
|
||||
backup_dir=$(find "$tmp_dir" -maxdepth 1 -type d -name "gotelegram_backup_*" | head -1)
|
||||
backup_dir=$(find "$tmp_dir" -maxdepth 1 -mindepth 1 -type d -name "gotelegram_backup_*" | head -1)
|
||||
[ -z "$backup_dir" ] && backup_dir="$tmp_dir"
|
||||
|
||||
# Проверяем метаданные
|
||||
# ── Parse metadata.json ──
|
||||
local bk_version="1.1" bk_id="" bk_mode="" bk_domain="" bk_ip="" bk_lang="" bk_date=""
|
||||
if [ -f "$backup_dir/metadata.json" ]; then
|
||||
local bk_version bk_mode bk_ip
|
||||
bk_version=$(jq -r '.gotelegram_version // "unknown"' "$backup_dir/metadata.json")
|
||||
bk_version=$(jq -r '.backup_version // "1.1"' "$backup_dir/metadata.json")
|
||||
bk_id=$(jq -r '.backup_id // empty' "$backup_dir/metadata.json")
|
||||
bk_mode=$(jq -r '.mode // "unknown"' "$backup_dir/metadata.json")
|
||||
bk_ip=$(jq -r '.ip // "unknown"' "$backup_dir/metadata.json")
|
||||
echo ""
|
||||
echo -e " ${BOLD}${WHITE}📦 Бекап:${NC}"
|
||||
echo -e " Версия: $bk_version | Режим: $bk_mode | IP: $bk_ip"
|
||||
echo -e " Дата: $(jq -r '.created_at' "$backup_dir/metadata.json")"
|
||||
echo ""
|
||||
bk_domain=$(jq -r '.domain // empty' "$backup_dir/metadata.json")
|
||||
bk_ip=$(jq -r '.ip // "-"' "$backup_dir/metadata.json")
|
||||
bk_lang=$(jq -r '.language // "-"' "$backup_dir/metadata.json")
|
||||
bk_date=$(jq -r '.created_at // "-"' "$backup_dir/metadata.json")
|
||||
fi
|
||||
|
||||
if ! confirm "Восстановить конфигурацию? Текущие настройки будут перезаписаны."; then
|
||||
echo "" >&2
|
||||
echo -e " ${BOLD}${WHITE}📦 $(_t_or backup_label 'Бекап'):${NC}" >&2
|
||||
echo -e " ${DIM}$(printf '─%.0s' {1..60})${NC}" >&2
|
||||
if [ -n "$bk_id" ]; then
|
||||
echo -e " ${WHITE}$(_t_or backup_id_label 'Backup ID'):${NC} ${BOLD}${CYAN}${bk_id}${NC}" >&2
|
||||
fi
|
||||
echo -e " ${WHITE}$(_t_or backup_format_label 'Формат'):${NC} UBF ${bk_version}" >&2
|
||||
echo -e " ${WHITE}$(_t_or backup_mode_label 'Режим'):${NC} ${bk_mode}${bk_domain:+ | $bk_domain}" >&2
|
||||
echo -e " ${WHITE}$(_t_or backup_lang_label 'Язык'):${NC} ${bk_lang} | IP: ${bk_ip}" >&2
|
||||
echo -e " ${WHITE}$(_t_or backup_date_label 'Дата'):${NC} ${bk_date}" >&2
|
||||
echo -e " ${DIM}$(printf '─%.0s' {1..60})${NC}" >&2
|
||||
echo "" >&2
|
||||
|
||||
if ! confirm "$(_t_or backup_confirm_restore 'Восстановить конфигурацию? Текущие настройки будут перезаписаны.')"; then
|
||||
rm -rf "$tmp_dir"
|
||||
[ "$tar_file" != "$backup_file" ] && rm -f "$tar_file"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Останавливаем сервисы
|
||||
stop_telemt 2>/dev/null
|
||||
systemctl stop nginx 2>/dev/null
|
||||
|
||||
# Восстанавливаем telemt конфиг
|
||||
if [ -f "$backup_dir/config.toml" ]; then
|
||||
# ── Detect layout ──
|
||||
# v2.0 paths: telemt/config.toml, gotelegram/config.json, nginx/site.conf, letsencrypt/live/<d>/
|
||||
# v1.1 paths: config.toml, gotelegram.json, nginx.conf, certs/
|
||||
local src_telemt src_gt src_lang src_nginx src_le_live src_le_renewal src_site src_bot
|
||||
if [ "$bk_version" = "2.0" ] || [ -d "$backup_dir/telemt" ]; then
|
||||
src_telemt="$backup_dir/telemt/config.toml"
|
||||
src_gt="$backup_dir/gotelegram/config.json"
|
||||
src_lang="$backup_dir/gotelegram/.language"
|
||||
src_nginx="$backup_dir/nginx/site.conf"
|
||||
src_le_live=$(find "$backup_dir/letsencrypt/live" -maxdepth 1 -mindepth 1 -type d 2>/dev/null | head -1)
|
||||
src_le_renewal="$backup_dir/letsencrypt/renewal"
|
||||
src_site="$backup_dir/site"
|
||||
src_bot="$backup_dir/bot/.env"
|
||||
else
|
||||
src_telemt="$backup_dir/config.toml"
|
||||
src_gt="$backup_dir/gotelegram.json"
|
||||
src_lang="$backup_dir/.language"
|
||||
src_nginx="$backup_dir/nginx.conf"
|
||||
src_le_live="$backup_dir/certs" # v1.1 dumps certs flat
|
||||
src_le_renewal=""
|
||||
src_site="$backup_dir/site"
|
||||
src_bot="" # v1.1 never backed up bot
|
||||
fi
|
||||
|
||||
# ── telemt config ──
|
||||
if [ -f "$src_telemt" ]; then
|
||||
mkdir -p /etc/telemt
|
||||
cp "$backup_dir/config.toml" "$TELEMT_CONFIG"
|
||||
cp "$src_telemt" "$TELEMT_CONFIG"
|
||||
chmod 600 "$TELEMT_CONFIG"
|
||||
log_success "telemt конфиг восстановлен"
|
||||
log_success "$(_t_or backup_restored_telemt 'telemt конфиг восстановлен')"
|
||||
fi
|
||||
|
||||
# Восстанавливаем GoTelegram конфиг
|
||||
if [ -f "$backup_dir/gotelegram.json" ]; then
|
||||
# ── GoTelegram config ──
|
||||
if [ -f "$src_gt" ]; then
|
||||
mkdir -p "$GOTELEGRAM_DIR"
|
||||
cp "$backup_dir/gotelegram.json" "$GOTELEGRAM_CONFIG"
|
||||
log_success "GoTelegram конфиг восстановлен"
|
||||
cp "$src_gt" "$GOTELEGRAM_CONFIG"
|
||||
log_success "$(_t_or backup_restored_gotelegram 'GoTelegram конфиг восстановлен')"
|
||||
fi
|
||||
|
||||
# Восстанавливаем nginx конфиг
|
||||
if [ -f "$backup_dir/nginx.conf" ]; then
|
||||
# ── Language ──
|
||||
if [ -f "$src_lang" ]; then
|
||||
mkdir -p "$GOTELEGRAM_DIR"
|
||||
cp "$src_lang" "$GOTELEGRAM_DIR/.language"
|
||||
log_success "$(_t_or backup_restored_lang 'Язык интерфейса восстановлен')"
|
||||
fi
|
||||
|
||||
# ── nginx ──
|
||||
if [ -f "$src_nginx" ]; then
|
||||
mkdir -p /etc/nginx/sites-available /etc/nginx/sites-enabled
|
||||
cp "$backup_dir/nginx.conf" "$NGINX_SITE_CONF"
|
||||
cp "$src_nginx" "$NGINX_SITE_CONF"
|
||||
ln -sf "$NGINX_SITE_CONF" "$NGINX_SITE_LINK"
|
||||
log_success "nginx конфиг восстановлен"
|
||||
log_success "$(_t_or backup_restored_nginx 'nginx конфиг восстановлен')"
|
||||
fi
|
||||
|
||||
# Восстанавливаем SSL
|
||||
if [ -d "$backup_dir/certs" ]; then
|
||||
local domain
|
||||
domain=$(config_get domain 2>/dev/null)
|
||||
if [ -n "$domain" ]; then
|
||||
local cert_dir="/etc/letsencrypt/live/$domain"
|
||||
mkdir -p "$cert_dir"
|
||||
cp "$backup_dir/certs/"* "$cert_dir/" 2>/dev/null
|
||||
log_success "SSL сертификаты восстановлены"
|
||||
# ── Let's Encrypt (v2.0: full tree; v1.1: just flat certs/) ──
|
||||
if [ -n "$bk_domain" ] && [ -d "$src_le_live" ]; then
|
||||
local live_dir="/etc/letsencrypt/live/$bk_domain"
|
||||
mkdir -p "$live_dir"
|
||||
cp "$src_le_live/"*.pem "$live_dir/" 2>/dev/null
|
||||
if [ -n "$src_le_renewal" ] && [ -f "$src_le_renewal/${bk_domain}.conf" ]; then
|
||||
mkdir -p /etc/letsencrypt/renewal
|
||||
cp "$src_le_renewal/${bk_domain}.conf" "/etc/letsencrypt/renewal/"
|
||||
fi
|
||||
log_success "$(_t_or backup_restored_ssl 'SSL сертификаты восстановлены')"
|
||||
fi
|
||||
|
||||
# Восстанавливаем шаблон сайта
|
||||
if [ -d "$backup_dir/site" ]; then
|
||||
# ── Site ──
|
||||
if [ -d "$src_site" ] && [ -f "$src_site/index.html" ]; then
|
||||
mkdir -p "$WEBSITE_ROOT"
|
||||
cp -r "$backup_dir/site"/* "$WEBSITE_ROOT/"
|
||||
cp -r "$src_site"/* "$WEBSITE_ROOT/"
|
||||
chown -R www-data:www-data "$WEBSITE_ROOT" 2>/dev/null
|
||||
log_success "Шаблон сайта восстановлен"
|
||||
log_success "$(_t_or backup_restored_site 'Шаблон сайта восстановлен')"
|
||||
fi
|
||||
|
||||
# Запускаем сервисы
|
||||
if is_telemt_installed; then
|
||||
# ── Bot .env (v2.0 only) ──
|
||||
if [ -n "$src_bot" ] && [ -f "$src_bot" ]; then
|
||||
mkdir -p "$BOT_DIR"
|
||||
cp "$src_bot" "$BOT_DIR/.env"
|
||||
chmod 600 "$BOT_DIR/.env"
|
||||
log_success "$(_t_or backup_restored_bot 'Конфиг Telegram-бота восстановлен')"
|
||||
fi
|
||||
|
||||
# ── Start services ──
|
||||
if type is_telemt_installed &>/dev/null && is_telemt_installed; then
|
||||
start_telemt
|
||||
fi
|
||||
systemctl start nginx 2>/dev/null
|
||||
|
||||
# Очистка
|
||||
# ── Cleanup ──
|
||||
rm -rf "$tmp_dir"
|
||||
[ "$tar_file" != "$backup_file" ] && rm -f "$tar_file"
|
||||
|
||||
log_success "Восстановление завершено!"
|
||||
show_proxy_info
|
||||
log_success "$(_t_or backup_restore_done 'Восстановление завершено!')"
|
||||
|
||||
# ── Auto-migrate v1.1 → v2.0 ──
|
||||
if [ "$bk_version" != "2.0" ]; then
|
||||
log_info "$(_t_or backup_automigrate 'Конвертирую старый бекап в UBF v2.0...')"
|
||||
create_backup "" >/dev/null 2>&1 && \
|
||||
log_success "$(_t_or backup_migrated 'Свежий UBF v2.0 бекап сохранён в $BACKUP_DIR')"
|
||||
fi
|
||||
|
||||
type show_proxy_info &>/dev/null && show_proxy_info
|
||||
return 0
|
||||
}
|
||||
|
||||
# ── Список бекапов ───────────────────────────────────────────────────────────
|
||||
list_backups() {
|
||||
if [ ! -d "$BACKUP_DIR" ] || [ -z "$(ls -A "$BACKUP_DIR" 2>/dev/null)" ]; then
|
||||
log_info "Бекапов нет"
|
||||
log_info "$(_t_or backup_none 'Бекапов нет')"
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo -e " ${BOLD}${WHITE}📦 Доступные бекапы:${NC}"
|
||||
echo -e " ${DIM}$(printf '─%.0s' {1..60})${NC}"
|
||||
echo "" >&2
|
||||
echo -e " ${BOLD}${WHITE}📦 $(_t_or backup_list_title 'Доступные бекапы'):${NC}" >&2
|
||||
echo -e " ${DIM}$(printf '─%.0s' {1..70})${NC}" >&2
|
||||
|
||||
local i=1
|
||||
for f in "$BACKUP_DIR"/gotelegram_backup_*.tar.gz*; do
|
||||
[ -f "$f" ] || continue
|
||||
[[ "$f" == *.sha256 ]] && continue
|
||||
local size date_str name
|
||||
local size name date_str id_tail encrypted=""
|
||||
size=$(du -h "$f" | cut -f1)
|
||||
name=$(basename "$f")
|
||||
date_str=$(echo "$name" | grep -oE '[0-9]{8}_[0-9]{6}' | head -1)
|
||||
local encrypted=""
|
||||
id_tail=$(echo "$name" | grep -oE '_[0-9a-f]{6}\.tar' | head -1 | tr -d '_.tar')
|
||||
[[ "$f" == *.enc ]] && encrypted=" 🔒"
|
||||
echo -e " ${CYAN}${i})${NC} ${name} (${size})${encrypted}"
|
||||
local id_display=""
|
||||
[ -n "$id_tail" ] && id_display=" ${DIM}[...${id_tail}]${NC}"
|
||||
echo -e " ${CYAN}${i})${NC} ${name} (${size})${encrypted}${id_display}" >&2
|
||||
((i++))
|
||||
done
|
||||
echo -e " ${DIM}$(printf '─%.0s' {1..60})${NC}"
|
||||
echo -e " ${DIM}$(printf '─%.0s' {1..70})${NC}" >&2
|
||||
}
|
||||
|
||||
# ── Очистка старых бекапов ───────────────────────────────────────────────────
|
||||
@@ -276,36 +475,40 @@ cleanup_old_backups() {
|
||||
find "$BACKUP_DIR" -name "gotelegram_backup_*.tar.gz*" ! -name "*.sha256" 2>/dev/null | sort | head -n "$to_delete" | while read -r f; do
|
||||
rm -f "$f" "${f}.sha256"
|
||||
done
|
||||
log_dim "Удалено $to_delete старых бекапов (оставлено $keep)"
|
||||
if type tf &>/dev/null; then
|
||||
log_dim "$(tf backup_cleanup_fmt "$to_delete" "$keep")"
|
||||
else
|
||||
log_dim "Удалено $to_delete старых бекапов (оставлено $keep)"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# ── Интерактивный бекап ──────────────────────────────────────────────────────
|
||||
interactive_backup() {
|
||||
echo ""
|
||||
echo -e " ${BOLD}${WHITE}💾 Создание бекапа${NC}"
|
||||
echo -ne " Зашифровать бекап паролем? [Y/n]: "
|
||||
echo -e " ${BOLD}${WHITE}💾 $(_t_or backup_create_title 'Создание бекапа')${NC}"
|
||||
echo -ne " $(_t_or backup_encrypt_prompt 'Зашифровать бекап паролем?') [Y/n]: "
|
||||
read -r use_pass
|
||||
|
||||
local password=""
|
||||
if [[ ! "$use_pass" =~ ^[Nn] ]]; then
|
||||
echo -ne " Введите пароль: "
|
||||
echo -ne " $(_t_or backup_enter_pass 'Введите пароль'): "
|
||||
read -rs password
|
||||
echo ""
|
||||
echo -ne " Повторите пароль: "
|
||||
echo -ne " $(_t_or backup_repeat_pass 'Повторите пароль'): "
|
||||
read -rs password2
|
||||
echo ""
|
||||
if [ "$password" != "$password2" ]; then
|
||||
log_error "Пароли не совпадают"
|
||||
log_error "$(_t_or backup_pass_mismatch 'Пароли не совпадают')"
|
||||
return 1
|
||||
fi
|
||||
if [ ${#password} -lt 6 ]; then
|
||||
log_error "Пароль слишком короткий (минимум 6 символов)"
|
||||
log_error "$(_t_or backup_pass_short 'Пароль слишком короткий (минимум 6 символов)')"
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
|
||||
create_backup "$password"
|
||||
create_backup "$password" >/dev/null
|
||||
cleanup_old_backups
|
||||
}
|
||||
|
||||
@@ -313,7 +516,7 @@ interactive_backup() {
|
||||
interactive_restore() {
|
||||
list_backups || return 1
|
||||
|
||||
echo -ne " Номер бекапа (или путь к файлу): "
|
||||
echo -ne " $(_t_or backup_pick_prompt 'Номер бекапа (или путь к файлу)'): "
|
||||
read -r choice
|
||||
|
||||
local backup_file=""
|
||||
@@ -333,9 +536,121 @@ interactive_restore() {
|
||||
fi
|
||||
|
||||
if [ -z "$backup_file" ]; then
|
||||
log_error "Бекап не найден"
|
||||
log_error "$(_t_or backup_not_found 'Бекап не найден')"
|
||||
return 1
|
||||
fi
|
||||
|
||||
restore_backup "$backup_file"
|
||||
}
|
||||
|
||||
# ── Manual secret recovery (v2.4.9) ──────────────────────────────────────────
|
||||
# Parser accepts any of the 3 formats and emits key=value lines on stdout:
|
||||
# tg://proxy?server=X&port=Y&secret=Z → raw_secret, server, port, domain (if ee-prefix)
|
||||
# ee<32hex><hex_domain> → raw_secret, domain
|
||||
# <32hex> → raw_secret only
|
||||
# Returns 0 on success, 1 on parse failure.
|
||||
parse_manual_secret() {
|
||||
local input="$1"
|
||||
input=$(echo "$input" | tr -d ' \t\n\r')
|
||||
[ -z "$input" ] && return 1
|
||||
|
||||
local raw_secret="" domain="" server="" port=""
|
||||
|
||||
if echo "$input" | grep -q '^tg://proxy?'; then
|
||||
local qs="${input#tg://proxy?}"
|
||||
local kv k v
|
||||
local -a kvs
|
||||
IFS='&' read -ra kvs <<< "$qs"
|
||||
for kv in "${kvs[@]}"; do
|
||||
k="${kv%%=*}"
|
||||
v="${kv#*=}"
|
||||
case "$k" in
|
||||
server) server="$v" ;;
|
||||
port) port="$v" ;;
|
||||
secret) raw_secret="$v" ;;
|
||||
esac
|
||||
done
|
||||
[ -z "$raw_secret" ] && return 1
|
||||
# Strip hex-escapes that Telegram sometimes URL-encodes
|
||||
raw_secret=$(echo "$raw_secret" | tr -d '%')
|
||||
fi
|
||||
|
||||
# After pulling from URL (if any), raw_secret might still be ee-prefixed.
|
||||
# Otherwise, try the raw_secret as the whole input.
|
||||
local candidate="${raw_secret:-$input}"
|
||||
|
||||
if [[ "$candidate" =~ ^[eE][eE][0-9a-fA-F]{32}[0-9a-fA-F]*$ ]]; then
|
||||
raw_secret="${candidate:2:32}"
|
||||
local hex_domain="${candidate:34}"
|
||||
if [ -n "$hex_domain" ]; then
|
||||
local decoded
|
||||
decoded=$(echo "$hex_domain" | xxd -r -p 2>/dev/null)
|
||||
# Validate decoded looks like a domain
|
||||
if echo "$decoded" | grep -qE '^[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'; then
|
||||
domain="$decoded"
|
||||
fi
|
||||
fi
|
||||
elif [[ "$candidate" =~ ^[0-9a-fA-F]{32}$ ]]; then
|
||||
raw_secret="$candidate"
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
|
||||
[[ "$raw_secret" =~ ^[0-9a-fA-F]{32}$ ]] || return 1
|
||||
raw_secret=$(echo "$raw_secret" | tr 'A-F' 'a-f')
|
||||
|
||||
echo "raw_secret=$raw_secret"
|
||||
[ -n "$domain" ] && echo "domain=$domain"
|
||||
[ -n "$server" ] && echo "server=$server"
|
||||
[ -n "$port" ] && echo "port=$port"
|
||||
return 0
|
||||
}
|
||||
|
||||
# ── Interactive: user types their old key, we parse it ─────────────────────
|
||||
manual_secret_input() {
|
||||
echo "" >&2
|
||||
echo -e " ${BOLD}${WHITE}🔑 $(_t_or manual_secret_title 'Ввод существующего ключа')${NC}" >&2
|
||||
echo -e " ${DIM}$(printf '─%.0s' {1..60})${NC}" >&2
|
||||
echo -e " ${DIM}$(_t_or manual_secret_help1 'Поддерживаются форматы:')${NC}" >&2
|
||||
echo -e " ${DIM} • tg://proxy?server=...&port=...&secret=...${NC}" >&2
|
||||
echo -e " ${DIM} • ee<32hex><hexdomain> (fake-TLS)${NC}" >&2
|
||||
echo -e " ${DIM} • 32hex (только raw secret)${NC}" >&2
|
||||
echo -e " ${DIM}$(printf '─%.0s' {1..60})${NC}" >&2
|
||||
echo -ne " ${WHITE}$(_t_or manual_secret_prompt 'Вставьте ключ'):${NC} " >&2
|
||||
read -r user_input
|
||||
|
||||
if [ -z "$user_input" ]; then
|
||||
log_error "$(_t_or manual_secret_empty 'Ключ не введён')"
|
||||
return 1
|
||||
fi
|
||||
|
||||
local parsed
|
||||
if ! parsed=$(parse_manual_secret "$user_input"); then
|
||||
log_error "$(_t_or manual_secret_bad 'Не удалось распознать формат ключа')"
|
||||
return 1
|
||||
fi
|
||||
|
||||
local p_raw="" p_domain="" p_server="" p_port=""
|
||||
while IFS='=' read -r k v; do
|
||||
case "$k" in
|
||||
raw_secret) p_raw="$v" ;;
|
||||
domain) p_domain="$v" ;;
|
||||
server) p_server="$v" ;;
|
||||
port) p_port="$v" ;;
|
||||
esac
|
||||
done <<< "$parsed"
|
||||
|
||||
echo "" >&2
|
||||
echo -e " ${GREEN}✓ $(_t_or manual_secret_parsed 'Ключ распознан')${NC}" >&2
|
||||
echo -e " ${WHITE}raw_secret:${NC} ${DIM}${p_raw:0:8}...${p_raw: -4}${NC}" >&2
|
||||
[ -n "$p_domain" ] && echo -e " ${WHITE}domain:${NC} ${CYAN}${p_domain}${NC}" >&2
|
||||
[ -n "$p_server" ] && echo -e " ${WHITE}server:${NC} ${CYAN}${p_server}${NC}" >&2
|
||||
[ -n "$p_port" ] && echo -e " ${WHITE}port:${NC} ${CYAN}${p_port}${NC}" >&2
|
||||
echo "" >&2
|
||||
|
||||
# Export for the subsequent install flow to pick up
|
||||
export GOTELEGRAM_EXISTING_SECRET="$p_raw"
|
||||
export GOTELEGRAM_EXISTING_DOMAIN="$p_domain"
|
||||
export GOTELEGRAM_EXISTING_PORT="$p_port"
|
||||
return 0
|
||||
}
|
||||
|
||||
526
lib/common.sh
Executable file → Normal file
526
lib/common.sh
Executable file → Normal file
@@ -1,9 +1,9 @@
|
||||
#!/bin/bash
|
||||
# GoTelegram v2.3 — Общие утилиты
|
||||
# Цвета, логирование, спиннер, системные функции, совместимость с v1
|
||||
# GoTelegram v2.4 — common utilities
|
||||
# Colors, logging, spinner, system helpers, v1 compat, i18n-aware
|
||||
|
||||
# ── Версия ────────────────────────────────────────────────────────────────────
|
||||
GOTELEGRAM_VERSION="2.3.1"
|
||||
# ── Version ───────────────────────────────────────────────────────────────────
|
||||
GOTELEGRAM_VERSION="2.4.10"
|
||||
GOTELEGRAM_NAME="GoTelegram"
|
||||
|
||||
# ── Пути ──────────────────────────────────────────────────────────────────────
|
||||
@@ -49,10 +49,12 @@ log_to_file() {
|
||||
echo "[$ts] $*" >> "$LOG_FILE" 2>/dev/null
|
||||
}
|
||||
|
||||
# ── Спиннер ──────────────────────────────────────────────────────────────────
|
||||
# ── Spinner ──────────────────────────────────────────────────────────────────
|
||||
_spin_pid=""
|
||||
spinner_start() {
|
||||
local msg="${1:-Подождите...}"
|
||||
local default_msg
|
||||
default_msg=$(type t &>/dev/null && t wait || echo "Please wait...")
|
||||
local msg="${1:-$default_msg}"
|
||||
(
|
||||
local frames=('⠋' '⠙' '⠹' '⠸' '⠼' '⠴' '⠦' '⠧' '⠇' '⠏')
|
||||
local i=0
|
||||
@@ -95,7 +97,9 @@ run_with_spinner() {
|
||||
if [ $rc -eq 0 ]; then
|
||||
log_success "$label"
|
||||
else
|
||||
log_error "$label ${RED}(ошибка, код: $rc)${NC}"
|
||||
local err_label
|
||||
err_label=$(type t &>/dev/null && t error || echo "error")
|
||||
log_error "$label ${RED}(${err_label}, code: $rc)${NC}"
|
||||
if [ -s "$err_file" ]; then
|
||||
log_dim " $(head -3 "$err_file")"
|
||||
fi
|
||||
@@ -104,35 +108,45 @@ run_with_spinner() {
|
||||
return $rc
|
||||
}
|
||||
|
||||
# ── Баннер ───────────────────────────────────────────────────────────────────
|
||||
# ── Banner ───────────────────────────────────────────────────────────────────
|
||||
show_banner() {
|
||||
local line
|
||||
line=$(printf '━%.0s' $(seq 1 60))
|
||||
echo ""
|
||||
echo -e "${CYAN}╔══════════════════════════════════════════════════════════╗${NC}"
|
||||
echo -e "${CYAN}║${NC} ${BOLD}${WHITE}🚀 GoTelegram v${GOTELEGRAM_VERSION}${NC} ${CYAN}║${NC}"
|
||||
echo -e "${CYAN}║${NC} ${DIM}MTProxy на ядре telemt (Rust + Tokio)${NC} ${CYAN}║${NC}"
|
||||
echo -e "${CYAN}║${NC} ${DIM}Anti-DPI • Fake TLS • TCP Splice • JA3/JA4${NC} ${CYAN}║${NC}"
|
||||
echo -e "${CYAN}╚══════════════════════════════════════════════════════════╝${NC}"
|
||||
echo -e "${CYAN}${line}${NC}"
|
||||
if type tf &>/dev/null; then
|
||||
echo -e " ${BOLD}${WHITE}🚀 $(tf banner_title "$GOTELEGRAM_VERSION")${NC}"
|
||||
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 " ${DIM}MTProxy powered by telemt (Rust + Tokio)${NC}"
|
||||
echo -e " ${DIM}Anti-DPI • Fake TLS • TCP Splice • JA3/JA4${NC}"
|
||||
fi
|
||||
echo -e "${CYAN}${line}${NC}"
|
||||
echo ""
|
||||
}
|
||||
|
||||
# ── Благодарности ────────────────────────────────────────────────────────────
|
||||
# ── Credits ──────────────────────────────────────────────────────────────────
|
||||
show_credits() {
|
||||
local line
|
||||
line=$(printf '─%.0s' $(seq 1 60))
|
||||
echo ""
|
||||
echo -e "${MAGENTA}╔══════════════════════════════════════════════════════════╗${NC}"
|
||||
echo -e "${MAGENTA}║${NC} ${BOLD}Благодарности / Credits${NC} ${MAGENTA}║${NC}"
|
||||
echo -e "${MAGENTA}╟──────────────────────────────────────────────────────────╢${NC}"
|
||||
echo -e "${MAGENTA}║${NC} ${WHITE}telemt${NC} — MTProxy engine (Rust) ${MAGENTA}║${NC}"
|
||||
echo -e "${MAGENTA}║${NC} ${DIM}github.com/telemt/telemt${NC} ${MAGENTA}║${NC}"
|
||||
echo -e "${MAGENTA}║${NC} ${MAGENTA}║${NC}"
|
||||
echo -e "${MAGENTA}║${NC} ${WHITE}HTML5 UP${NC} — адаптивные HTML/CSS шаблоны ${MAGENTA}║${NC}"
|
||||
echo -e "${MAGENTA}║${NC} ${DIM}html5up.net • CC BY 3.0 • @ajlkn${NC} ${MAGENTA}║${NC}"
|
||||
echo -e "${MAGENTA}║${NC} ${MAGENTA}║${NC}"
|
||||
echo -e "${MAGENTA}║${NC} ${WHITE}learning-zone${NC} — 150+ HTML5 шаблонов ${MAGENTA}║${NC}"
|
||||
echo -e "${MAGENTA}║${NC} ${DIM}github.com/learning-zone/website-templates${NC} ${MAGENTA}║${NC}"
|
||||
echo -e "${MAGENTA}║${NC} ${MAGENTA}║${NC}"
|
||||
echo -e "${MAGENTA}║${NC} ${WHITE}Start Bootstrap${NC} — MIT лицензия ${MAGENTA}║${NC}"
|
||||
echo -e "${MAGENTA}║${NC} ${DIM}startbootstrap.com${NC} ${MAGENTA}║${NC}"
|
||||
echo -e "${MAGENTA}╚══════════════════════════════════════════════════════════╝${NC}"
|
||||
echo -e "${MAGENTA}${line}${NC}"
|
||||
echo -e " ${BOLD}$(type t &>/dev/null && t credits_title || echo 'Credits')${NC}"
|
||||
echo -e "${MAGENTA}${line}${NC}"
|
||||
echo -e " ${WHITE}telemt${NC} — MTProxy engine (Rust)"
|
||||
echo -e " ${DIM}github.com/telemt/telemt${NC}"
|
||||
echo ""
|
||||
echo -e " ${WHITE}HTML5 UP${NC} — responsive HTML/CSS templates"
|
||||
echo -e " ${DIM}html5up.net • CC BY 3.0 • @ajlkn${NC}"
|
||||
echo ""
|
||||
echo -e " ${WHITE}learning-zone${NC} — 150+ HTML5 templates"
|
||||
echo -e " ${DIM}github.com/learning-zone/website-templates${NC}"
|
||||
echo ""
|
||||
echo -e " ${WHITE}Start Bootstrap${NC} — MIT license"
|
||||
echo -e " ${DIM}startbootstrap.com${NC}"
|
||||
echo -e "${MAGENTA}${line}${NC}"
|
||||
echo ""
|
||||
}
|
||||
|
||||
@@ -164,31 +178,41 @@ get_server_ip() {
|
||||
return 1
|
||||
}
|
||||
|
||||
_t_or() {
|
||||
# Helper: translate if i18n available, otherwise return fallback
|
||||
local key="$1" fallback="$2"
|
||||
if type t &>/dev/null; then
|
||||
t "$key"
|
||||
else
|
||||
echo "$fallback"
|
||||
fi
|
||||
}
|
||||
|
||||
check_root() {
|
||||
if [ "$EUID" -ne 0 ]; then
|
||||
log_error "Запустите скрипт с sudo / от root"
|
||||
log_error "$(_t_or err_need_root 'Run the script with sudo / as root')"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
check_os() {
|
||||
if [ ! -f /etc/os-release ]; then
|
||||
log_error "Не удалось определить ОС. Требуется Linux."
|
||||
log_error "$(_t_or err_os_unknown 'Failed to detect OS. Linux is required.')"
|
||||
return 1
|
||||
fi
|
||||
# Validate os-release before sourcing (reject command injection: ;, backticks, $())
|
||||
if grep -qE '(;|`|\$\(|\$\{)' /etc/os-release 2>/dev/null; then
|
||||
log_warning "/etc/os-release содержит подозрительные строки, пропускаем"
|
||||
log_warning "/etc/os-release contains suspicious strings, skipping"
|
||||
return 0
|
||||
fi
|
||||
. /etc/os-release
|
||||
case "$ID" in
|
||||
ubuntu|debian|centos|rocky|almalinux|fedora|rhel)
|
||||
log_dim "ОС: $PRETTY_NAME"
|
||||
log_dim "OS: $PRETTY_NAME"
|
||||
return 0
|
||||
;;
|
||||
*)
|
||||
log_warning "ОС $ID может быть несовместима. Поддерживаются: Ubuntu, Debian, CentOS, Rocky."
|
||||
log_warning "OS $ID may be incompatible. Supported: Ubuntu, Debian, CentOS, Rocky."
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
@@ -216,28 +240,215 @@ get_pkg_manager() {
|
||||
install_pkg() {
|
||||
local pkg="$1"
|
||||
case "$(get_pkg_manager)" in
|
||||
apt) apt-get install -y -qq "$pkg" ;;
|
||||
apt) apt_install "$pkg" ;;
|
||||
dnf) dnf install -y -q "$pkg" ;;
|
||||
yum) yum install -y -q "$pkg" ;;
|
||||
*) log_error "Неизвестный пакетный менеджер"; return 1 ;;
|
||||
*) log_error "$(_t_or err_bad_pkg_mgr 'Unknown package manager')"; return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# ── apt lock wait + install ─────────────────────────────────────────────────
|
||||
# На свежих Ubuntu/Debian unattended-upgrades часто держит dpkg lock на старте
|
||||
# → любой apt-get install падает с "Could not get lock /var/lib/dpkg/lock-frontend".
|
||||
# Эти функции ждут освобождения лока до 300с, потом запускают apt с нативным
|
||||
# таймаутом DPkg::Lock::Timeout. Использовать везде, где раньше был
|
||||
# "apt-get install ...".
|
||||
apt_lock_wait() {
|
||||
local max_wait="${1:-300}"
|
||||
local waited=0
|
||||
local warned=0
|
||||
while fuser /var/lib/dpkg/lock-frontend &>/dev/null \
|
||||
|| fuser /var/lib/dpkg/lock &>/dev/null \
|
||||
|| fuser /var/lib/apt/lists/lock &>/dev/null \
|
||||
|| pgrep -f '^/usr/bin/unattended-upgrade' &>/dev/null; do
|
||||
if [ "$warned" = "0" ]; then
|
||||
log_warning "apt/dpkg locked by unattended-upgrades, waiting up to ${max_wait}s..."
|
||||
warned=1
|
||||
fi
|
||||
sleep 3
|
||||
waited=$((waited + 3))
|
||||
if [ "$waited" -ge "$max_wait" ]; then
|
||||
log_error "apt lock not released after ${max_wait}s"
|
||||
log_dim "Manual fix: systemctl stop unattended-upgrades && killall -9 unattended-upgr 2>/dev/null; dpkg --configure -a"
|
||||
return 1
|
||||
fi
|
||||
done
|
||||
[ "$warned" = "1" ] && log_success "apt lock released (waited ${waited}s)"
|
||||
return 0
|
||||
}
|
||||
|
||||
# apt_install <pkg> [pkg2 ...] — ждёт lock + ставит пакеты + показывает ошибку
|
||||
apt_install() {
|
||||
[ $# -eq 0 ] && return 0
|
||||
apt_lock_wait || return 1
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
local opts="-o DPkg::Lock::Timeout=120"
|
||||
local err_file; err_file=$(mktemp 2>/dev/null || echo /tmp/apt_err.$$)
|
||||
if ! apt-get $opts install -y -qq "$@" 2>"$err_file"; then
|
||||
log_error "apt-get install failed: $*"
|
||||
[ -s "$err_file" ] && tail -n 5 "$err_file" | sed 's/^/ /' >&2
|
||||
rm -f "$err_file"
|
||||
return 1
|
||||
fi
|
||||
rm -f "$err_file"
|
||||
return 0
|
||||
}
|
||||
|
||||
# apt_update — тихий update с ожиданием лока
|
||||
apt_update() {
|
||||
apt_lock_wait || return 1
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
apt-get -o DPkg::Lock::Timeout=120 update -qq 2>/dev/null || true
|
||||
return 0
|
||||
}
|
||||
|
||||
# ── Зависимости GoTelegram ──────────────────────────────────────────────────
|
||||
# Полный список внешних команд, которые скрипт использует. Для каждой команды
|
||||
# указан пакет на apt и dnf/yum (имена различаются: например dig = dnsutils на
|
||||
# Debian, bind-utils на RHEL).
|
||||
#
|
||||
# КРИТИЧЕСКИЕ (без них скрипт просто не работает):
|
||||
# jq — парсинг config.json, templates_catalog.json
|
||||
# curl — скачивание telemt и проверки HTTPS
|
||||
# openssl — генерация секретов, шифрование бекапов, SSL проверка
|
||||
# git — клонирование шаблонов через download_template
|
||||
# xxd — hex-encode домена для fake-TLS секрета (ee-prefix)
|
||||
# tar — распаковка telemt архива и бекапы
|
||||
# dig — DNS-проверка домена в Pro-режиме
|
||||
#
|
||||
# ЖЕЛАТЕЛЬНЫЕ (есть fallback, но с ними лучше):
|
||||
# qrencode — QR-коды для прокси-ссылок
|
||||
# bc — красивое форматирование чисел в статистике
|
||||
#
|
||||
# Pro-режим доустанавливает nginx/certbot через install_nginx/install_certbot
|
||||
# (они большие и нужны только если пользователь выбрал Pro).
|
||||
|
||||
# Маппинг команды -> (apt_pkg, dnf_pkg). apt_pkg_for_cmd <cmd>
|
||||
apt_pkg_for_cmd() {
|
||||
case "$1" in
|
||||
dig) echo "dnsutils" ;;
|
||||
xxd) echo "xxd" ;; # Ubuntu 22+: отдельный пакет, fallback ниже
|
||||
nslookup) echo "dnsutils" ;;
|
||||
host) echo "dnsutils" ;;
|
||||
ss) echo "iproute2" ;;
|
||||
netstat) echo "net-tools" ;;
|
||||
flock) echo "util-linux" ;;
|
||||
*) echo "$1" ;; # команда == имя пакета
|
||||
esac
|
||||
}
|
||||
|
||||
dnf_pkg_for_cmd() {
|
||||
case "$1" in
|
||||
dig|nslookup|host) echo "bind-utils" ;;
|
||||
xxd) echo "vim-common" ;;
|
||||
ss) echo "iproute" ;;
|
||||
netstat) echo "net-tools" ;;
|
||||
flock) echo "util-linux" ;;
|
||||
*) echo "$1" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
ensure_deps() {
|
||||
local missing=()
|
||||
for cmd in curl jq openssl git; do
|
||||
if ! command -v "$cmd" &>/dev/null; then
|
||||
missing+=("$cmd")
|
||||
fi
|
||||
# Критические зависимости — без них скрипт не работает.
|
||||
# flock используется bot_action_dispatch для сериализации параллельных
|
||||
# вызовов (иначе гонка на config.json при одновременных change-template /
|
||||
# change-lite-domain из бота).
|
||||
local critical=(curl jq openssl git xxd tar dig flock)
|
||||
# Желательные — есть fallback, устанавливать всё равно, но не падать если не смогли
|
||||
local optional=(qrencode bc)
|
||||
|
||||
local missing_critical=() missing_optional=() cmd
|
||||
for cmd in "${critical[@]}"; do
|
||||
command -v "$cmd" &>/dev/null || missing_critical+=("$cmd")
|
||||
done
|
||||
if [ ${#missing[@]} -gt 0 ]; then
|
||||
log_step "Установка зависимостей: ${missing[*]}"
|
||||
case "$(get_pkg_manager)" in
|
||||
apt) apt-get update -qq && apt-get install -y -qq "${missing[@]}" ;;
|
||||
dnf) dnf install -y -q "${missing[@]}" ;;
|
||||
yum) yum install -y -q "${missing[@]}" ;;
|
||||
for cmd in "${optional[@]}"; do
|
||||
command -v "$cmd" &>/dev/null || missing_optional+=("$cmd")
|
||||
done
|
||||
|
||||
local all_missing=("${missing_critical[@]}" "${missing_optional[@]}")
|
||||
[ ${#all_missing[@]} -eq 0 ] && return 0
|
||||
|
||||
# Собираем список пакетов для выбранного менеджера
|
||||
local pkg_mgr pkg pkgs=()
|
||||
pkg_mgr=$(get_pkg_manager)
|
||||
|
||||
for cmd in "${all_missing[@]}"; do
|
||||
case "$pkg_mgr" in
|
||||
apt) pkg=$(apt_pkg_for_cmd "$cmd") ;;
|
||||
dnf|yum) pkg=$(dnf_pkg_for_cmd "$cmd") ;;
|
||||
*) pkg="$cmd" ;;
|
||||
esac
|
||||
pkgs+=("$pkg")
|
||||
done
|
||||
|
||||
# Убираем дубликаты (например dig+nslookup оба = dnsutils)
|
||||
local uniq_pkgs=()
|
||||
for pkg in "${pkgs[@]}"; do
|
||||
local found=0 p
|
||||
for p in "${uniq_pkgs[@]}"; do
|
||||
[ "$p" = "$pkg" ] && { found=1; break; }
|
||||
done
|
||||
[ "$found" = "0" ] && uniq_pkgs+=("$pkg")
|
||||
done
|
||||
|
||||
if type tf &>/dev/null; then
|
||||
log_step "$(tf deps_installing "${all_missing[*]}")"
|
||||
else
|
||||
log_step "Installing dependencies: ${all_missing[*]} (packages: ${uniq_pkgs[*]})"
|
||||
fi
|
||||
|
||||
case "$pkg_mgr" in
|
||||
apt)
|
||||
apt_update
|
||||
apt_install "${uniq_pkgs[@]}" || true
|
||||
;;
|
||||
dnf) dnf install -y -q "${uniq_pkgs[@]}" 2>/dev/null ;;
|
||||
yum) yum install -y -q "${uniq_pkgs[@]}" 2>/dev/null ;;
|
||||
*)
|
||||
log_error "Unknown package manager — install manually: ${uniq_pkgs[*]}"
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
|
||||
# Фолбэки для xxd: на некоторых системах нужен vim-common вместо xxd
|
||||
if ! command -v xxd &>/dev/null && [ "$pkg_mgr" = "apt" ]; then
|
||||
apt_install vim-common || true
|
||||
fi
|
||||
|
||||
# Повторная проверка критических команд
|
||||
local still_missing=()
|
||||
for cmd in "${critical[@]}"; do
|
||||
command -v "$cmd" &>/dev/null || still_missing+=("$cmd")
|
||||
done
|
||||
|
||||
if [ ${#still_missing[@]} -gt 0 ]; then
|
||||
log_error "Critical dependencies still missing: ${still_missing[*]}"
|
||||
log_error "Install manually and re-run gotelegram"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Опциональные — только предупреждение
|
||||
local still_missing_opt=()
|
||||
for cmd in "${optional[@]}"; do
|
||||
command -v "$cmd" &>/dev/null || still_missing_opt+=("$cmd")
|
||||
done
|
||||
if [ ${#still_missing_opt[@]} -gt 0 ]; then
|
||||
log_warning "Optional deps missing (features degraded): ${still_missing_opt[*]}"
|
||||
fi
|
||||
|
||||
log_success "Dependencies ready"
|
||||
return 0
|
||||
}
|
||||
|
||||
# Быстрая проверка — только смотрит что критические установлены, ничего не ставит.
|
||||
# Возвращает 0 если всё ок, 1 если что-то отсутствует. Используется на старте
|
||||
# main() чтобы не дёргать apt-get update при каждом запуске меню.
|
||||
check_deps_present() {
|
||||
local cmd
|
||||
for cmd in curl jq openssl git xxd tar dig flock; do
|
||||
command -v "$cmd" &>/dev/null || return 1
|
||||
done
|
||||
return 0
|
||||
}
|
||||
|
||||
check_port() {
|
||||
@@ -252,12 +463,177 @@ check_port() {
|
||||
return 1 # свободен
|
||||
}
|
||||
|
||||
# ── Preflight: port conflict detection ───────────────────────────────────────
|
||||
# Проверяет, что нужные для установки порты свободны. Если порт занят —
|
||||
# определяет процесс и сопоставляет с известным списком proxy/VPN софта
|
||||
# (xray, sing-box, v2ray, trojan, hysteria, mtg, shadowsocks, x-ui/3x-ui,
|
||||
# marzban, amneziawg, caddy, apache, haproxy). Пользователь видит явное
|
||||
# предупреждение и может либо прервать установку, либо продолжить на свой
|
||||
# страх и риск (GOTELEGRAM_SKIP_PREFLIGHT=1 — полностью отключить проверку).
|
||||
#
|
||||
# Используемые порты GoTelegram:
|
||||
# 443 — telemt (внешний, MTProxy + fake-TLS) — lite и pro
|
||||
# 80 — nginx redirect + certbot ACME HTTP-01 — только pro
|
||||
# 8443 — nginx internal mask (127.0.0.1:8443) — только pro
|
||||
|
||||
# get_port_process <port> → "<pid>|<comm>" если занят, иначе пусто
|
||||
get_port_process() {
|
||||
local port="$1"
|
||||
local line="" pid="" proc=""
|
||||
line=$(ss -tlnp 2>/dev/null | grep -E ":${port}[[:space:]]" | head -1)
|
||||
if [ -z "$line" ]; then
|
||||
line=$(netstat -tlnp 2>/dev/null | grep -E ":${port}[[:space:]]" | head -1)
|
||||
fi
|
||||
if [ -n "$line" ]; then
|
||||
pid=$(echo "$line" | grep -oE 'pid=[0-9]+' | head -1 | cut -d= -f2)
|
||||
if [ -z "$pid" ]; then
|
||||
# netstat format: "12345/procname"
|
||||
pid=$(echo "$line" | grep -oE '[0-9]+/[^ ]+' | head -1 | cut -d/ -f1)
|
||||
fi
|
||||
fi
|
||||
if [ -z "$pid" ]; then
|
||||
pid=$(fuser -n tcp "$port" 2>/dev/null | tr -s ' ' | awk '{print $1}' | head -1)
|
||||
pid="${pid:-}"
|
||||
fi
|
||||
if [ -n "$pid" ] && [ "$pid" -gt 0 ] 2>/dev/null; then
|
||||
proc=$(ps -p "$pid" -o comm= 2>/dev/null | tr -d ' \n')
|
||||
[ -z "$proc" ] && proc="unknown"
|
||||
echo "${pid}|${proc}"
|
||||
return 0
|
||||
fi
|
||||
if [ -n "$line" ]; then
|
||||
# Port is occupied but process cannot be identified (kernel socket / no root)
|
||||
echo "0|unknown"
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
# match_known_conflict <comm> → печатает человекочитаемое имя если это
|
||||
# известный proxy/VPN/web софт. Возвращает 0 если нашли, 1 иначе.
|
||||
match_known_conflict() {
|
||||
local proc="$1"
|
||||
case "$proc" in
|
||||
*xray*|*Xray*) echo "Xray"; return 0 ;;
|
||||
*sing-box*|*sing_box*|*singbox*) echo "sing-box"; return 0 ;;
|
||||
*v2ray*|*V2Ray*) echo "V2Ray"; return 0 ;;
|
||||
*trojan*) echo "Trojan"; return 0 ;;
|
||||
*hysteria*) echo "Hysteria"; return 0 ;;
|
||||
*mtg*) echo "mtg (old MTProxy)"; return 0 ;;
|
||||
*ss-server*|*ss-local*|*shadowsocks*|*ssserver*) echo "Shadowsocks"; return 0 ;;
|
||||
*x-ui*|*3x-ui*|*xui*) echo "x-ui / 3x-ui panel"; return 0 ;;
|
||||
*marzban*) echo "Marzban panel"; return 0 ;;
|
||||
*amneziawg*|*awg-go*|*awg*) echo "AmneziaWG"; return 0 ;;
|
||||
*caddy*) echo "Caddy web server"; return 0 ;;
|
||||
*apache2*|*httpd*) echo "Apache httpd"; return 0 ;;
|
||||
*haproxy*) echo "HAProxy"; return 0 ;;
|
||||
*nginx*) echo "nginx (already running)"; return 0 ;;
|
||||
*tgproxy*|*mtproxy*|*mtproto*) echo "MTProto Proxy (other impl)"; return 0 ;;
|
||||
*wireguard*|*wg-quick*) echo "WireGuard"; return 0 ;;
|
||||
*openvpn*) echo "OpenVPN"; return 0 ;;
|
||||
esac
|
||||
return 1
|
||||
}
|
||||
|
||||
# preflight_check <mode> [port]
|
||||
# mode = "lite" | "pro"
|
||||
# port = selected port for lite mode (default 443)
|
||||
# Returns:
|
||||
# 0 — OK to proceed (no conflicts, or user confirmed to force)
|
||||
# 1 — user aborted (caller should show promo and return)
|
||||
preflight_check() {
|
||||
local mode="${1:-lite}"
|
||||
local lite_port="${2:-443}"
|
||||
|
||||
# Escape hatch
|
||||
if [ "${GOTELEGRAM_SKIP_PREFLIGHT:-0}" = "1" ]; then
|
||||
log_dim "preflight: skipped (GOTELEGRAM_SKIP_PREFLIGHT=1)"
|
||||
return 0
|
||||
fi
|
||||
|
||||
local required_ports=()
|
||||
if [ "$mode" = "pro" ]; then
|
||||
required_ports=(443 80 8443)
|
||||
else
|
||||
# lite: проверяем только выбранный внешний порт
|
||||
required_ports=("$lite_port")
|
||||
fi
|
||||
|
||||
local known_conflicts=() unknown_conflicts=() info port pid proc label
|
||||
for port in "${required_ports[@]}"; do
|
||||
info=$(get_port_process "$port")
|
||||
if [ -n "$info" ]; then
|
||||
pid="${info%%|*}"
|
||||
proc="${info##*|}"
|
||||
if label=$(match_known_conflict "$proc"); then
|
||||
known_conflicts+=("${port}|${label}|${pid}|${proc}")
|
||||
else
|
||||
unknown_conflicts+=("${port}|${pid}|${proc}")
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
if [ ${#known_conflicts[@]} -eq 0 ] && [ ${#unknown_conflicts[@]} -eq 0 ]; then
|
||||
log_dim "preflight: ports ${required_ports[*]} свободны"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Показываем баннер конфликта
|
||||
echo "" >&2
|
||||
echo -e " ${BOLD}${YELLOW}⚠ $(_t_or preflight_title 'Предустановочная проверка: обнаружены конфликты портов')${NC}" >&2
|
||||
echo -e " ${DIM}$(printf '─%.0s' {1..60})${NC}" >&2
|
||||
|
||||
local item p label2 pid2 proc2 rest
|
||||
if [ ${#known_conflicts[@]} -gt 0 ]; then
|
||||
echo -e " ${RED}$(_t_or preflight_known 'Известный proxy/VPN/веб-софт занимает нужные порты:')${NC}" >&2
|
||||
for item in "${known_conflicts[@]}"; do
|
||||
p="${item%%|*}"
|
||||
rest="${item#*|}"
|
||||
label2="${rest%%|*}"
|
||||
rest="${rest#*|}"
|
||||
pid2="${rest%%|*}"
|
||||
proc2="${rest##*|}"
|
||||
echo -e " ${RED}✗${NC} ${BOLD}:${p}${NC} → ${BOLD}${label2}${NC} ${DIM}(pid=${pid2}, cmd=${proc2})${NC}" >&2
|
||||
done
|
||||
fi
|
||||
if [ ${#unknown_conflicts[@]} -gt 0 ]; then
|
||||
echo -e " ${YELLOW}$(_t_or preflight_unknown 'Порты заняты неизвестными процессами:')${NC}" >&2
|
||||
for item in "${unknown_conflicts[@]}"; do
|
||||
p="${item%%|*}"
|
||||
rest="${item#*|}"
|
||||
pid2="${rest%%|*}"
|
||||
proc2="${rest##*|}"
|
||||
echo -e " ${YELLOW}⚠${NC} ${BOLD}:${p}${NC} ${DIM}(pid=${pid2}, cmd=${proc2})${NC}" >&2
|
||||
done
|
||||
fi
|
||||
|
||||
echo -e " ${DIM}$(printf '─%.0s' {1..60})${NC}" >&2
|
||||
echo -e " ${WHITE}$(_t_or preflight_needed 'GoTelegram нужны порты:')${NC} ${CYAN}${required_ports[*]}${NC}" >&2
|
||||
echo -e " ${WHITE}$(_t_or preflight_hint_header 'Рекомендации:')${NC}" >&2
|
||||
echo -e " ${DIM}• $(_t_or preflight_hint1 'Остановите и удалите конфликтующие сервисы (systemctl stop ...)')${NC}" >&2
|
||||
echo -e " ${DIM}• $(_t_or preflight_hint2 'Либо возьмите чистый VPS без других прокси')${NC}" >&2
|
||||
echo -e " ${DIM}• $(_t_or preflight_hint3 'Установка поверх, скорее всего, завершится некорректно')${NC}" >&2
|
||||
echo -e " ${DIM}$(_t_or preflight_skip_hint 'Override: GOTELEGRAM_SKIP_PREFLIGHT=1 gotelegram')${NC}" >&2
|
||||
echo "" >&2
|
||||
|
||||
if confirm "$(_t_or preflight_proceed 'Продолжить установку всё равно (скорее всего не заработает)?')"; then
|
||||
log_warning "$(_t_or preflight_forced 'Установка продолжена вопреки конфликтам — возможны ошибки')"
|
||||
return 0
|
||||
fi
|
||||
log_info "$(_t_or preflight_aborted 'Установка отменена из-за конфликтов портов')"
|
||||
return 1
|
||||
}
|
||||
|
||||
check_disk_space() {
|
||||
local min_mb="${1:-500}"
|
||||
local avail_mb
|
||||
avail_mb=$(df -m / | awk 'NR==2 {print $4}')
|
||||
if [ "$avail_mb" -lt "$min_mb" ]; then
|
||||
log_error "Мало места на диске: ${avail_mb}MB (нужно ${min_mb}MB+)"
|
||||
if type tf &>/dev/null; then
|
||||
log_error "$(tf err_low_disk "$avail_mb" "$min_mb")"
|
||||
else
|
||||
log_error "Low disk space: ${avail_mb}MB (need ${min_mb}MB+)"
|
||||
fi
|
||||
return 1
|
||||
fi
|
||||
return 0
|
||||
@@ -266,6 +642,8 @@ check_disk_space() {
|
||||
# ── Конфигурация GoTelegram (JSON) ──────────────────────────────────────────
|
||||
save_gotelegram_config() {
|
||||
mkdir -p "$(dirname "$GOTELEGRAM_CONFIG")"
|
||||
local cur_lang
|
||||
cur_lang=$(type get_language &>/dev/null && get_language || echo en)
|
||||
cat > "$GOTELEGRAM_CONFIG" << EOJSON
|
||||
{
|
||||
"version": "$GOTELEGRAM_VERSION",
|
||||
@@ -276,6 +654,7 @@ save_gotelegram_config() {
|
||||
"mask_host": "${5:-google.com}",
|
||||
"domain": "${6:-}",
|
||||
"template_id": "${7:-}",
|
||||
"language": "${cur_lang}",
|
||||
"installed_at": "$(date -Iseconds)",
|
||||
"updated_at": "$(date -Iseconds)"
|
||||
}
|
||||
@@ -295,13 +674,11 @@ load_gotelegram_config() {
|
||||
config_get() {
|
||||
local key="$1"
|
||||
if [ ! -f "$GOTELEGRAM_CONFIG" ]; then
|
||||
log_dim "Конфиг не найден: $GOTELEGRAM_CONFIG" >&2
|
||||
return 2 # file missing
|
||||
fi
|
||||
local val
|
||||
val=$(jq -r ".$key // empty" "$GOTELEGRAM_CONFIG" 2>/dev/null)
|
||||
if [ $? -ne 0 ]; then
|
||||
log_dim "Ошибка чтения JSON: $GOTELEGRAM_CONFIG" >&2
|
||||
return 3 # invalid JSON
|
||||
fi
|
||||
if [ -z "$val" ]; then
|
||||
@@ -361,7 +738,7 @@ get_v1_config() {
|
||||
}
|
||||
|
||||
migrate_v1_to_v2() {
|
||||
log_step "Миграция с v1 (mtg) на v2 (telemt)"
|
||||
log_step "$(_t_or v1_migration_step 'Migrating from v1 (mtg) to v2 (telemt)')"
|
||||
|
||||
local v1_config
|
||||
v1_config=$(get_v1_config)
|
||||
@@ -371,44 +748,59 @@ migrate_v1_to_v2() {
|
||||
old_secret=$(echo "$v1_config" | jq -r '.secret // empty')
|
||||
|
||||
if [ -z "$old_secret" ]; then
|
||||
log_warning "Не удалось извлечь secret из v1. Будет создан новый."
|
||||
log_warning "Failed to extract secret from v1. A new one will be generated."
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo -e " ${WHITE}Найдена установка v1 (mtg):${NC}"
|
||||
echo -e " Порт: ${CYAN}${old_port}${NC}"
|
||||
echo -e " Secret: ${CYAN}${old_secret:0:16}...${NC}"
|
||||
echo -e " ${WHITE}$(_t_or v1_found_title 'Found v1 (mtg) installation:')${NC}"
|
||||
if type tf &>/dev/null; then
|
||||
echo -e " $(tf v1_port "$old_port")"
|
||||
echo -e " $(tf v1_secret "${old_secret:0:16}")"
|
||||
else
|
||||
echo -e " Port: ${CYAN}${old_port}${NC}"
|
||||
echo -e " Secret: ${CYAN}${old_secret:0:16}...${NC}"
|
||||
fi
|
||||
echo ""
|
||||
echo -e " ${YELLOW}Внимание:${NC} секрет mtg НЕ совместим с telemt напрямую."
|
||||
echo -e " Клиентам потребуется новая ссылка."
|
||||
echo -e " ${YELLOW}$(_t_or warning 'Warning'):${NC} $(_t_or v1_incompatible 'mtg secret is NOT directly compatible with telemt.')"
|
||||
echo -e " $(_t_or v1_new_link 'Clients will need a new link.')"
|
||||
echo ""
|
||||
echo -ne " Остановить v1 контейнер и перейти на v2? [Y/n]: "
|
||||
echo -ne " $(_t_or v1_stop_migrate 'Stop v1 container and migrate to v2? [Y/n]:') "
|
||||
read -r ans
|
||||
if [[ "$ans" =~ ^[Nn] ]]; then
|
||||
log_info "Миграция отменена. v1 оставлен без изменений."
|
||||
log_info "$(_t_or v1_migration_cancelled 'Migration cancelled. v1 left intact.')"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Останавливаем v1
|
||||
log_info "Остановка v1 контейнера..."
|
||||
# Stop v1
|
||||
log_info "$(_t_or v1_stopping 'Stopping v1 container...')"
|
||||
docker stop "$V1_CONTAINER_NAME" 2>/dev/null
|
||||
docker rm "$V1_CONTAINER_NAME" 2>/dev/null
|
||||
|
||||
# Бекапим v1 конфиг
|
||||
# Backup v1 config
|
||||
if [ -f "$V1_CONFIG_FILE" ]; then
|
||||
mkdir -p "$GOTELEGRAM_DIR"
|
||||
cp "$V1_CONFIG_FILE" "$GOTELEGRAM_DIR/v1_backup_proxy.json" 2>/dev/null
|
||||
log_success "Конфиг v1 сохранён в $GOTELEGRAM_DIR/v1_backup_proxy.json"
|
||||
if type tf &>/dev/null; then
|
||||
log_success "$(tf v1_config_saved "$GOTELEGRAM_DIR/v1_backup_proxy.json")"
|
||||
else
|
||||
log_success "v1 config saved to $GOTELEGRAM_DIR/v1_backup_proxy.json"
|
||||
fi
|
||||
fi
|
||||
|
||||
log_success "v1 остановлен. Порт $old_port освобождён."
|
||||
if type tf &>/dev/null; then
|
||||
log_success "$(tf v1_port_freed "$old_port")"
|
||||
else
|
||||
log_success "v1 stopped. Port $old_port freed."
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
# ── Подтверждение ────────────────────────────────────────────────────────────
|
||||
# ── Confirm prompt ───────────────────────────────────────────────────────────
|
||||
confirm() {
|
||||
local msg="${1:-Продолжить?}"
|
||||
local default_msg
|
||||
default_msg=$(_t_or install_continue_anyway 'Continue?')
|
||||
local msg="${1:-$default_msg}"
|
||||
echo -ne " ${msg} [Y/n]: " >&2
|
||||
read -r ans
|
||||
[[ ! "$ans" =~ ^[Nn] ]]
|
||||
@@ -429,7 +821,7 @@ select_option() {
|
||||
((i++))
|
||||
done
|
||||
echo -e " ${DIM}$(printf '─%.0s' {1..50})${NC}" >&2
|
||||
echo -ne " ${WHITE}Выбор:${NC} " >&2
|
||||
echo -ne " ${WHITE}$(_t_or choose 'Choose'):${NC} " >&2
|
||||
read -r choice
|
||||
echo "$choice"
|
||||
}
|
||||
|
||||
140
lib/i18n.sh
Executable file
140
lib/i18n.sh
Executable file
@@ -0,0 +1,140 @@
|
||||
#!/bin/bash
|
||||
# GoTelegram v2.4 — i18n engine
|
||||
# Internationalization support: EN (English) / RU (Русский)
|
||||
#
|
||||
# Usage:
|
||||
# source lib/i18n.sh
|
||||
# load_language "ru" # or "en"
|
||||
# echo "$(t menu_install)" # translated string
|
||||
# printf "$(t greeting)\n" "$name" # with format args
|
||||
|
||||
# ── Global i18n state ──
|
||||
declare -gA I18N
|
||||
LANG_CODE="${LANG_CODE:-en}"
|
||||
LANG_FILE=""
|
||||
|
||||
# ── Load a language ──
|
||||
# Sources lib/lang/${lang}.sh into the I18N associative array.
|
||||
# Falls back to English if requested language file is missing.
|
||||
load_language() {
|
||||
local lang="${1:-en}"
|
||||
# Sanitize: only allow [a-z]{2} codes
|
||||
if ! [[ "$lang" =~ ^[a-z]{2}$ ]]; then
|
||||
lang="en"
|
||||
fi
|
||||
|
||||
local lang_dir
|
||||
lang_dir="$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")/lang"
|
||||
local lang_file="${lang_dir}/${lang}.sh"
|
||||
|
||||
if [ ! -f "$lang_file" ]; then
|
||||
lang_file="${lang_dir}/en.sh"
|
||||
lang="en"
|
||||
fi
|
||||
|
||||
if [ -f "$lang_file" ]; then
|
||||
# Clear previous keys then source the new language
|
||||
I18N=()
|
||||
# shellcheck disable=SC1090
|
||||
source "$lang_file"
|
||||
LANG_CODE="$lang"
|
||||
LANG_FILE="$lang_file"
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
# ── Translate: fetch value by key ──
|
||||
# t <key> → echoes translation (or key if missing)
|
||||
t() {
|
||||
local key="$1"
|
||||
local val="${I18N[$key]:-}"
|
||||
if [ -z "$val" ]; then
|
||||
# Fallback to key name so missing translations are visible
|
||||
echo "$key"
|
||||
else
|
||||
echo "$val"
|
||||
fi
|
||||
}
|
||||
|
||||
# ── Translate + printf-style formatting ──
|
||||
# tf <key> <arg1> <arg2> ...
|
||||
tf() {
|
||||
local key="$1"
|
||||
shift
|
||||
local fmt="${I18N[$key]:-$key}"
|
||||
# shellcheck disable=SC2059
|
||||
printf "$fmt" "$@"
|
||||
}
|
||||
|
||||
# ── Get current language code ──
|
||||
get_language() {
|
||||
echo "$LANG_CODE"
|
||||
}
|
||||
|
||||
# ── Detect saved language from config.json, default en ──
|
||||
detect_language() {
|
||||
local cfg="${GOTELEGRAM_CONFIG:-/opt/gotelegram/config.json}"
|
||||
local lang=""
|
||||
if [ -f "$cfg" ] && command -v jq >/dev/null 2>&1; then
|
||||
lang=$(jq -r '.language // empty' "$cfg" 2>/dev/null)
|
||||
fi
|
||||
# Also check marker file (language set before config.json exists)
|
||||
if [ -z "$lang" ]; then
|
||||
local marker="${GOTELEGRAM_DIR:-/opt/gotelegram}/.language"
|
||||
if [ -f "$marker" ]; then
|
||||
lang=$(head -c 2 "$marker" 2>/dev/null | tr -d '[:space:]')
|
||||
fi
|
||||
fi
|
||||
# Sanitize
|
||||
if ! [[ "$lang" =~ ^(en|ru)$ ]]; then
|
||||
lang="en"
|
||||
fi
|
||||
echo "$lang"
|
||||
}
|
||||
|
||||
# ── Persist selected language ──
|
||||
# Saves to config.json if present, otherwise to marker file
|
||||
save_language() {
|
||||
local lang="$1"
|
||||
if ! [[ "$lang" =~ ^(en|ru)$ ]]; then
|
||||
return 1
|
||||
fi
|
||||
mkdir -p "${GOTELEGRAM_DIR:-/opt/gotelegram}" 2>/dev/null
|
||||
# Always write marker for early-access (before config.json exists)
|
||||
echo "$lang" > "${GOTELEGRAM_DIR:-/opt/gotelegram}/.language" 2>/dev/null
|
||||
|
||||
local cfg="${GOTELEGRAM_CONFIG:-/opt/gotelegram/config.json}"
|
||||
if [ -f "$cfg" ] && command -v jq >/dev/null 2>&1; then
|
||||
local tmp
|
||||
tmp=$(mktemp) || return 1
|
||||
if jq --arg lang "$lang" '. + {language: $lang}' "$cfg" > "$tmp" 2>/dev/null; then
|
||||
mv "$tmp" "$cfg"
|
||||
chmod 600 "$cfg"
|
||||
else
|
||||
rm -f "$tmp"
|
||||
fi
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
# ── First-run interactive language picker ──
|
||||
# Shows a minimal, language-agnostic picker (keeps it culture-neutral).
|
||||
# Returns the chosen code via echo.
|
||||
pick_language_interactive() {
|
||||
echo "" >&2
|
||||
echo " ┌──────────────────────────────────────────┐" >&2
|
||||
echo " │ Select language / Выберите язык │" >&2
|
||||
echo " ├──────────────────────────────────────────┤" >&2
|
||||
echo " │ 1) English │" >&2
|
||||
echo " │ 2) Русский │" >&2
|
||||
echo " └──────────────────────────────────────────┘" >&2
|
||||
echo -n " > " >&2
|
||||
local ch
|
||||
read -r ch
|
||||
case "$ch" in
|
||||
1|en|EN|english|English) echo "en" ;;
|
||||
2|ru|RU|russian|Russian|русский) echo "ru" ;;
|
||||
*) echo "en" ;;
|
||||
esac
|
||||
}
|
||||
481
lib/lang/en.sh
Normal file
481
lib/lang/en.sh
Normal file
@@ -0,0 +1,481 @@
|
||||
#!/bin/bash
|
||||
# GoTelegram v2.4 — English translations
|
||||
# shellcheck disable=SC2034,SC2148
|
||||
|
||||
# ── Common words ────────────────────────────────────────────────────────
|
||||
I18N[yes]="Yes"
|
||||
I18N[no]="No"
|
||||
I18N[ok]="OK"
|
||||
I18N[cancel]="Cancel"
|
||||
I18N[back]="« Back"
|
||||
I18N[exit]="Exit"
|
||||
I18N[skip]="Skip"
|
||||
I18N[choose]="Choose"
|
||||
I18N[press_enter]="Press Enter..."
|
||||
I18N[press_enter_to_return]="Press Enter to return to menu..."
|
||||
I18N[invalid_choice]="Invalid choice"
|
||||
I18N[running]="running"
|
||||
I18N[stopped]="stopped"
|
||||
I18N[not_installed]="not installed"
|
||||
I18N[unknown]="unknown"
|
||||
I18N[error]="Error"
|
||||
I18N[warning]="Warning"
|
||||
I18N[info]="Info"
|
||||
I18N[success]="Done"
|
||||
I18N[wait]="Please wait..."
|
||||
|
||||
# ── Banner ──────────────────────────────────────────────────────────────
|
||||
I18N[banner_title]="GoTelegram 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"
|
||||
|
||||
# ── Main menu (dashboard) ───────────────────────────────────────────────
|
||||
I18N[dashboard_title]="Control panel"
|
||||
I18N[svc_proxy]="Proxy"
|
||||
I18N[svc_nginx]="nginx"
|
||||
I18N[svc_site]="Site"
|
||||
I18N[svc_ssl]="SSL"
|
||||
I18N[svc_bot]="Bot"
|
||||
I18N[ssl_until]="until %s"
|
||||
I18N[net_ip]="IP:"
|
||||
I18N[net_port]="Port:"
|
||||
I18N[net_mode]="Mode:"
|
||||
I18N[net_domain]="Domain:"
|
||||
I18N[connection_link]="Telegram connection link:"
|
||||
I18N[proxy_not_configured]="Proxy is not configured. Select option 1."
|
||||
# ── show_proxy_info labels ─────────────────────────────────────────────
|
||||
I18N[info_status_running]="Running"
|
||||
I18N[info_status_stopped]="Stopped"
|
||||
I18N[info_status_not_installed]="Not installed"
|
||||
I18N[info_proxy_status]="Proxy status"
|
||||
I18N[info_engine]="Engine"
|
||||
I18N[info_ip]="IP"
|
||||
I18N[info_domain]="Domain"
|
||||
I18N[info_port]="Port"
|
||||
I18N[info_mode]="Mode"
|
||||
I18N[info_mask]="Mask host"
|
||||
I18N[info_secret]="Secret"
|
||||
I18N[info_link]="Link"
|
||||
# ── show_traffic_stats labels ──────────────────────────────────────────
|
||||
I18N[stats_sh_proxy]="Proxy (telemt, port 443)"
|
||||
I18N[stats_sh_site]="Site (nginx, port 8443)"
|
||||
I18N[stats_sh_hdr_period]="Period"
|
||||
I18N[stats_sh_hdr_inbound]="Inbound"
|
||||
I18N[stats_sh_hdr_rate]="Rate"
|
||||
I18N[stats_sh_packets]="Packets"
|
||||
I18N[stats_sh_1min]="1 min"
|
||||
I18N[stats_sh_5min]="5 min"
|
||||
I18N[stats_sh_60min]="60 min"
|
||||
I18N[stats_sh_1day]="1 day"
|
||||
I18N[stats_sh_7days]="7 days"
|
||||
I18N[stats_sh_30days]="30 days"
|
||||
I18N[stats_sh_365days]="365 days"
|
||||
I18N[menu_proxy]="Proxy ▸"
|
||||
I18N[menu_stats]="Statistics ▸"
|
||||
I18N[menu_manage]="Management ▸"
|
||||
I18N[menu_telegram_bot]="Telegram bot ▸"
|
||||
I18N[menu_about]="About ▸"
|
||||
I18N[auto_refresh_30s]="Refresh in 30 sec"
|
||||
|
||||
# ── Submenu: Proxy ──────────────────────────────────────────────────────
|
||||
I18N[submenu_proxy_title]="🚀 PROXY"
|
||||
I18N[proxy_install_update]="Install / Update"
|
||||
I18N[proxy_status_detail]="Detailed status"
|
||||
I18N[proxy_copy_link]="Copy link"
|
||||
I18N[proxy_share]="Share key"
|
||||
I18N[proxy_restart]="Restart"
|
||||
I18N[proxy_logs]="Logs"
|
||||
I18N[proxy_change_mode]="Change mode / template"
|
||||
|
||||
# ── Submenu: Manage ─────────────────────────────────────────────────────
|
||||
I18N[submenu_manage_title]="⚙️ MANAGEMENT"
|
||||
I18N[manage_backup]="Backup"
|
||||
I18N[manage_restore]="Restore"
|
||||
I18N[manage_update_telemt]="Update telemt"
|
||||
I18N[manage_site_ssl]="Site / SSL"
|
||||
I18N[manage_remove]="Remove"
|
||||
I18N[manage_language]="Language / Язык"
|
||||
|
||||
# ── Submenu: About ──────────────────────────────────────────────────────
|
||||
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_engine]="Engine:"
|
||||
I18N[version_tech]="Technology:"
|
||||
I18N[version_license]="License:"
|
||||
|
||||
# ── Install flow ────────────────────────────────────────────────────────
|
||||
I18N[install_select_mode]="🎭 Select masquerade mode:"
|
||||
I18N[install_lite_title]="⚡ Lite — masquerade as popular website"
|
||||
I18N[install_lite_desc1]="Fast, no domain needed. telemt disguises traffic"
|
||||
I18N[install_lite_desc2]="as the chosen site (google.com etc.)"
|
||||
I18N[install_pro_title]="🛡 Pro — your own site + full masquerade"
|
||||
I18N[install_pro_desc1]="nginx + SSL + HTML template + telemt."
|
||||
I18N[install_pro_desc2]="DPI sees a real website with a real certificate."
|
||||
I18N[install_pro_desc3]="Requires: a domain pointing to this server."
|
||||
I18N[install_mode_choice]="Choice (1/2):"
|
||||
I18N[install_bad_choice]="Invalid choice: %s"
|
||||
I18N[install_lite_step]="Installing Lite mode"
|
||||
I18N[install_pro_step]="Installing Pro mode"
|
||||
I18N[install_enter_domain]="Enter your domain (e.g. example.com):"
|
||||
I18N[install_bad_domain]="Invalid domain: %s"
|
||||
I18N[install_dns_mismatch]="Domain %s points to %s, not to %s"
|
||||
I18N[install_continue_anyway]="Continue anyway?"
|
||||
I18N[install_enter_email]="Email for SSL (Enter = no email):"
|
||||
I18N[install_config_title]="📋 Configuration:"
|
||||
I18N[install_cfg_ip]="IP:"
|
||||
I18N[install_cfg_port]="Port:"
|
||||
I18N[install_cfg_mask]="Masquerade:"
|
||||
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_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"
|
||||
|
||||
# ── Change mode/template ────────────────────────────────────────────────
|
||||
I18N[change_current_mode]="Current mode:"
|
||||
I18N[change_template]="Change site template (pro only)"
|
||||
I18N[change_mode_switch]="Switch mode (lite ↔ pro)"
|
||||
I18N[change_only_pro]="Template change is available in pro mode only"
|
||||
I18N[change_requires_reinstall]="Mode switch requires reinstall."
|
||||
I18N[change_reinstall_confirm]="Reinstall proxy?"
|
||||
|
||||
# ── Logs ────────────────────────────────────────────────────────────────
|
||||
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_server]="🌍 Server: %s"
|
||||
I18N[share_port]="🔌 Port: %s"
|
||||
I18N[share_connect_cta]="👉 Connect with one tap:"
|
||||
I18N[share_footer]="Just tap the link or configure manually."
|
||||
|
||||
# ── Website ─────────────────────────────────────────────────────────────
|
||||
I18N[website_title]="🌐 Website management"
|
||||
I18N[website_domain]="Domain:"
|
||||
I18N[website_ssl_until]="SSL until:"
|
||||
I18N[website_only_pro]="Website management is available in pro mode only"
|
||||
I18N[website_renew_ssl]="Renew SSL certificate"
|
||||
I18N[website_restart_nginx]="Restart nginx"
|
||||
I18N[website_change_template]="Change template"
|
||||
|
||||
# ── Remove ──────────────────────────────────────────────────────────────
|
||||
I18N[remove_title]="🗑 Remove GoTelegram"
|
||||
I18N[remove_proxy_only]="Remove proxy only (telemt)"
|
||||
I18N[remove_bot_only]="Remove Telegram bot only"
|
||||
I18N[remove_all]="Remove everything (proxy + bot + settings)"
|
||||
I18N[remove_warn_proxy]="This will remove the proxy and all its settings."
|
||||
I18N[remove_confirm_proxy]="Remove proxy?"
|
||||
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)"
|
||||
|
||||
# ── Telegram bot submenu ────────────────────────────────────────────────
|
||||
I18N[bot_title]="🤖 Telegram bot"
|
||||
I18N[bot_status_running]="● Running"
|
||||
I18N[bot_status_stopped]="○ Stopped"
|
||||
I18N[bot_status_not_installed]="✗ Not installed"
|
||||
I18N[bot_menu_status]="📊 Bot status"
|
||||
I18N[bot_menu_logs]="📋 Bot logs"
|
||||
I18N[bot_menu_restart]="🔄 Restart bot"
|
||||
I18N[bot_menu_stop]="⏹ Stop bot"
|
||||
I18N[bot_menu_start]="▶️ Start bot"
|
||||
I18N[bot_menu_settings]="⚙️ Settings (.env)"
|
||||
I18N[bot_menu_remove]="🗑 Remove bot"
|
||||
I18N[bot_menu_install]="🔧 Install bot"
|
||||
I18N[bot_intro1]="The bot lets you manage the proxy from Telegram:"
|
||||
I18N[bot_intro2]="status, restart, change mode, backup, QR code."
|
||||
I18N[bot_install_step]="Installing Telegram bot"
|
||||
I18N[bot_install_python]="Installing Python3..."
|
||||
I18N[bot_files_not_found]="Bot files not found in %s"
|
||||
I18N[bot_create_venv]="Creating virtual environment..."
|
||||
I18N[bot_install_deps]="Installing dependencies..."
|
||||
I18N[bot_enter_token]="Enter BOT_TOKEN from @BotFather:"
|
||||
I18N[bot_token_empty]="Token cannot be empty"
|
||||
I18N[bot_token]="Token:"
|
||||
I18N[bot_add_admin_how]="How to add the administrator?"
|
||||
I18N[bot_admin_auto]="Auto — bot will capture the ID on first /start"
|
||||
I18N[bot_admin_manual]="Manual — enter the ID now"
|
||||
I18N[bot_admin_ids_prompt]="Admin IDs (space or comma separated):"
|
||||
I18N[bot_env_created]=".env created"
|
||||
I18N[bot_env_exists]=".env already exists, settings preserved"
|
||||
I18N[bot_wait_admin_title]="Waiting for administrator"
|
||||
I18N[bot_wait_admin_msg1]="Open the bot in Telegram and send"
|
||||
I18N[bot_wait_admin_msg2]="The bot will automatically make you an admin"
|
||||
I18N[bot_wait_admin_skip]="Press Ctrl+C to skip"
|
||||
I18N[bot_wait_spinner]="Waiting... send /start to the bot (%d sec)"
|
||||
I18N[bot_admin_assigned]="Administrator assigned!"
|
||||
I18N[bot_wait_skipped]="Skipped. Add admin later via: menu → Telegram bot → Settings"
|
||||
I18N[bot_wait_timeout]="Timeout (5 min). Add admin via: menu → Telegram bot → Settings"
|
||||
I18N[bot_installed]="Bot installed and running!"
|
||||
I18N[bot_status_title]="📊 Telegram bot status"
|
||||
I18N[bot_token_configured]="configured"
|
||||
I18N[bot_access_open]="all users"
|
||||
I18N[bot_logs_title]="📋 Bot logs (last 30 lines):"
|
||||
I18N[bot_settings_title]="⚙️ Bot settings"
|
||||
I18N[bot_current_env]="Current .env:"
|
||||
I18N[bot_change_token]="Change BOT_TOKEN"
|
||||
I18N[bot_change_allowed]="Change ALLOWED_IDS"
|
||||
I18N[bot_new_token]="New BOT_TOKEN:"
|
||||
I18N[bot_token_empty_err]="Empty token"
|
||||
I18N[bot_token_updated]="Token updated, bot restarted"
|
||||
I18N[bot_allowed_prompt]="ALLOWED_IDS (space or comma separated, empty = auto):"
|
||||
I18N[bot_access_updated]="Access updated, bot restarted"
|
||||
I18N[bot_remove_warn]="This will remove the Telegram bot and all its settings."
|
||||
I18N[bot_remove_confirm]="Remove bot?"
|
||||
I18N[bot_removed]="Bot fully removed"
|
||||
I18N[bot_restarted]="Bot restarted"
|
||||
I18N[bot_stopped]="Bot stopped"
|
||||
I18N[bot_started]="Bot started"
|
||||
I18N[bot_status_colon]="Status:"
|
||||
I18N[bot_access_colon]="Access:"
|
||||
I18N[bot_access_ids_fmt]="ID: %s"
|
||||
|
||||
# ── Promo / Donate ──────────────────────────────────────────────────────
|
||||
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_link_label]="Link:"
|
||||
I18N[promo_off60]="60%% discount on the first month"
|
||||
I18N[promo_ant20]="20%% + 3%% when paid for 3 months"
|
||||
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_menu_in]="Menu in %d sec..."
|
||||
|
||||
# ── Stats ───────────────────────────────────────────────────────────────
|
||||
I18N[stats_title]="📊 Traffic statistics"
|
||||
I18N[stats_module_missing]="Statistics module not loaded."
|
||||
I18N[stats_file_missing]="File lib/stats.sh not found."
|
||||
I18N[stats_toggle]="Toggle counter (now: %s)"
|
||||
I18N[stats_install_collector]="Install/update stats collector"
|
||||
I18N[stats_auto_refresh]="Refresh every 3 sec"
|
||||
I18N[stats_on]="on"
|
||||
I18N[stats_off]="off"
|
||||
|
||||
# ── Templates catalog ───────────────────────────────────────────────────
|
||||
I18N[templates_categories]="📂 Site template categories:"
|
||||
I18N[templates_custom_git]="📎 Custom template from git URL"
|
||||
I18N[templates_random]="🎲 Random template"
|
||||
I18N[templates_count_fmt]="(%d templates)"
|
||||
I18N[templates_list]="📋 %s — available templates:"
|
||||
I18N[templates_preview_title]="🔍 Template preview:"
|
||||
I18N[templates_name]="Name:"
|
||||
I18N[templates_source]="Source:"
|
||||
I18N[templates_description]="Description:"
|
||||
I18N[templates_preview]="👁 Preview:"
|
||||
I18N[templates_preview_hint]="Open the link in a browser to preview the template"
|
||||
I18N[templates_repo]="📦 Repo:"
|
||||
I18N[templates_thanks]="💜 Thanks to the authors of %s for the open source code!"
|
||||
I18N[templates_install_this]="Install this template?"
|
||||
I18N[templates_cat_empty]="No templates in this category"
|
||||
I18N[templates_downloading]="Downloading template \"%s\"..."
|
||||
I18N[templates_downloaded]="Template \"%s\" downloaded"
|
||||
I18N[templates_downloaded_subfolder]="Template \"%s\" downloaded (from subfolder)"
|
||||
I18N[templates_no_index]="Template does not contain index.html"
|
||||
I18N[templates_path]="Path: %s"
|
||||
I18N[templates_catalog_not_found]="Templates catalog not found: %s"
|
||||
|
||||
# ── Custom git template ─────────────────────────────────────────────────
|
||||
I18N[custom_git_title]="📎 CUSTOM TEMPLATE FROM GIT URL"
|
||||
I18N[custom_git_help_1]="You can use ANY public static HTML repository as a template."
|
||||
I18N[custom_git_help_2]="The repository must be public and contain a ready-made"
|
||||
I18N[custom_git_help_3]="index.html (build via npm is NOT performed)."
|
||||
I18N[custom_git_formats]="Supported URL formats:"
|
||||
I18N[custom_git_fmt_github]=" • https://github.com/user/repo"
|
||||
I18N[custom_git_fmt_gitlab]=" • https://gitlab.com/user/repo"
|
||||
I18N[custom_git_fmt_gitext]=" • https://example.com/user/repo.git"
|
||||
I18N[custom_git_fmt_branch]=" • https://github.com/user/repo@branch (branch after @)"
|
||||
I18N[custom_git_auto_detect]="Repository structure (auto-detection):"
|
||||
I18N[custom_git_auto_1]=" 1. index.html in repo root"
|
||||
I18N[custom_git_auto_2]=" 2. dist/index.html (StartBootstrap, Vite, webpack)"
|
||||
I18N[custom_git_auto_3]=" 3. public/ or build/ or _site/ or site/ or docs/"
|
||||
I18N[custom_git_auto_4]=" 4. Fallback: search index.html across whole repo"
|
||||
I18N[custom_git_requirements]="Requirements:"
|
||||
I18N[custom_git_req_1]=" • HTTPS only (ssh:// and git:// are blocked)"
|
||||
I18N[custom_git_req_2]=" • Public repositories only"
|
||||
I18N[custom_git_req_3]=" • Repo size up to 100 MB"
|
||||
I18N[custom_git_req_4]=" • Static HTML (no PHP/Python/Node server code)"
|
||||
I18N[custom_git_examples]="Tested example repos:"
|
||||
I18N[custom_git_ex_1]=" • https://github.com/html5up-collective/strata"
|
||||
I18N[custom_git_ex_2]=" • https://github.com/StartBootstrap/startbootstrap-landing-page"
|
||||
I18N[custom_git_enter_url]="Paste git URL (or Enter to cancel):"
|
||||
I18N[custom_git_empty]="No URL provided, cancelled"
|
||||
I18N[custom_git_bad_url]="Invalid URL. Only https:// addresses are accepted"
|
||||
I18N[custom_git_cloning]="Cloning repository..."
|
||||
I18N[custom_git_clone_failed]="Failed to clone repository: %s"
|
||||
I18N[custom_git_too_big]="Repository is too large: %s (limit 100MB)"
|
||||
I18N[custom_git_scanning]="Scanning for index.html..."
|
||||
I18N[custom_git_found_at]="✓ Found index.html in: %s"
|
||||
I18N[custom_git_no_index]="index.html not found in the repository"
|
||||
I18N[custom_git_installed]="Custom template installed from %s"
|
||||
I18N[custom_git_saved]="Template URL saved in config (menu → Site → Update from git)"
|
||||
|
||||
# ── First-run language picker ───────────────────────────────────────────
|
||||
I18N[lang_picker_title]="Select language / Выберите язык"
|
||||
I18N[lang_english]="English"
|
||||
I18N[lang_russian]="Русский"
|
||||
I18N[lang_saved]="Language saved: %s"
|
||||
I18N[lang_change_prompt]="Select a new language:"
|
||||
|
||||
# ── Backup ──────────────────────────────────────────────────────────────
|
||||
I18N[backup_title]="💾 Backup"
|
||||
I18N[backup_creating]="Creating backup..."
|
||||
I18N[backup_created]="Backup created: %s"
|
||||
I18N[backup_failed]="Backup creation failed"
|
||||
I18N[backup_restore_title]="↩️ Restore from backup"
|
||||
I18N[backup_no_files]="No backup files"
|
||||
I18N[backup_select]="Select a backup to restore:"
|
||||
I18N[backup_restoring]="Restoring..."
|
||||
I18N[backup_restored]="Backup restored"
|
||||
I18N[backup_collecting]="Collecting configuration..."
|
||||
I18N[backup_site_included]="Website template included"
|
||||
I18N[backup_archive_err]="Archive creation failed"
|
||||
I18N[backup_archive_missing]="Archive not created"
|
||||
I18N[backup_encrypt_err]="Encryption failed"
|
||||
I18N[backup_encrypted]="Backup encrypted (AES-256-CBC)"
|
||||
I18N[backup_created_fmt]="Backup created: %s (%s)"
|
||||
I18N[backup_file_not_found_fmt]="File not found: %s"
|
||||
I18N[backup_enter_pass]="Enter backup password"
|
||||
I18N[backup_bad_pass]="Wrong password or corrupted file"
|
||||
I18N[backup_extract_err]="Archive extraction failed"
|
||||
I18N[backup_label]="Backup"
|
||||
I18N[backup_version_label]="Version"
|
||||
I18N[backup_mode_label]="Mode"
|
||||
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_lang]="Interface language restored"
|
||||
I18N[backup_restored_nginx]="nginx config restored"
|
||||
I18N[backup_restored_ssl]="SSL certificates restored"
|
||||
I18N[backup_restored_site]="Website template restored"
|
||||
I18N[backup_restore_done]="Restore completed!"
|
||||
I18N[backup_none]="No backups"
|
||||
I18N[backup_list_title]="Available backups"
|
||||
I18N[backup_cleanup_fmt]="Removed %s old backups (kept %s)"
|
||||
I18N[backup_create_title]="Create backup"
|
||||
I18N[backup_encrypt_prompt]="Encrypt backup with a password?"
|
||||
I18N[backup_repeat_pass]="Repeat password"
|
||||
I18N[backup_pass_mismatch]="Passwords do not match"
|
||||
I18N[backup_pass_short]="Password too short (minimum 6 characters)"
|
||||
I18N[backup_pick_prompt]="Backup number (or path to file)"
|
||||
I18N[backup_not_found]="Backup not found"
|
||||
|
||||
# ── Preflight (v2.4.8) ──────────────────────────────────────────────────
|
||||
I18N[preflight_title]="Preflight: port conflicts detected"
|
||||
I18N[preflight_known]="Known proxy/VPN/web software is using required ports:"
|
||||
I18N[preflight_unknown]="Required ports are held by unknown processes:"
|
||||
I18N[preflight_needed]="GoTelegram requires ports:"
|
||||
I18N[preflight_hint_header]="Recommended actions:"
|
||||
I18N[preflight_hint1]="Stop and remove the conflicting services (systemctl stop ...)"
|
||||
I18N[preflight_hint2]="Or use a clean VPS without other proxies"
|
||||
I18N[preflight_hint3]="Installing on top will most likely fail"
|
||||
I18N[preflight_skip_hint]="Bypass: GOTELEGRAM_SKIP_PREFLIGHT=1 gotelegram"
|
||||
I18N[preflight_proceed]="Continue installation anyway (likely to fail)?"
|
||||
I18N[preflight_forced]="Installation continued despite conflicts — errors likely"
|
||||
I18N[preflight_aborted]="Installation aborted due to port conflicts"
|
||||
|
||||
# ── Errors / misc ───────────────────────────────────────────────────────
|
||||
I18N[err_need_root]="Run the script with sudo / as root"
|
||||
I18N[err_os_unknown]="Failed to detect OS. Linux is required."
|
||||
I18N[err_low_disk]="Low disk space: %sMB (need %sMB+)"
|
||||
I18N[err_bad_pkg_mgr]="Unknown package manager"
|
||||
I18N[err_unexpected]="Unexpected error"
|
||||
I18N[bye]="See you later! 👋"
|
||||
I18N[auto_refresh]="Refresh in 30 sec"
|
||||
|
||||
# ── Deps ────────────────────────────────────────────────────────────────
|
||||
I18N[deps_installing]="Installing dependencies: %s"
|
||||
|
||||
# ── Migration ───────────────────────────────────────────────────────────
|
||||
I18N[v1_detected]="⚠️ GoTelegram 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:"
|
||||
I18N[v1_port]="Port: %s"
|
||||
I18N[v1_secret]="Secret: %s..."
|
||||
I18N[v1_incompatible]="mtg secret is NOT directly compatible with telemt."
|
||||
I18N[v1_new_link]="Clients will need a new link."
|
||||
I18N[v1_stop_migrate]="Stop v1 container and migrate to v2? [Y/n]:"
|
||||
I18N[v1_migration_cancelled]="Migration cancelled. v1 left intact."
|
||||
I18N[v1_stopping]="Stopping v1 container..."
|
||||
I18N[v1_config_saved]="v1 config saved to %s"
|
||||
I18N[v1_port_freed]="v1 stopped. Port %s freed."
|
||||
|
||||
# ── v2.4.9: UBF v2.0 backup + manual secret recovery ─────────────────────
|
||||
I18N[install_source_title]="Installation source"
|
||||
I18N[install_source_choice]="Choose source [1-3]:"
|
||||
I18N[install_menu_new]="Fresh installation"
|
||||
I18N[install_menu_new_desc]="Generate a new key and set up from scratch"
|
||||
I18N[install_menu_restore]="Restore from backup"
|
||||
I18N[install_menu_restore_desc]="Full restore from a .tar.gz[.enc] file"
|
||||
I18N[install_menu_existing_key]="Use existing key"
|
||||
I18N[install_menu_existing_key_desc]="Paste a tg://proxy link or a key manually"
|
||||
I18N[install_hint_pro_mode]="The key contains a domain — this is usually Pro mode"
|
||||
I18N[install_reuse_secret]="Using the provided key"
|
||||
I18N[install_reuse_domain]="Using domain from the key"
|
||||
I18N[install_reuse_port]="Using port from the key"
|
||||
|
||||
I18N[manual_secret_title]="Enter existing key"
|
||||
I18N[manual_secret_help1]="Supported formats:"
|
||||
I18N[manual_secret_prompt]="Paste the key"
|
||||
I18N[manual_secret_empty]="Key is empty"
|
||||
I18N[manual_secret_bad]="Could not parse the key format"
|
||||
I18N[manual_secret_parsed]="Key parsed"
|
||||
|
||||
I18N[backup_id_label]="Backup ID"
|
||||
I18N[backup_file_label]="File"
|
||||
I18N[backup_size_label]="Size"
|
||||
I18N[backup_key_label]="Key in backup (fingerprint)"
|
||||
I18N[backup_format_label]="Format"
|
||||
I18N[backup_mode_label]="Mode"
|
||||
I18N[backup_lang_label]="Language"
|
||||
I18N[backup_date_label]="Date"
|
||||
I18N[backup_label]="Backup"
|
||||
I18N[backup_ssl_included]="SSL certificates included (+ chain + renewal)"
|
||||
I18N[backup_site_included]="Website template included"
|
||||
I18N[backup_bot_included]="Telegram bot config included"
|
||||
I18N[backup_restored_bot]="Telegram bot config restored"
|
||||
I18N[backup_automigrate]="Converting legacy backup to UBF v2.0..."
|
||||
I18N[backup_migrated]="Fresh UBF v2.0 backup saved"
|
||||
I18N[backup_collecting]="Collecting configuration..."
|
||||
I18N[backup_archive_err]="Archive creation failed"
|
||||
I18N[backup_archive_missing]="Archive was not created"
|
||||
I18N[backup_encrypt_err]="Encryption failed"
|
||||
I18N[backup_encrypted]="Backup encrypted (AES-256-CBC)"
|
||||
I18N[backup_created]="Backup created"
|
||||
I18N[backup_enter_pass]="Enter password"
|
||||
I18N[backup_repeat_pass]="Repeat password"
|
||||
I18N[backup_pass_mismatch]="Passwords do not match"
|
||||
I18N[backup_pass_short]="Password too short (min 6 chars)"
|
||||
I18N[backup_bad_pass]="Wrong password or corrupted file"
|
||||
I18N[backup_extract_err]="Archive extraction failed"
|
||||
I18N[backup_confirm_restore]="Restore configuration? Current settings will be overwritten."
|
||||
I18N[backup_restored_telemt]="telemt config restored"
|
||||
I18N[backup_restored_gotelegram]="GoTelegram config restored"
|
||||
I18N[backup_restored_lang]="Interface language restored"
|
||||
I18N[backup_restored_nginx]="nginx config restored"
|
||||
I18N[backup_restored_ssl]="SSL certificates restored"
|
||||
I18N[backup_restored_site]="Website template restored"
|
||||
I18N[backup_restore_done]="Restore completed!"
|
||||
I18N[backup_create_title]="Create backup"
|
||||
I18N[backup_encrypt_prompt]="Encrypt the backup with a password?"
|
||||
I18N[backup_none]="No backups found"
|
||||
I18N[backup_list_title]="Available backups"
|
||||
I18N[backup_pick_prompt]="Backup number (or file path)"
|
||||
I18N[backup_not_found]="Backup not found"
|
||||
I18N[backup_file_not_found_fmt]="File not found: %s"
|
||||
I18N[backup_cleanup_fmt]="Deleted %s old backups (kept %s)"
|
||||
481
lib/lang/ru.sh
Normal file
481
lib/lang/ru.sh
Normal file
@@ -0,0 +1,481 @@
|
||||
#!/bin/bash
|
||||
# GoTelegram v2.4 — Russian translations
|
||||
# shellcheck disable=SC2034,SC2148
|
||||
|
||||
# ── Common words ────────────────────────────────────────────────────────
|
||||
I18N[yes]="Да"
|
||||
I18N[no]="Нет"
|
||||
I18N[ok]="OK"
|
||||
I18N[cancel]="Отмена"
|
||||
I18N[back]="« Назад"
|
||||
I18N[exit]="Выход"
|
||||
I18N[skip]="Пропустить"
|
||||
I18N[choose]="Выбор"
|
||||
I18N[press_enter]="Нажмите Enter..."
|
||||
I18N[press_enter_to_return]="Нажмите Enter для возврата в меню..."
|
||||
I18N[invalid_choice]="Неверный выбор"
|
||||
I18N[running]="работает"
|
||||
I18N[stopped]="остановлен"
|
||||
I18N[not_installed]="не установлен"
|
||||
I18N[unknown]="неизвестно"
|
||||
I18N[error]="Ошибка"
|
||||
I18N[warning]="Внимание"
|
||||
I18N[info]="Инфо"
|
||||
I18N[success]="Готово"
|
||||
I18N[wait]="Подождите..."
|
||||
|
||||
# ── Banner ──────────────────────────────────────────────────────────────
|
||||
I18N[banner_title]="GoTelegram v%s"
|
||||
I18N[banner_subtitle]="MTProxy на ядре telemt (Rust + Tokio)"
|
||||
I18N[banner_features]="Anti-DPI • Fake TLS • TCP Splice • JA3/JA4"
|
||||
I18N[credits_title]="Благодарности / Credits"
|
||||
|
||||
# ── Main menu (dashboard) ───────────────────────────────────────────────
|
||||
I18N[dashboard_title]="Панель управления"
|
||||
I18N[svc_proxy]="Прокси"
|
||||
I18N[svc_nginx]="nginx"
|
||||
I18N[svc_site]="Сайт"
|
||||
I18N[svc_ssl]="SSL"
|
||||
I18N[svc_bot]="Бот"
|
||||
I18N[ssl_until]="до %s"
|
||||
I18N[net_ip]="IP:"
|
||||
I18N[net_port]="Порт:"
|
||||
I18N[net_mode]="Режим:"
|
||||
I18N[net_domain]="Домен:"
|
||||
I18N[connection_link]="Ссылка для Telegram:"
|
||||
I18N[proxy_not_configured]="Прокси не настроен. Выберите пункт 1."
|
||||
# ── show_proxy_info labels ─────────────────────────────────────────────
|
||||
I18N[info_status_running]="Работает"
|
||||
I18N[info_status_stopped]="Остановлен"
|
||||
I18N[info_status_not_installed]="Не установлен"
|
||||
I18N[info_proxy_status]="Статус прокси"
|
||||
I18N[info_engine]="Ядро"
|
||||
I18N[info_ip]="IP"
|
||||
I18N[info_domain]="Домен"
|
||||
I18N[info_port]="Порт"
|
||||
I18N[info_mode]="Режим"
|
||||
I18N[info_mask]="Маскировка"
|
||||
I18N[info_secret]="Secret"
|
||||
I18N[info_link]="Ссылка"
|
||||
# ── show_traffic_stats labels ──────────────────────────────────────────
|
||||
I18N[stats_sh_proxy]="Proxy (telemt, порт 443)"
|
||||
I18N[stats_sh_site]="Сайт (nginx, порт 8443)"
|
||||
I18N[stats_sh_hdr_period]="Период"
|
||||
I18N[stats_sh_hdr_inbound]="Входящий"
|
||||
I18N[stats_sh_hdr_rate]="Скорость"
|
||||
I18N[stats_sh_packets]="Пакетов"
|
||||
I18N[stats_sh_1min]="1 мин"
|
||||
I18N[stats_sh_5min]="5 мин"
|
||||
I18N[stats_sh_60min]="60 мин"
|
||||
I18N[stats_sh_1day]="1 день"
|
||||
I18N[stats_sh_7days]="7 дней"
|
||||
I18N[stats_sh_30days]="30 дней"
|
||||
I18N[stats_sh_365days]="365 дней"
|
||||
I18N[menu_proxy]="Прокси ▸"
|
||||
I18N[menu_stats]="Статистика ▸"
|
||||
I18N[menu_manage]="Управление ▸"
|
||||
I18N[menu_telegram_bot]="Telegram-бот ▸"
|
||||
I18N[menu_about]="О программе ▸"
|
||||
I18N[auto_refresh_30s]="Обновление через 30 сек"
|
||||
|
||||
# ── Submenu: Proxy ──────────────────────────────────────────────────────
|
||||
I18N[submenu_proxy_title]="🚀 ПРОКСИ"
|
||||
I18N[proxy_install_update]="Установить / Обновить"
|
||||
I18N[proxy_status_detail]="Статус подробно"
|
||||
I18N[proxy_copy_link]="Скопировать ссылку"
|
||||
I18N[proxy_share]="Поделиться ключом"
|
||||
I18N[proxy_restart]="Перезапуск"
|
||||
I18N[proxy_logs]="Логи"
|
||||
I18N[proxy_change_mode]="Сменить режим / шаблон"
|
||||
|
||||
# ── Submenu: Manage ─────────────────────────────────────────────────────
|
||||
I18N[submenu_manage_title]="⚙️ УПРАВЛЕНИЕ"
|
||||
I18N[manage_backup]="Бекап"
|
||||
I18N[manage_restore]="Восстановить"
|
||||
I18N[manage_update_telemt]="Обновить telemt"
|
||||
I18N[manage_site_ssl]="Сайт / SSL"
|
||||
I18N[manage_remove]="Удалить"
|
||||
I18N[manage_language]="Язык / Language"
|
||||
|
||||
# ── Submenu: About ──────────────────────────────────────────────────────
|
||||
I18N[submenu_about_title]="ℹ️ О ПРОГРАММЕ"
|
||||
I18N[about_version_info]="Информация о версии"
|
||||
I18N[about_promo]="Промо / Донат"
|
||||
I18N[version_title]="🔍 Информация"
|
||||
I18N[version_label]="GoTelegram:"
|
||||
I18N[version_engine]="Ядро:"
|
||||
I18N[version_tech]="Технология:"
|
||||
I18N[version_license]="Лицензия:"
|
||||
|
||||
# ── Install flow ────────────────────────────────────────────────────────
|
||||
I18N[install_select_mode]="🎭 Выберите режим маскировки:"
|
||||
I18N[install_lite_title]="⚡ Lite — маскировка под популярный сайт"
|
||||
I18N[install_lite_desc1]="Быстро, без домена. telemt маскирует трафик"
|
||||
I18N[install_lite_desc2]="под выбранный сайт (google.com и т.д.)"
|
||||
I18N[install_pro_title]="🛡 Pro — свой сайт + полная маскировка"
|
||||
I18N[install_pro_desc1]="nginx + SSL + HTML-шаблон + telemt."
|
||||
I18N[install_pro_desc2]="DPI видит реальный сайт с реальным сертификатом."
|
||||
I18N[install_pro_desc3]="Требует: домен, направленный на этот сервер."
|
||||
I18N[install_mode_choice]="Выбор (1/2):"
|
||||
I18N[install_bad_choice]="Неверный выбор: %s"
|
||||
I18N[install_lite_step]="Установка Lite-режима"
|
||||
I18N[install_pro_step]="Установка Pro-режима"
|
||||
I18N[install_enter_domain]="Введите ваш домен (например, example.com):"
|
||||
I18N[install_bad_domain]="Некорректный домен: %s"
|
||||
I18N[install_dns_mismatch]="Домен %s указывает на %s, а не на %s"
|
||||
I18N[install_continue_anyway]="Продолжить всё равно?"
|
||||
I18N[install_enter_email]="Email для SSL (Enter = без email):"
|
||||
I18N[install_config_title]="📋 Конфигурация:"
|
||||
I18N[install_cfg_ip]="IP:"
|
||||
I18N[install_cfg_port]="Порт:"
|
||||
I18N[install_cfg_mask]="Маскировка:"
|
||||
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_arch_desc1]="telemt принимает весь трафик на 443 (маскировка под HTTPS)"
|
||||
I18N[install_arch_desc2]="nginx обслуживает сайт на внутреннем порту %s"
|
||||
I18N[install_arch_desc3]="Провайдер видит только HTTPS-трафик к %s:443"
|
||||
|
||||
# ── Change mode/template ────────────────────────────────────────────────
|
||||
I18N[change_current_mode]="Текущий режим:"
|
||||
I18N[change_template]="Сменить шаблон сайта (только pro)"
|
||||
I18N[change_mode_switch]="Переключить режим (lite ↔ pro)"
|
||||
I18N[change_only_pro]="Смена шаблона доступна только в pro-режиме"
|
||||
I18N[change_requires_reinstall]="Переключение режима требует переустановки."
|
||||
I18N[change_reinstall_confirm]="Переустановить прокси?"
|
||||
|
||||
# ── Logs ────────────────────────────────────────────────────────────────
|
||||
I18N[logs_telemt_title]="📋 Логи telemt (последние %s строк):"
|
||||
|
||||
# ── Link / Share ────────────────────────────────────────────────────────
|
||||
I18N[link_title]="🔗 Ссылка для подключения:"
|
||||
I18N[share_title]="📤 Перешлите это сообщение:"
|
||||
I18N[share_line1]="🔐 MTProxy для Telegram (GoTelegram v%s)"
|
||||
I18N[share_server]="🌍 Сервер: %s"
|
||||
I18N[share_port]="🔌 Порт: %s"
|
||||
I18N[share_connect_cta]="👉 Подключиться одним нажатием:"
|
||||
I18N[share_footer]="Просто нажмите на ссылку или настройте вручную."
|
||||
|
||||
# ── Website ─────────────────────────────────────────────────────────────
|
||||
I18N[website_title]="🌐 Управление сайтом"
|
||||
I18N[website_domain]="Домен:"
|
||||
I18N[website_ssl_until]="SSL до:"
|
||||
I18N[website_only_pro]="Управление сайтом доступно только в pro-режиме"
|
||||
I18N[website_renew_ssl]="Обновить SSL сертификат"
|
||||
I18N[website_restart_nginx]="Перезапустить nginx"
|
||||
I18N[website_change_template]="Сменить шаблон"
|
||||
|
||||
# ── Remove ──────────────────────────────────────────────────────────────
|
||||
I18N[remove_title]="🗑 Удаление GoTelegram"
|
||||
I18N[remove_proxy_only]="Удалить только прокси (telemt)"
|
||||
I18N[remove_bot_only]="Удалить только Telegram-бота"
|
||||
I18N[remove_all]="Удалить всё (прокси + бот + настройки)"
|
||||
I18N[remove_warn_proxy]="Это удалит прокси и все его настройки."
|
||||
I18N[remove_confirm_proxy]="Удалить прокси?"
|
||||
I18N[remove_backup_before]="Сделать бекап перед удалением?"
|
||||
I18N[remove_warn_all]="Это удалит ВСЁ: прокси, бот, сайт, настройки."
|
||||
I18N[remove_confirm_all]="Вы точно уверены?"
|
||||
I18N[remove_proxy_done]="Прокси удалён"
|
||||
I18N[remove_all_done]="GoTelegram полностью удалён (прокси + бот)"
|
||||
|
||||
# ── Telegram bot submenu ────────────────────────────────────────────────
|
||||
I18N[bot_title]="🤖 Telegram-бот"
|
||||
I18N[bot_status_running]="● Работает"
|
||||
I18N[bot_status_stopped]="○ Остановлен"
|
||||
I18N[bot_status_not_installed]="✗ Не установлен"
|
||||
I18N[bot_menu_status]="📊 Статус бота"
|
||||
I18N[bot_menu_logs]="📋 Логи бота"
|
||||
I18N[bot_menu_restart]="🔄 Перезапустить бота"
|
||||
I18N[bot_menu_stop]="⏹ Остановить бота"
|
||||
I18N[bot_menu_start]="▶️ Запустить бота"
|
||||
I18N[bot_menu_settings]="⚙️ Настройки (.env)"
|
||||
I18N[bot_menu_remove]="🗑 Удалить бота"
|
||||
I18N[bot_menu_install]="🔧 Установить бота"
|
||||
I18N[bot_intro1]="Бот позволяет управлять прокси прямо из Telegram:"
|
||||
I18N[bot_intro2]="статус, перезапуск, смена режима, бекап, QR-код."
|
||||
I18N[bot_install_step]="Установка Telegram-бота"
|
||||
I18N[bot_install_python]="Установка Python3..."
|
||||
I18N[bot_files_not_found]="Файлы бота не найдены в %s"
|
||||
I18N[bot_create_venv]="Создание виртуального окружения..."
|
||||
I18N[bot_install_deps]="Установка зависимостей..."
|
||||
I18N[bot_enter_token]="Введите BOT_TOKEN от @BotFather:"
|
||||
I18N[bot_token_empty]="Токен не может быть пустым"
|
||||
I18N[bot_token]="Token:"
|
||||
I18N[bot_add_admin_how]="Как добавить администратора?"
|
||||
I18N[bot_admin_auto]="Автоматически — бот определит ID при первом /start"
|
||||
I18N[bot_admin_manual]="Вручную — ввести ID сейчас"
|
||||
I18N[bot_admin_ids_prompt]="ID администраторов (через пробел/запятую):"
|
||||
I18N[bot_env_created]=".env создан"
|
||||
I18N[bot_env_exists]=".env уже существует, настройки сохранены"
|
||||
I18N[bot_wait_admin_title]="Ожидание администратора"
|
||||
I18N[bot_wait_admin_msg1]="Откройте бота в Telegram и отправьте"
|
||||
I18N[bot_wait_admin_msg2]="Бот автоматически назначит вас администратором"
|
||||
I18N[bot_wait_admin_skip]="Нажмите Ctrl+C чтобы пропустить"
|
||||
I18N[bot_wait_spinner]="Ожидание... напишите /start боту (%d сек)"
|
||||
I18N[bot_admin_assigned]="Администратор назначен!"
|
||||
I18N[bot_wait_skipped]="Пропущено. Добавить админа позже: меню → Telegram-бот → Настройки"
|
||||
I18N[bot_wait_timeout]="Таймаут (5 мин). Добавить админа: меню → Telegram-бот → Настройки"
|
||||
I18N[bot_installed]="Бот установлен и запущен!"
|
||||
I18N[bot_status_title]="📊 Статус Telegram-бота"
|
||||
I18N[bot_token_configured]="настроен"
|
||||
I18N[bot_access_open]="все пользователи"
|
||||
I18N[bot_logs_title]="📋 Логи бота (последние 30 строк):"
|
||||
I18N[bot_settings_title]="⚙️ Настройки бота"
|
||||
I18N[bot_current_env]="Текущий .env:"
|
||||
I18N[bot_change_token]="Сменить BOT_TOKEN"
|
||||
I18N[bot_change_allowed]="Изменить ALLOWED_IDS"
|
||||
I18N[bot_new_token]="Новый BOT_TOKEN:"
|
||||
I18N[bot_token_empty_err]="Пустой токен"
|
||||
I18N[bot_token_updated]="Токен обновлён, бот перезапущен"
|
||||
I18N[bot_allowed_prompt]="ALLOWED_IDS (через пробел/запятую, пусто = авто):"
|
||||
I18N[bot_access_updated]="Доступ обновлён, бот перезапущен"
|
||||
I18N[bot_remove_warn]="Это удалит Telegram-бота и все его настройки."
|
||||
I18N[bot_remove_confirm]="Удалить бота?"
|
||||
I18N[bot_removed]="Бот полностью удалён"
|
||||
I18N[bot_restarted]="Бот перезапущен"
|
||||
I18N[bot_stopped]="Бот остановлен"
|
||||
I18N[bot_started]="Бот запущен"
|
||||
I18N[bot_status_colon]="Статус:"
|
||||
I18N[bot_access_colon]="Доступ:"
|
||||
I18N[bot_access_ids_fmt]="ID: %s"
|
||||
|
||||
# ── Promo / Donate ──────────────────────────────────────────────────────
|
||||
I18N[promo_host1_title]="💰 ХОСТИНГ #1 — СКИДКА ДО 60%"
|
||||
I18N[promo_host2_title]="💰 ХОСТИНГ #2 — СКИДКА ДО 60%"
|
||||
I18N[promo_tips_title]="☕ Донат / Чаевые"
|
||||
I18N[promo_link_label]="Ссылка:"
|
||||
I18N[promo_off60]="60%% скидки на первый месяц"
|
||||
I18N[promo_ant20]="20%% + 3%% при оплате за 3 месяца"
|
||||
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_menu_in]="Меню через %d сек..."
|
||||
|
||||
# ── Stats ───────────────────────────────────────────────────────────────
|
||||
I18N[stats_title]="📊 Статистика трафика"
|
||||
I18N[stats_module_missing]="Модуль статистики не загружен."
|
||||
I18N[stats_file_missing]="Файл lib/stats.sh не найден."
|
||||
I18N[stats_toggle]="Вкл/Выкл подсчёт (сейчас: %s)"
|
||||
I18N[stats_install_collector]="Установить/обновить сборщик статистики"
|
||||
I18N[stats_auto_refresh]="Обновление каждые 3 сек"
|
||||
I18N[stats_on]="вкл"
|
||||
I18N[stats_off]="выкл"
|
||||
|
||||
# ── Templates catalog ───────────────────────────────────────────────────
|
||||
I18N[templates_categories]="📂 Категории шаблонов сайтов:"
|
||||
I18N[templates_custom_git]="📎 Свой шаблон по git URL"
|
||||
I18N[templates_random]="🎲 Случайный шаблон"
|
||||
I18N[templates_count_fmt]="(%d шаблонов)"
|
||||
I18N[templates_list]="📋 %s — доступные шаблоны:"
|
||||
I18N[templates_preview_title]="🔍 Превью шаблона:"
|
||||
I18N[templates_name]="Название:"
|
||||
I18N[templates_source]="Источник:"
|
||||
I18N[templates_description]="Описание:"
|
||||
I18N[templates_preview]="👁 Превью:"
|
||||
I18N[templates_preview_hint]="Откройте ссылку в браузере для просмотра шаблона"
|
||||
I18N[templates_repo]="📦 Репо:"
|
||||
I18N[templates_thanks]="💜 Спасибо авторам %s за открытый код!"
|
||||
I18N[templates_install_this]="Установить этот шаблон?"
|
||||
I18N[templates_cat_empty]="В этой категории нет шаблонов"
|
||||
I18N[templates_downloading]="Скачивание шаблона \"%s\"..."
|
||||
I18N[templates_downloaded]="Шаблон \"%s\" скачан"
|
||||
I18N[templates_downloaded_subfolder]="Шаблон \"%s\" скачан (из подпапки)"
|
||||
I18N[templates_no_index]="Шаблон не содержит index.html"
|
||||
I18N[templates_path]="Путь: %s"
|
||||
I18N[templates_catalog_not_found]="Каталог шаблонов не найден: %s"
|
||||
|
||||
# ── Custom git template ─────────────────────────────────────────────────
|
||||
I18N[custom_git_title]="📎 СВОЙ ШАБЛОН ПО GIT URL"
|
||||
I18N[custom_git_help_1]="Вы можете использовать ЛЮБОЙ репозиторий со статическим HTML-сайтом"
|
||||
I18N[custom_git_help_2]="в качестве шаблона. Репозиторий должен быть публичным и содержать"
|
||||
I18N[custom_git_help_3]="готовый index.html (сборка через npm НЕ выполняется)."
|
||||
I18N[custom_git_formats]="Поддерживаемые форматы URL:"
|
||||
I18N[custom_git_fmt_github]=" • https://github.com/user/repo"
|
||||
I18N[custom_git_fmt_gitlab]=" • https://gitlab.com/user/repo"
|
||||
I18N[custom_git_fmt_gitext]=" • https://example.com/user/repo.git"
|
||||
I18N[custom_git_fmt_branch]=" • https://github.com/user/repo@branch (ветка после @)"
|
||||
I18N[custom_git_auto_detect]="Структура репозитория (авто-определение):"
|
||||
I18N[custom_git_auto_1]=" 1. index.html в корне репозитория"
|
||||
I18N[custom_git_auto_2]=" 2. dist/index.html (StartBootstrap, Vite, webpack)"
|
||||
I18N[custom_git_auto_3]=" 3. public/ или build/ или _site/ или site/ или docs/"
|
||||
I18N[custom_git_auto_4]=" 4. Fallback: поиск index.html по всему репозиторию"
|
||||
I18N[custom_git_requirements]="Требования:"
|
||||
I18N[custom_git_req_1]=" • Только HTTPS (ssh:// и git:// блокируются)"
|
||||
I18N[custom_git_req_2]=" • Только публичные репозитории"
|
||||
I18N[custom_git_req_3]=" • Размер репо не более 100 МБ"
|
||||
I18N[custom_git_req_4]=" • Статический HTML (без серверного кода PHP/Python/Node)"
|
||||
I18N[custom_git_examples]="Примеры проверенных репо:"
|
||||
I18N[custom_git_ex_1]=" • https://github.com/html5up-collective/strata"
|
||||
I18N[custom_git_ex_2]=" • https://github.com/StartBootstrap/startbootstrap-landing-page"
|
||||
I18N[custom_git_enter_url]="Вставьте git URL (или Enter для отмены):"
|
||||
I18N[custom_git_empty]="URL не указан, отмена"
|
||||
I18N[custom_git_bad_url]="Недопустимый URL. Принимаются только https:// адреса"
|
||||
I18N[custom_git_cloning]="Клонирование репозитория..."
|
||||
I18N[custom_git_clone_failed]="Не удалось клонировать репозиторий: %s"
|
||||
I18N[custom_git_too_big]="Репозиторий слишком большой: %s (лимит 100MB)"
|
||||
I18N[custom_git_scanning]="Поиск index.html в структуре..."
|
||||
I18N[custom_git_found_at]="✓ Найден index.html в: %s"
|
||||
I18N[custom_git_no_index]="index.html не найден в репозитории"
|
||||
I18N[custom_git_installed]="Свой шаблон установлен из %s"
|
||||
I18N[custom_git_saved]="URL шаблона сохранён в конфиге (меню → Сайт → Обновить из git)"
|
||||
|
||||
# ── First-run language picker ───────────────────────────────────────────
|
||||
I18N[lang_picker_title]="Выберите язык / Select language"
|
||||
I18N[lang_english]="English"
|
||||
I18N[lang_russian]="Русский"
|
||||
I18N[lang_saved]="Язык сохранён: %s"
|
||||
I18N[lang_change_prompt]="Выберите новый язык:"
|
||||
|
||||
# ── Backup ──────────────────────────────────────────────────────────────
|
||||
I18N[backup_title]="💾 Бекап"
|
||||
I18N[backup_creating]="Создание бекапа..."
|
||||
I18N[backup_created]="Бекап создан: %s"
|
||||
I18N[backup_failed]="Ошибка создания бекапа"
|
||||
I18N[backup_restore_title]="↩️ Восстановление из бекапа"
|
||||
I18N[backup_no_files]="Нет файлов бекапа"
|
||||
I18N[backup_select]="Выберите бекап для восстановления:"
|
||||
I18N[backup_restoring]="Восстановление..."
|
||||
I18N[backup_restored]="Бекап восстановлен"
|
||||
I18N[backup_collecting]="Собираю конфигурацию..."
|
||||
I18N[backup_site_included]="Шаблон сайта включён"
|
||||
I18N[backup_archive_err]="Ошибка создания архива"
|
||||
I18N[backup_archive_missing]="Архив не создан"
|
||||
I18N[backup_encrypt_err]="Ошибка шифрования"
|
||||
I18N[backup_encrypted]="Бекап зашифрован (AES-256-CBC)"
|
||||
I18N[backup_created_fmt]="Бекап создан: %s (%s)"
|
||||
I18N[backup_file_not_found_fmt]="Файл не найден: %s"
|
||||
I18N[backup_enter_pass]="Введите пароль от бекапа"
|
||||
I18N[backup_bad_pass]="Неверный пароль или повреждённый файл"
|
||||
I18N[backup_extract_err]="Ошибка распаковки архива"
|
||||
I18N[backup_label]="Бекап"
|
||||
I18N[backup_version_label]="Версия"
|
||||
I18N[backup_mode_label]="Режим"
|
||||
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_lang]="Язык интерфейса восстановлен"
|
||||
I18N[backup_restored_nginx]="nginx конфиг восстановлен"
|
||||
I18N[backup_restored_ssl]="SSL сертификаты восстановлены"
|
||||
I18N[backup_restored_site]="Шаблон сайта восстановлен"
|
||||
I18N[backup_restore_done]="Восстановление завершено!"
|
||||
I18N[backup_none]="Бекапов нет"
|
||||
I18N[backup_list_title]="Доступные бекапы"
|
||||
I18N[backup_cleanup_fmt]="Удалено %s старых бекапов (оставлено %s)"
|
||||
I18N[backup_create_title]="Создание бекапа"
|
||||
I18N[backup_encrypt_prompt]="Зашифровать бекап паролем?"
|
||||
I18N[backup_repeat_pass]="Повторите пароль"
|
||||
I18N[backup_pass_mismatch]="Пароли не совпадают"
|
||||
I18N[backup_pass_short]="Пароль слишком короткий (минимум 6 символов)"
|
||||
I18N[backup_pick_prompt]="Номер бекапа (или путь к файлу)"
|
||||
I18N[backup_not_found]="Бекап не найден"
|
||||
|
||||
# ── Preflight (v2.4.8) ──────────────────────────────────────────────────
|
||||
I18N[preflight_title]="Предустановочная проверка: обнаружены конфликты портов"
|
||||
I18N[preflight_known]="Известный proxy/VPN/веб-софт занимает нужные порты:"
|
||||
I18N[preflight_unknown]="Порты заняты неизвестными процессами:"
|
||||
I18N[preflight_needed]="GoTelegram нужны порты:"
|
||||
I18N[preflight_hint_header]="Рекомендации:"
|
||||
I18N[preflight_hint1]="Остановите и удалите конфликтующие сервисы (systemctl stop ...)"
|
||||
I18N[preflight_hint2]="Либо возьмите чистый VPS без других прокси"
|
||||
I18N[preflight_hint3]="Установка поверх, скорее всего, завершится некорректно"
|
||||
I18N[preflight_skip_hint]="Обойти проверку: GOTELEGRAM_SKIP_PREFLIGHT=1 gotelegram"
|
||||
I18N[preflight_proceed]="Продолжить установку всё равно (скорее всего не заработает)?"
|
||||
I18N[preflight_forced]="Установка продолжена вопреки конфликтам — возможны ошибки"
|
||||
I18N[preflight_aborted]="Установка отменена из-за конфликтов портов"
|
||||
|
||||
# ── Errors / misc ───────────────────────────────────────────────────────
|
||||
I18N[err_need_root]="Запустите скрипт с sudo / от root"
|
||||
I18N[err_os_unknown]="Не удалось определить ОС. Требуется Linux."
|
||||
I18N[err_low_disk]="Мало места на диске: %sMB (нужно %sMB+)"
|
||||
I18N[err_bad_pkg_mgr]="Неизвестный пакетный менеджер"
|
||||
I18N[err_unexpected]="Неожиданная ошибка"
|
||||
I18N[bye]="До встречи! 👋"
|
||||
I18N[auto_refresh]="Обновление через 30 сек"
|
||||
|
||||
# ── Deps ────────────────────────────────────────────────────────────────
|
||||
I18N[deps_installing]="Установка зависимостей: %s"
|
||||
|
||||
# ── Migration ───────────────────────────────────────────────────────────
|
||||
I18N[v1_detected]="⚠️ Обнаружена установка GoTelegram v1 (mtg)"
|
||||
I18N[v1_container]="Контейнер: %s"
|
||||
I18N[v1_migration_step]="Миграция с v1 (mtg) на v2 (telemt)"
|
||||
I18N[v1_found_title]="Найдена установка v1 (mtg):"
|
||||
I18N[v1_port]="Порт: %s"
|
||||
I18N[v1_secret]="Secret: %s..."
|
||||
I18N[v1_incompatible]="секрет mtg НЕ совместим с telemt напрямую."
|
||||
I18N[v1_new_link]="Клиентам потребуется новая ссылка."
|
||||
I18N[v1_stop_migrate]="Остановить v1 контейнер и перейти на v2? [Y/n]:"
|
||||
I18N[v1_migration_cancelled]="Миграция отменена. v1 оставлен без изменений."
|
||||
I18N[v1_stopping]="Остановка v1 контейнера..."
|
||||
I18N[v1_config_saved]="Конфиг v1 сохранён в %s"
|
||||
I18N[v1_port_freed]="v1 остановлен. Порт %s освобождён."
|
||||
|
||||
# ── v2.4.9: UBF v2.0 backup + manual secret recovery ─────────────────────
|
||||
I18N[install_source_title]="Источник установки"
|
||||
I18N[install_source_choice]="Выберите источник [1-3]:"
|
||||
I18N[install_menu_new]="Новая установка"
|
||||
I18N[install_menu_new_desc]="Сгенерировать новый ключ и настроить с нуля"
|
||||
I18N[install_menu_restore]="Восстановить из бекапа"
|
||||
I18N[install_menu_restore_desc]="Полное восстановление из файла .tar.gz[.enc]"
|
||||
I18N[install_menu_existing_key]="Использовать существующий ключ"
|
||||
I18N[install_menu_existing_key_desc]="Ввести ссылку tg://proxy или ключ вручную"
|
||||
I18N[install_hint_pro_mode]="Ключ содержит домен — обычно это Pro режим"
|
||||
I18N[install_reuse_secret]="Используется переданный ключ"
|
||||
I18N[install_reuse_domain]="Используется домен из ключа"
|
||||
I18N[install_reuse_port]="Используется порт из ключа"
|
||||
|
||||
I18N[manual_secret_title]="Ввод существующего ключа"
|
||||
I18N[manual_secret_help1]="Поддерживаются форматы:"
|
||||
I18N[manual_secret_prompt]="Вставьте ключ"
|
||||
I18N[manual_secret_empty]="Ключ не введён"
|
||||
I18N[manual_secret_bad]="Не удалось распознать формат ключа"
|
||||
I18N[manual_secret_parsed]="Ключ распознан"
|
||||
|
||||
I18N[backup_id_label]="Backup ID"
|
||||
I18N[backup_file_label]="Файл"
|
||||
I18N[backup_size_label]="Размер"
|
||||
I18N[backup_key_label]="Ключ в бекапе (fingerprint)"
|
||||
I18N[backup_format_label]="Формат"
|
||||
I18N[backup_mode_label]="Режим"
|
||||
I18N[backup_lang_label]="Язык"
|
||||
I18N[backup_date_label]="Дата"
|
||||
I18N[backup_label]="Бекап"
|
||||
I18N[backup_ssl_included]="SSL-сертификаты включены (+ chain + renewal)"
|
||||
I18N[backup_site_included]="Шаблон сайта включён"
|
||||
I18N[backup_bot_included]="Конфиг Telegram-бота включён"
|
||||
I18N[backup_restored_bot]="Конфиг Telegram-бота восстановлен"
|
||||
I18N[backup_automigrate]="Конвертирую старый бекап в UBF v2.0..."
|
||||
I18N[backup_migrated]="Свежий UBF v2.0 бекап сохранён"
|
||||
I18N[backup_collecting]="Собираю конфигурацию..."
|
||||
I18N[backup_archive_err]="Ошибка создания архива"
|
||||
I18N[backup_archive_missing]="Архив не создан"
|
||||
I18N[backup_encrypt_err]="Ошибка шифрования"
|
||||
I18N[backup_encrypted]="Бекап зашифрован (AES-256-CBC)"
|
||||
I18N[backup_created]="Бекап создан"
|
||||
I18N[backup_enter_pass]="Введите пароль"
|
||||
I18N[backup_repeat_pass]="Повторите пароль"
|
||||
I18N[backup_pass_mismatch]="Пароли не совпадают"
|
||||
I18N[backup_pass_short]="Пароль слишком короткий (минимум 6 символов)"
|
||||
I18N[backup_bad_pass]="Неверный пароль или повреждённый файл"
|
||||
I18N[backup_extract_err]="Ошибка распаковки архива"
|
||||
I18N[backup_confirm_restore]="Восстановить конфигурацию? Текущие настройки будут перезаписаны."
|
||||
I18N[backup_restored_telemt]="telemt конфиг восстановлен"
|
||||
I18N[backup_restored_gotelegram]="GoTelegram конфиг восстановлен"
|
||||
I18N[backup_restored_lang]="Язык интерфейса восстановлен"
|
||||
I18N[backup_restored_nginx]="nginx конфиг восстановлен"
|
||||
I18N[backup_restored_ssl]="SSL сертификаты восстановлены"
|
||||
I18N[backup_restored_site]="Шаблон сайта восстановлен"
|
||||
I18N[backup_restore_done]="Восстановление завершено!"
|
||||
I18N[backup_create_title]="Создание бекапа"
|
||||
I18N[backup_encrypt_prompt]="Зашифровать бекап паролем?"
|
||||
I18N[backup_none]="Бекапов нет"
|
||||
I18N[backup_list_title]="Доступные бекапы"
|
||||
I18N[backup_pick_prompt]="Номер бекапа (или путь к файлу)"
|
||||
I18N[backup_not_found]="Бекап не найден"
|
||||
I18N[backup_file_not_found_fmt]="Файл не найден: %s"
|
||||
I18N[backup_cleanup_fmt]="Удалено %s старых бекапов (оставлено %s)"
|
||||
60
lib/stats.sh
Executable file → Normal file
60
lib/stats.sh
Executable file → Normal file
@@ -91,6 +91,13 @@ EOF
|
||||
cp "$CURRENT_SNAPSHOT" "$snapshot_file" 2>/dev/null
|
||||
|
||||
# Append to history CSV (once per minute, check if last entry is fresh)
|
||||
# Auto-recreate the file with header if it was deleted — otherwise the
|
||||
# collector would silently stop writing history after any wipe (v2.4.1 fix).
|
||||
if [[ ! -f "$HISTORY_FILE" ]]; then
|
||||
mkdir -p "$(dirname "$HISTORY_FILE")" 2>/dev/null
|
||||
echo "epoch,proxy_bytes,site_bytes" > "$HISTORY_FILE" 2>/dev/null
|
||||
fi
|
||||
|
||||
if [[ -f "$HISTORY_FILE" ]]; then
|
||||
local last_ts
|
||||
last_ts=$(grep -E '^[0-9]' "$HISTORY_FILE" 2>/dev/null | tail -1 | cut -d, -f1)
|
||||
@@ -236,36 +243,49 @@ show_traffic_stats() {
|
||||
local site_rates=$(stats_calculate_rates "site")
|
||||
IFS='|' read -r s1m s1mr s5m s5mr s60m s60mr s1d s1dr s7d s7dr s30d s30dr s365d s365dr <<< "$site_rates"
|
||||
|
||||
# i18n labels (fall back to English if t() not loaded)
|
||||
local lbl_proxy; lbl_proxy="$(_t_or stats_sh_proxy 'Proxy (telemt, port 443)')"
|
||||
local lbl_site; lbl_site="$(_t_or stats_sh_site 'Site (nginx, port 8443)')"
|
||||
local lbl_hdr; lbl_hdr="$(_t_or stats_sh_hdr_period 'Period') │ $(_t_or stats_sh_hdr_inbound 'Inbound') │ $(_t_or stats_sh_hdr_rate 'Rate')"
|
||||
local lbl_pkts; lbl_pkts="$(_t_or stats_sh_packets 'Packets')"
|
||||
local l1m; l1m="$(_t_or stats_sh_1min '1 min')"
|
||||
local l5m; l5m="$(_t_or stats_sh_5min '5 min')"
|
||||
local l60m; l60m="$(_t_or stats_sh_60min '60 min')"
|
||||
local l1d; l1d="$(_t_or stats_sh_1day '1 day')"
|
||||
local l7d; l7d="$(_t_or stats_sh_7days '7 days')"
|
||||
local l30d; l30d="$(_t_or stats_sh_30days '30 days')"
|
||||
local l365d; l365d="$(_t_or stats_sh_365days '365 days')"
|
||||
|
||||
# Display proxy stats
|
||||
{
|
||||
echo ""
|
||||
echo -e "${BLUE} Proxy (telemt, порт 443):${NC}"
|
||||
echo -e "${BLUE} ${lbl_proxy}:${NC}"
|
||||
echo -e "${BLUE} ─────────────────────────────────────────${NC}"
|
||||
echo -e "${BLUE} Период │ Входящий │ Скорость${NC}"
|
||||
echo -e "${BLUE} ${lbl_hdr}${NC}"
|
||||
echo -e "${BLUE} ─────────────────────────────────────────${NC}"
|
||||
printf " %-9s │ %14s │ %s\n" "1 мин" "$p1m" "$p1mr"
|
||||
printf " %-9s │ %14s │ %s\n" "5 мин" "$p5m" "$p5mr"
|
||||
printf " %-9s │ %14s │ %s\n" "60 мин" "$p60m" "$p60mr"
|
||||
printf " %-9s │ %14s │ %s\n" "1 день" "$p1d" "$p1dr"
|
||||
printf " %-9s │ %14s │ %s\n" "7 дней" "$p7d" "$p7dr"
|
||||
printf " %-9s │ %14s │ %s\n" "30 дней" "$p30d" "$p30dr"
|
||||
printf " %-9s │ %14s │ %s\n" "365 дней" "$p365d" "$p365dr"
|
||||
printf " %-9s │ %14s │ %s\n" "$l1m" "$p1m" "$p1mr"
|
||||
printf " %-9s │ %14s │ %s\n" "$l5m" "$p5m" "$p5mr"
|
||||
printf " %-9s │ %14s │ %s\n" "$l60m" "$p60m" "$p60mr"
|
||||
printf " %-9s │ %14s │ %s\n" "$l1d" "$p1d" "$p1dr"
|
||||
printf " %-9s │ %14s │ %s\n" "$l7d" "$p7d" "$p7dr"
|
||||
printf " %-9s │ %14s │ %s\n" "$l30d" "$p30d" "$p30dr"
|
||||
printf " %-9s │ %14s │ %s\n" "$l365d" "$p365d" "$p365dr"
|
||||
echo -e "${BLUE} ─────────────────────────────────────────${NC}"
|
||||
printf " Пакетов: %d\n\n" "$proxy_pkts"
|
||||
printf " %s: %d\n\n" "$lbl_pkts" "$proxy_pkts"
|
||||
|
||||
echo -e "${BLUE} Сайт (nginx, порт 8443):${NC}"
|
||||
echo -e "${BLUE} ${lbl_site}:${NC}"
|
||||
echo -e "${BLUE} ─────────────────────────────────────────${NC}"
|
||||
echo -e "${BLUE} Период │ Входящий │ Скорость${NC}"
|
||||
echo -e "${BLUE} ${lbl_hdr}${NC}"
|
||||
echo -e "${BLUE} ─────────────────────────────────────────${NC}"
|
||||
printf " %-9s │ %14s │ %s\n" "1 мин" "$s1m" "$s1mr"
|
||||
printf " %-9s │ %14s │ %s\n" "5 мин" "$s5m" "$s5mr"
|
||||
printf " %-9s │ %14s │ %s\n" "60 мин" "$s60m" "$s60mr"
|
||||
printf " %-9s │ %14s │ %s\n" "1 день" "$s1d" "$s1dr"
|
||||
printf " %-9s │ %14s │ %s\n" "7 дней" "$s7d" "$s7dr"
|
||||
printf " %-9s │ %14s │ %s\n" "30 дней" "$s30d" "$s30dr"
|
||||
printf " %-9s │ %14s │ %s\n" "365 дней" "$s365d" "$s365dr"
|
||||
printf " %-9s │ %14s │ %s\n" "$l1m" "$s1m" "$s1mr"
|
||||
printf " %-9s │ %14s │ %s\n" "$l5m" "$s5m" "$s5mr"
|
||||
printf " %-9s │ %14s │ %s\n" "$l60m" "$s60m" "$s60mr"
|
||||
printf " %-9s │ %14s │ %s\n" "$l1d" "$s1d" "$s1dr"
|
||||
printf " %-9s │ %14s │ %s\n" "$l7d" "$s7d" "$s7dr"
|
||||
printf " %-9s │ %14s │ %s\n" "$l30d" "$s30d" "$s30dr"
|
||||
printf " %-9s │ %14s │ %s\n" "$l365d" "$s365d" "$s365dr"
|
||||
echo -e "${BLUE} ─────────────────────────────────────────${NC}"
|
||||
printf " Пакетов: %d\n" "$site_pkts"
|
||||
printf " %s: %d\n" "$lbl_pkts" "$site_pkts"
|
||||
echo ""
|
||||
} >&2
|
||||
}
|
||||
|
||||
101
lib/telemt.sh
Executable file → Normal file
101
lib/telemt.sh
Executable file → Normal file
@@ -19,27 +19,55 @@ get_latest_telemt_version() {
|
||||
}
|
||||
|
||||
get_telemt_download_url() {
|
||||
local arch
|
||||
# 1) Сначала пробуем GitHub Releases API — он отдаёт точное имя ассета
|
||||
# последнего релиза (в т.ч. если в репо есть несколько архитектур,
|
||||
# pre-release и т.д.). Это наш предпочтительный путь.
|
||||
local resp url arch
|
||||
arch=$(get_arch)
|
||||
local resp
|
||||
resp=$(curl -s --max-time 10 "$TELEMT_RELEASE_API" 2>/dev/null)
|
||||
if [ -z "$resp" ]; then return 1; fi
|
||||
if [ -n "$resp" ]; then
|
||||
url=$(echo "$resp" | jq -r --arg a "$arch" '
|
||||
.assets[]?.browser_download_url
|
||||
| select(test("linux"))
|
||||
| select(
|
||||
($a == "amd64" and (test("x86_64|amd64"))) or
|
||||
($a == "arm64" and (test("aarch64|arm64"))) or
|
||||
($a == "armv7" and (test("armv7"))) or
|
||||
(test($a))
|
||||
)
|
||||
| select(test("gnu"))
|
||||
' 2>/dev/null | head -1)
|
||||
if [ -n "$url" ] && [ "$url" != "null" ]; then
|
||||
echo "$url"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
# URL format: telemt-x86_64-linux-gnu.tar.gz (arch BEFORE linux)
|
||||
local arch_pattern
|
||||
# 2) Fallback: API не ответил / отдал 403 (rate limit на shared-IP VPS),
|
||||
# отдал пустой JSON, или jq не нашёл подходящий ассет.
|
||||
# Берём прямой "magic redirect" CDN-URL — он не считается в API rate
|
||||
# limit и всегда указывает на последний релиз.
|
||||
local arch_name
|
||||
case "$arch" in
|
||||
amd64) arch_pattern="(amd64|x86_64)" ;;
|
||||
arm64) arch_pattern="(arm64|aarch64)" ;;
|
||||
armv7) arch_pattern="(armv7|arm)" ;;
|
||||
*) arch_pattern="${arch}" ;;
|
||||
amd64) arch_name="x86_64" ;;
|
||||
arm64) arch_name="aarch64" ;;
|
||||
armv7) arch_name="armv7" ;;
|
||||
*) arch_name="$arch" ;;
|
||||
esac
|
||||
echo "https://github.com/${TELEMT_GITHUB}/releases/latest/download/telemt-${arch_name}-linux-gnu.tar.gz"
|
||||
}
|
||||
|
||||
echo "$resp" | jq -r ".assets[].browser_download_url" 2>/dev/null \
|
||||
| grep -iE "$arch_pattern" \
|
||||
| grep -i "linux" \
|
||||
| grep -v "sha256" \
|
||||
| grep "gnu" \
|
||||
| head -1
|
||||
# Fallback URL using musl libc (some minimal distros / older glibc)
|
||||
get_telemt_download_url_musl() {
|
||||
local arch arch_name
|
||||
arch=$(get_arch)
|
||||
case "$arch" in
|
||||
amd64) arch_name="x86_64" ;;
|
||||
arm64) arch_name="aarch64" ;;
|
||||
armv7) arch_name="armv7" ;;
|
||||
*) arch_name="$arch" ;;
|
||||
esac
|
||||
echo "https://github.com/${TELEMT_GITHUB}/releases/latest/download/telemt-${arch_name}-linux-musl.tar.gz"
|
||||
}
|
||||
|
||||
# ── Установленная версия ─────────────────────────────────────────────────────
|
||||
@@ -69,18 +97,35 @@ download_telemt() {
|
||||
log_info "Скачивание: $url"
|
||||
|
||||
if ! curl -L -s --max-time 120 -o "$tmp_file" "$url"; then
|
||||
log_error "Ошибка скачивания telemt"
|
||||
rm -f "$tmp_file"
|
||||
return 1
|
||||
log_warning "Не удалось скачать gnu-сборку, пробую musl..."
|
||||
url=$(get_telemt_download_url_musl)
|
||||
log_info "Скачивание: $url"
|
||||
if ! curl -L -s --max-time 120 -o "$tmp_file" "$url"; then
|
||||
log_error "Ошибка скачивания telemt (gnu и musl)"
|
||||
rm -f "$tmp_file"
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Проверяем что файл не пустой и не HTML
|
||||
local file_size
|
||||
file_size=$(stat -c%s "$tmp_file" 2>/dev/null || echo 0)
|
||||
if [ "$file_size" -lt 1000 ]; then
|
||||
log_error "Скачанный файл слишком маленький ($file_size байт) — возможна ошибка сети"
|
||||
rm -f "$tmp_file"
|
||||
return 1
|
||||
# Try musl as fallback if gnu came back empty/error-html
|
||||
log_warning "Файл подозрительно мал ($file_size байт), пробую musl-сборку..."
|
||||
url=$(get_telemt_download_url_musl)
|
||||
log_info "Скачивание: $url"
|
||||
if ! curl -L -s --max-time 120 -o "$tmp_file" "$url"; then
|
||||
log_error "Ошибка скачивания telemt (musl fallback)"
|
||||
rm -f "$tmp_file"
|
||||
return 1
|
||||
fi
|
||||
file_size=$(stat -c%s "$tmp_file" 2>/dev/null || echo 0)
|
||||
if [ "$file_size" -lt 1000 ]; then
|
||||
log_error "Скачанный файл слишком маленький ($file_size байт) — возможна ошибка сети"
|
||||
rm -f "$tmp_file"
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Определяем тип файла и распаковываем
|
||||
@@ -187,8 +232,20 @@ EOF
|
||||
}
|
||||
|
||||
# ── Управление сервисом ──────────────────────────────────────────────────────
|
||||
# start_telemt ensures telemt is running with the CURRENT on-disk config.
|
||||
# If the service is already active we must restart (not plain start) — otherwise
|
||||
# the running process keeps its old in-memory config and the freshly generated
|
||||
# /etc/telemt/config.toml is silently ignored. This was the root cause of the
|
||||
# "lite-mode key doesn't work after reinstall" bug: telemt had loaded the
|
||||
# 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.
|
||||
start_telemt() {
|
||||
systemctl start "$TELEMT_SERVICE" 2>/dev/null
|
||||
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
|
||||
log_success "telemt запущен"
|
||||
|
||||
80
lib/telemt_config.sh
Executable file → Normal file
80
lib/telemt_config.sh
Executable file → Normal file
@@ -81,13 +81,17 @@ add_secret_to_config() {
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Добавляем новый блок [[users]]
|
||||
cat >> "$config" << EOSECRET
|
||||
# telemt v3: добавляем ключ в секцию [access.users]
|
||||
# Формат: name = "secret" под блоком [access.users]
|
||||
if grep -q '\[access\.users\]' "$config"; then
|
||||
sed -i "/\[access\.users\]/a ${name} = \"${secret}\"" "$config"
|
||||
else
|
||||
cat >> "$config" << EOSECRET
|
||||
|
||||
[[users]]
|
||||
name = "${name}"
|
||||
secret = "${secret}"
|
||||
[access.users]
|
||||
${name} = "${secret}"
|
||||
EOSECRET
|
||||
fi
|
||||
|
||||
log_success "Добавлен секрет: $name"
|
||||
}
|
||||
@@ -242,10 +246,19 @@ select_port() {
|
||||
|
||||
# ── Генерация ссылки tg://proxy ──────────────────────────────────────────────
|
||||
generate_proxy_link() {
|
||||
local ip="${1:-$(get_server_ip)}"
|
||||
local server="${1:-$(get_server_ip)}"
|
||||
local port="${2:-443}"
|
||||
local secret="$3"
|
||||
echo "tg://proxy?server=${ip}&port=${port}&secret=${secret}"
|
||||
local mask_host="${4:-}"
|
||||
|
||||
# Если указан mask_host (fake-TLS), формируем ee-секрет
|
||||
if [ -n "$mask_host" ]; then
|
||||
local domain_hex
|
||||
domain_hex=$(printf '%s' "$mask_host" | xxd -p | tr -d '\n')
|
||||
secret="ee${secret}${domain_hex}"
|
||||
fi
|
||||
|
||||
echo "tg://proxy?server=${server}&port=${port}&secret=${secret}"
|
||||
}
|
||||
|
||||
# ── Вывод информации о прокси ────────────────────────────────────────────────
|
||||
@@ -263,38 +276,35 @@ show_proxy_info() {
|
||||
mode=$(config_get mode 2>/dev/null || echo "lite")
|
||||
domain=$(config_get domain 2>/dev/null || echo "")
|
||||
|
||||
# Pro-режим: ссылка с доменом и fake-TLS секретом
|
||||
# Генерация ссылки: оба режима используют ee-секрет с mask_host
|
||||
if [ "$mode" = "pro" ] && [ -n "$domain" ]; then
|
||||
local domain_hex faketls_secret
|
||||
domain_hex=$(printf '%s' "$domain" | xxd -p | tr -d '\n')
|
||||
faketls_secret="ee${secret}${domain_hex}"
|
||||
link="tg://proxy?server=${domain}&port=${port}&secret=${faketls_secret}"
|
||||
link=$(generate_proxy_link "$domain" "$port" "$secret" "$domain")
|
||||
else
|
||||
link=$(generate_proxy_link "$ip" "$port" "$secret")
|
||||
link=$(generate_proxy_link "$ip" "$port" "$secret" "$mask_host")
|
||||
fi
|
||||
|
||||
local status_icon status_text
|
||||
case "$status" in
|
||||
running) status_icon="✅"; status_text="Работает" ;;
|
||||
stopped) status_icon="⏸️"; status_text="Остановлен" ;;
|
||||
*) status_icon="❌"; status_text="Не установлен" ;;
|
||||
running) status_icon="✅"; status_text="$(t info_status_running)" ;;
|
||||
stopped) status_icon="⏸️"; status_text="$(t info_status_stopped)" ;;
|
||||
*) status_icon="❌"; status_text="$(t info_status_not_installed)" ;;
|
||||
esac
|
||||
|
||||
echo ""
|
||||
echo -e " ${BOLD}${WHITE}${status_icon} Статус прокси: ${status_text}${NC}"
|
||||
echo -e " ${BOLD}${WHITE}${status_icon} $(t info_proxy_status): ${status_text}${NC}"
|
||||
echo -e " ${DIM}$(printf '─%.0s' {1..50})${NC}"
|
||||
echo -e " ${WHITE}Ядро:${NC} telemt (Rust)"
|
||||
echo -e " ${WHITE}$(t info_engine):${NC} telemt (Rust)"
|
||||
if [ "$mode" = "pro" ] && [ -n "$domain" ]; then
|
||||
echo -e " ${WHITE}Домен:${NC} ${CYAN}${domain}${NC}"
|
||||
echo -e " ${WHITE}$(t info_domain):${NC} ${CYAN}${domain}${NC}"
|
||||
else
|
||||
echo -e " ${WHITE}IP:${NC} ${CYAN}${ip}${NC}"
|
||||
echo -e " ${WHITE}$(t info_ip):${NC} ${CYAN}${ip}${NC}"
|
||||
fi
|
||||
echo -e " ${WHITE}Порт:${NC} ${CYAN}${port}${NC}"
|
||||
echo -e " ${WHITE}Режим:${NC} ${CYAN}${mode}${NC}"
|
||||
echo -e " ${WHITE}Маскировка:${NC} ${CYAN}${mask_host}${NC}"
|
||||
echo -e " ${WHITE}Secret:${NC} ${CYAN}${secret:0:16}...${NC}"
|
||||
echo -e " ${WHITE}$(t info_port):${NC} ${CYAN}${port}${NC}"
|
||||
echo -e " ${WHITE}$(t info_mode):${NC} ${CYAN}${mode}${NC}"
|
||||
echo -e " ${WHITE}$(t info_mask):${NC} ${CYAN}${mask_host}${NC}"
|
||||
echo -e " ${WHITE}$(t info_secret):${NC} ${CYAN}${secret:0:16}...${NC}"
|
||||
echo -e " ${DIM}$(printf '─%.0s' {1..50})${NC}"
|
||||
echo -e " ${WHITE}Ссылка:${NC}"
|
||||
echo -e " ${WHITE}$(t info_link):${NC}"
|
||||
echo -e " ${GREEN}${link}${NC}"
|
||||
echo ""
|
||||
|
||||
@@ -313,20 +323,20 @@ show_proxy_info_pro() {
|
||||
local link="tg://proxy?server=${domain}&port=443&secret=${faketls_secret}"
|
||||
|
||||
echo ""
|
||||
echo -e " ${BOLD}${WHITE}✅ Pro-прокси настроен${NC}"
|
||||
echo -e " ${BOLD}${WHITE}✅ $(t info_proxy_status): $(t info_status_running) (Pro)${NC}"
|
||||
echo -e " ${DIM}$(printf '─%.0s' {1..55})${NC}"
|
||||
echo -e " ${WHITE}Ядро:${NC} telemt (Rust)"
|
||||
echo -e " ${WHITE}Домен:${NC} ${CYAN}${domain}${NC}"
|
||||
echo -e " ${WHITE}Порт:${NC} ${CYAN}443${NC} (внешний, telemt)"
|
||||
echo -e " ${WHITE}Режим:${NC} ${MAGENTA}Pro (fake-TLS)${NC}"
|
||||
echo -e " ${WHITE}nginx:${NC} ${CYAN}127.0.0.1:8443${NC} (внутренний)"
|
||||
echo -e " ${WHITE}Secret:${NC} ${CYAN}${faketls_secret:0:20}...${NC}"
|
||||
echo -e " ${WHITE}$(t info_engine):${NC} telemt (Rust)"
|
||||
echo -e " ${WHITE}$(t info_domain):${NC} ${CYAN}${domain}${NC}"
|
||||
echo -e " ${WHITE}$(t info_port):${NC} ${CYAN}443${NC} (telemt)"
|
||||
echo -e " ${WHITE}$(t info_mode):${NC} ${MAGENTA}Pro (fake-TLS)${NC}"
|
||||
echo -e " ${WHITE}nginx:${NC} ${CYAN}127.0.0.1:8443${NC}"
|
||||
echo -e " ${WHITE}$(t info_secret):${NC} ${CYAN}${faketls_secret:0:20}...${NC}"
|
||||
echo -e " ${DIM}$(printf '─%.0s' {1..55})${NC}"
|
||||
echo -e " ${WHITE}Ссылка для Telegram:${NC}"
|
||||
echo -e " ${WHITE}$(t info_link):${NC}"
|
||||
echo -e " ${GREEN}${link}${NC}"
|
||||
echo ""
|
||||
echo -e " ${DIM}Провайдер видит: HTTPS-трафик к ${domain}:443${NC}"
|
||||
echo -e " ${DIM}Telegram-клиент маскирует соединение под TLS${NC}"
|
||||
echo -e " ${DIM}ISP sees: HTTPS → ${domain}:443${NC}"
|
||||
echo -e " ${DIM}Telegram client masquerades as TLS${NC}"
|
||||
echo ""
|
||||
|
||||
# QR если доступен
|
||||
|
||||
@@ -1,20 +1,29 @@
|
||||
#!/bin/bash
|
||||
# GoTelegram v2.2 — Каталог шаблонов сайтов
|
||||
# Выбор из ~200 шаблонов, превью-ссылки, скачивание через git sparse-checkout
|
||||
# GoTelegram v2.4 — website templates catalog
|
||||
# Pick from ~1800 templates, preview links, git sparse-checkout downloads,
|
||||
# + custom git URL templates (user-supplied public repos)
|
||||
|
||||
CATALOG_FILE="$(dirname "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")")/templates_catalog.json"
|
||||
TEMPLATES_CACHE="/tmp/gotelegram_templates"
|
||||
|
||||
# ── Загрузка каталога ────────────────────────────────────────────────────────
|
||||
# Custom git template limits
|
||||
CUSTOM_GIT_MAX_SIZE_MB=100
|
||||
CUSTOM_GIT_CLONE_TIMEOUT=90
|
||||
|
||||
# ── Catalog loading ────────────────────────────────────────────────────
|
||||
load_catalog() {
|
||||
if [ ! -f "$CATALOG_FILE" ]; then
|
||||
log_error "Каталог шаблонов не найден: $CATALOG_FILE"
|
||||
if type tf &>/dev/null; then
|
||||
log_error "$(tf templates_catalog_not_found "$CATALOG_FILE")"
|
||||
else
|
||||
log_error "Templates catalog not found: $CATALOG_FILE"
|
||||
fi
|
||||
return 1
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
# ── Категории ────────────────────────────────────────────────────────────────
|
||||
# ── Categories ─────────────────────────────────────────────────────────
|
||||
get_categories() {
|
||||
jq -r '.categories[] | "\(.id)|\(.name)|\(.icon)|\(.templates | length)"' "$CATALOG_FILE" 2>/dev/null
|
||||
}
|
||||
@@ -24,13 +33,13 @@ get_category_name() {
|
||||
jq -r ".categories[] | select(.id == \"$cat_id\") | .name" "$CATALOG_FILE" 2>/dev/null
|
||||
}
|
||||
|
||||
# ── Шаблоны по категории ────────────────────────────────────────────────────
|
||||
# ── Templates in a category ────────────────────────────────────────────
|
||||
get_templates_by_category() {
|
||||
local cat_id="$1"
|
||||
jq -r ".categories[] | select(.id == \"$cat_id\") | .templates[] | \"\(.id)|\(.name)|\(.source)|\(.preview_url)\"" "$CATALOG_FILE" 2>/dev/null
|
||||
}
|
||||
|
||||
# ── Информация о шаблоне ────────────────────────────────────────────────────
|
||||
# ── Template info ──────────────────────────────────────────────────────
|
||||
get_template_info() {
|
||||
local tpl_id="$1"
|
||||
jq ".categories[].templates[] | select(.id == \"$tpl_id\")" "$CATALOG_FILE" 2>/dev/null
|
||||
@@ -42,16 +51,19 @@ get_template_field() {
|
||||
jq -r ".categories[].templates[] | select(.id == \"$tpl_id\") | .$field" "$CATALOG_FILE" 2>/dev/null
|
||||
}
|
||||
|
||||
# ── Интерактивный выбор категории ────────────────────────────────────────────
|
||||
# ── Interactive category picker (returns category id or special __custom_git__/__random__) ──
|
||||
select_category() {
|
||||
load_catalog || return 1
|
||||
|
||||
echo "" >&2
|
||||
echo -e " ${BOLD}${WHITE}📂 Категории шаблонов сайтов:${NC}" >&2
|
||||
echo -e " ${BOLD}${WHITE}$(t templates_categories)${NC}" >&2
|
||||
echo -e " ${DIM}$(printf '─%.0s' {1..55})${NC}" >&2
|
||||
|
||||
# First item: custom git URL template
|
||||
printf " ${CYAN}%2d)${NC} ${GREEN}%s${NC}\n" 1 "$(t templates_custom_git)" >&2
|
||||
|
||||
local cats=()
|
||||
local i=1
|
||||
local i=2
|
||||
while IFS='|' read -r id name icon count; do
|
||||
[ "$count" -eq 0 ] && continue
|
||||
local emoji
|
||||
@@ -67,40 +79,52 @@ select_category() {
|
||||
chart-bar) emoji="🔧" ;;
|
||||
*) emoji="📄" ;;
|
||||
esac
|
||||
printf " ${CYAN}%2d)${NC} ${emoji} %-30s ${DIM}(%d шаблонов)${NC}\n" "$i" "$name" "$count" >&2
|
||||
printf " ${CYAN}%2d)${NC} ${emoji} %-30s ${DIM}$(tf templates_count_fmt "$count")${NC}\n" "$i" "$name" >&2
|
||||
cats+=("$id")
|
||||
((i++))
|
||||
done < <(get_categories)
|
||||
|
||||
printf " ${CYAN}%2d)${NC} 🎲 Случайный шаблон\n" "$i" >&2
|
||||
printf " ${CYAN}%2d)${NC} %s\n" "$i" "$(t templates_random)" >&2
|
||||
echo -e " ${DIM}$(printf '─%.0s' {1..55})${NC}" >&2
|
||||
echo -ne " ${WHITE}Выбор:${NC} " >&2
|
||||
echo -ne " ${WHITE}$(t choose):${NC} " >&2
|
||||
read -r choice
|
||||
|
||||
# Случайный
|
||||
if [ "$choice" -eq "$i" ] 2>/dev/null; then
|
||||
if ! [[ "$choice" =~ ^[0-9]+$ ]]; then
|
||||
log_error "$(t invalid_choice)"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Custom git URL
|
||||
if [ "$choice" -eq 1 ]; then
|
||||
echo "__custom_git__"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Random
|
||||
if [ "$choice" -eq "$i" ]; then
|
||||
local random_cat="${cats[$((RANDOM % ${#cats[@]}))]}"
|
||||
echo "$random_cat"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [[ "$choice" =~ ^[0-9]+$ ]] && [ "$choice" -ge 1 ] && [ "$choice" -lt "$i" ]; then
|
||||
echo "${cats[$((choice-1))]}"
|
||||
# Regular category (offset by 1 because item 1 is custom git)
|
||||
if [ "$choice" -ge 2 ] && [ "$choice" -lt "$i" ]; then
|
||||
echo "${cats[$((choice-2))]}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
log_error "Неверный выбор"
|
||||
log_error "$(t invalid_choice)"
|
||||
return 1
|
||||
}
|
||||
|
||||
# ── Интерактивный выбор шаблона ──────────────────────────────────────────────
|
||||
# ── Interactive template picker ────────────────────────────────────────
|
||||
select_template() {
|
||||
local cat_id="$1"
|
||||
local cat_name
|
||||
cat_name=$(get_category_name "$cat_id")
|
||||
|
||||
echo "" >&2
|
||||
echo -e " ${BOLD}${WHITE}📋 $cat_name — доступные шаблоны:${NC}" >&2
|
||||
echo -e " ${BOLD}${WHITE}$(tf templates_list "$cat_name")${NC}" >&2
|
||||
echo -e " ${DIM}$(printf '─%.0s' {1..60})${NC}" >&2
|
||||
|
||||
local tpls=()
|
||||
@@ -112,29 +136,29 @@ select_template() {
|
||||
done < <(get_templates_by_category "$cat_id")
|
||||
|
||||
if [ ${#tpls[@]} -eq 0 ]; then
|
||||
log_info "В этой категории нет шаблонов"
|
||||
log_info "$(t templates_cat_empty)"
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo -e " ${DIM}$(printf '─%.0s' {1..60})${NC}" >&2
|
||||
echo -ne " ${WHITE}Выбор (1-$((i-1))):${NC} " >&2
|
||||
echo -ne " ${WHITE}$(t choose) (1-$((i-1))):${NC} " >&2
|
||||
read -r choice
|
||||
|
||||
if [[ "$choice" =~ ^[0-9]+$ ]] && [ "$choice" -ge 1 ] && [ "$choice" -lt "$i" ]; then
|
||||
local selected_id="${tpls[$((choice-1))]}"
|
||||
|
||||
# Показываем превью
|
||||
show_template_preview "$selected_id"
|
||||
# Show preview
|
||||
show_template_preview "$selected_id" || return 1
|
||||
|
||||
echo "$selected_id"
|
||||
return 0
|
||||
fi
|
||||
|
||||
log_error "Неверный выбор"
|
||||
log_error "$(t invalid_choice)"
|
||||
return 1
|
||||
}
|
||||
|
||||
# ── Показ превью шаблона ────────────────────────────────────────────────────
|
||||
# ── Template preview ───────────────────────────────────────────────────
|
||||
show_template_preview() {
|
||||
local tpl_id="$1"
|
||||
local info
|
||||
@@ -148,36 +172,36 @@ show_template_preview() {
|
||||
description=$(echo "$info" | jq -r '.description // "—"')
|
||||
|
||||
echo "" >&2
|
||||
echo -e " ${BOLD}${WHITE}🔍 Превью шаблона:${NC}" >&2
|
||||
echo -e " ${BOLD}${WHITE}$(t templates_preview_title)${NC}" >&2
|
||||
echo -e " ${DIM}$(printf '─%.0s' {1..55})${NC}" >&2
|
||||
echo -e " ${WHITE}Название:${NC} $name" >&2
|
||||
echo -e " ${WHITE}Источник:${NC} $source" >&2
|
||||
echo -e " ${WHITE}Описание:${NC} $description" >&2
|
||||
echo -e " ${WHITE}$(t templates_name)${NC} $name" >&2
|
||||
echo -e " ${WHITE}$(t templates_source)${NC} $source" >&2
|
||||
echo -e " ${WHITE}$(t templates_description)${NC} $description" >&2
|
||||
|
||||
if [ -n "$preview_url" ]; then
|
||||
echo "" >&2
|
||||
echo -e " ${GREEN}👁 Превью:${NC} ${CYAN}${preview_url}${NC}" >&2
|
||||
echo -e " ${DIM}Откройте ссылку в браузере для просмотра шаблона${NC}" >&2
|
||||
echo -e " ${GREEN}$(t templates_preview)${NC} ${CYAN}${preview_url}${NC}" >&2
|
||||
echo -e " ${DIM}$(t templates_preview_hint)${NC}" >&2
|
||||
fi
|
||||
|
||||
if [ -n "$repo_url" ]; then
|
||||
echo -e " ${DIM}📦 Репо: ${repo_url}${NC}" >&2
|
||||
echo -e " ${DIM}$(t templates_repo) ${repo_url}${NC}" >&2
|
||||
fi
|
||||
|
||||
# Благодарность автору
|
||||
# Thanks
|
||||
echo "" >&2
|
||||
echo -e " ${MAGENTA}💜 Спасибо авторам ${source} за открытый код!${NC}" >&2
|
||||
echo -e " ${MAGENTA}$(tf templates_thanks "$source")${NC}" >&2
|
||||
|
||||
echo -e " ${DIM}$(printf '─%.0s' {1..55})${NC}" >&2
|
||||
echo "" >&2
|
||||
|
||||
if ! confirm "Установить этот шаблон?"; then
|
||||
if ! confirm "$(t templates_install_this)"; then
|
||||
return 1
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
# ── Скачивание шаблона ───────────────────────────────────────────────────────
|
||||
# ── Template download (from catalog) ───────────────────────────────────
|
||||
download_template() {
|
||||
local tpl_id="$1"
|
||||
local output_dir="${2:-$TEMPLATES_CACHE}"
|
||||
@@ -194,9 +218,9 @@ download_template() {
|
||||
rm -rf "$clone_dir"
|
||||
mkdir -p "$clone_dir"
|
||||
|
||||
log_info "Скачивание шаблона \"$name\"..."
|
||||
log_info "$(tf templates_downloading "$name")"
|
||||
|
||||
# Для HTML5 UP — отдельный репо с папками
|
||||
# HTML5 UP — one repo with folders
|
||||
if [ "$source" = "html5up" ]; then
|
||||
local tmp_clone="/tmp/html5up_clone_$$"
|
||||
rm -rf "$tmp_clone"
|
||||
@@ -204,7 +228,7 @@ download_template() {
|
||||
# Sparse checkout
|
||||
git clone --depth 1 --filter=blob:none --sparse "$repo_url" "$tmp_clone" 2>/dev/null
|
||||
if [ $? -ne 0 ]; then
|
||||
# Fallback: полный clone
|
||||
# Fallback: full clone
|
||||
git clone --depth 1 "$repo_url" "$tmp_clone" 2>/dev/null
|
||||
fi
|
||||
|
||||
@@ -217,7 +241,7 @@ download_template() {
|
||||
fi
|
||||
rm -rf "$tmp_clone"
|
||||
|
||||
# Для learning-zone — один большой репо
|
||||
# learning-zone — one big repo
|
||||
elif [ "$source" = "learning-zone" ]; then
|
||||
local tmp_clone="/tmp/lz_clone_$$"
|
||||
rm -rf "$tmp_clone"
|
||||
@@ -236,14 +260,14 @@ download_template() {
|
||||
fi
|
||||
rm -rf "$tmp_clone"
|
||||
|
||||
# Для StartBootstrap — каждый шаблон в своём репо
|
||||
# StartBootstrap — each template in its own repo
|
||||
elif [ "$source" = "startbootstrap" ]; then
|
||||
local sb_tmp="/tmp/sb_clone_$$"
|
||||
rm -rf "$sb_tmp"
|
||||
git clone --depth 1 "$repo_url" "$sb_tmp" 2>/dev/null
|
||||
if [ -d "$sb_tmp" ]; then
|
||||
rm -rf "$sb_tmp/.git"
|
||||
# StartBootstrap хранит production-файлы в dist/
|
||||
# StartBootstrap stores production files in dist/
|
||||
if [ -f "$sb_tmp/dist/index.html" ]; then
|
||||
cp -r "$sb_tmp/dist/"* "$clone_dir/"
|
||||
elif [ -f "$sb_tmp/index.html" ]; then
|
||||
@@ -260,7 +284,7 @@ download_template() {
|
||||
fi
|
||||
rm -rf "$sb_tmp"
|
||||
|
||||
# Для ThemeWagon / ColorlibHQ — каждый шаблон в отдельном репо
|
||||
# ThemeWagon / ColorlibHQ — each template in its own repo
|
||||
elif [ "$source" = "themewagon" ] || [ "$source" = "colorlib" ]; then
|
||||
local tw_tmp="/tmp/tw_clone_$$"
|
||||
rm -rf "$tw_tmp"
|
||||
@@ -283,7 +307,7 @@ download_template() {
|
||||
fi
|
||||
rm -rf "$tw_tmp"
|
||||
|
||||
# Для dawidolko — один большой репо с папками (как learning-zone)
|
||||
# dawidolko — one big repo with folders (similar to learning-zone)
|
||||
elif [ "$source" = "dawidolko" ]; then
|
||||
local tmp_clone="/tmp/dw_clone_$$"
|
||||
rm -rf "$tmp_clone"
|
||||
@@ -301,13 +325,13 @@ download_template() {
|
||||
rm -rf "$tmp_clone"
|
||||
fi
|
||||
|
||||
# Проверяем результат
|
||||
# Check result
|
||||
if [ -f "$clone_dir/index.html" ]; then
|
||||
log_success "Шаблон \"$name\" скачан"
|
||||
log_success "$(tf templates_downloaded "$name")"
|
||||
echo "$clone_dir"
|
||||
return 0
|
||||
else
|
||||
# fallback: ищем index.html в подпапках (нестандартная структура)
|
||||
# fallback: find index.html in subfolders (non-standard structure)
|
||||
local fallback_index
|
||||
fallback_index=$(find "$clone_dir" -name "index.html" -type f 2>/dev/null | head -1)
|
||||
if [ -n "$fallback_index" ]; then
|
||||
@@ -315,33 +339,253 @@ download_template() {
|
||||
fallback_dir=$(dirname "$fallback_index")
|
||||
if [ "$fallback_dir" != "$clone_dir" ]; then
|
||||
cp -r "$fallback_dir/"* "$clone_dir/"
|
||||
log_success "Шаблон \"$name\" скачан (из подпапки)"
|
||||
log_success "$(tf templates_downloaded_subfolder "$name")"
|
||||
echo "$clone_dir"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
log_error "Шаблон не содержит index.html"
|
||||
log_dim "Путь: $clone_dir"
|
||||
log_error "$(t templates_no_index)"
|
||||
log_dim "$(tf templates_path "$clone_dir")"
|
||||
ls -la "$clone_dir" 2>/dev/null >&2
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# ── Полный интерактивный процесс выбора ──────────────────────────────────────
|
||||
# ── Custom git URL helpers ─────────────────────────────────────────────
|
||||
|
||||
# Validate a user-supplied git URL
|
||||
# Accepts: https://host/path[.git][@branch]
|
||||
# Rejects: ssh://, git://, file://, absolute file paths
|
||||
_validate_custom_git_url() {
|
||||
local url="$1"
|
||||
# Must begin with https://
|
||||
[[ "$url" =~ ^https:// ]] || return 1
|
||||
# Reject shell metacharacters that could be exploited
|
||||
[[ "$url" =~ [[:space:]\;\`\$\(\)\<\>\|\\\&] ]] && return 1
|
||||
# Reasonable length limit
|
||||
[ "${#url}" -gt 512 ] && return 1
|
||||
return 0
|
||||
}
|
||||
|
||||
# Parse URL → sets CUSTOM_GIT_CLEAN and CUSTOM_GIT_BRANCH globals
|
||||
_parse_custom_git_url() {
|
||||
local url="$1"
|
||||
CUSTOM_GIT_CLEAN=""
|
||||
CUSTOM_GIT_BRANCH=""
|
||||
# Handle trailing @branch
|
||||
if [[ "$url" =~ ^(https://[^@]+)@([A-Za-z0-9._/-]+)$ ]]; then
|
||||
CUSTOM_GIT_CLEAN="${BASH_REMATCH[1]}"
|
||||
CUSTOM_GIT_BRANCH="${BASH_REMATCH[2]}"
|
||||
else
|
||||
CUSTOM_GIT_CLEAN="$url"
|
||||
fi
|
||||
# Strip trailing slash
|
||||
CUSTOM_GIT_CLEAN="${CUSTOM_GIT_CLEAN%/}"
|
||||
# Append .git if missing (works better with git clone on some hosts)
|
||||
if [[ ! "$CUSTOM_GIT_CLEAN" =~ \.git$ ]]; then
|
||||
CUSTOM_GIT_CLEAN="${CUSTOM_GIT_CLEAN}.git"
|
||||
fi
|
||||
}
|
||||
|
||||
# Check repo size (in MB) by inspecting cloned directory
|
||||
_clone_dir_size_mb() {
|
||||
local dir="$1"
|
||||
du -sm "$dir" 2>/dev/null | awk '{print $1}'
|
||||
}
|
||||
|
||||
# ── Show detailed help for custom git template ─────────────────────────
|
||||
show_custom_git_help() {
|
||||
local line
|
||||
line=$(printf '─%.0s' $(seq 1 60))
|
||||
echo "" >&2
|
||||
echo -e " ${BOLD}${GREEN}$(t custom_git_title)${NC}" >&2
|
||||
echo -e " ${DIM}${line}${NC}" >&2
|
||||
echo -e " $(t custom_git_help_1)" >&2
|
||||
echo -e " $(t custom_git_help_2)" >&2
|
||||
echo -e " $(t custom_git_help_3)" >&2
|
||||
echo "" >&2
|
||||
echo -e " ${BOLD}${WHITE}$(t custom_git_formats)${NC}" >&2
|
||||
echo -e " ${CYAN}$(t custom_git_fmt_github)${NC}" >&2
|
||||
echo -e " ${CYAN}$(t custom_git_fmt_gitlab)${NC}" >&2
|
||||
echo -e " ${CYAN}$(t custom_git_fmt_gitext)${NC}" >&2
|
||||
echo -e " ${CYAN}$(t custom_git_fmt_branch)${NC}" >&2
|
||||
echo "" >&2
|
||||
echo -e " ${BOLD}${WHITE}$(t custom_git_auto_detect)${NC}" >&2
|
||||
echo -e " $(t custom_git_auto_1)" >&2
|
||||
echo -e " $(t custom_git_auto_2)" >&2
|
||||
echo -e " $(t custom_git_auto_3)" >&2
|
||||
echo -e " $(t custom_git_auto_4)" >&2
|
||||
echo "" >&2
|
||||
echo -e " ${BOLD}${WHITE}$(t custom_git_requirements)${NC}" >&2
|
||||
echo -e " ${YELLOW}$(t custom_git_req_1)${NC}" >&2
|
||||
echo -e " ${YELLOW}$(t custom_git_req_2)${NC}" >&2
|
||||
echo -e " ${YELLOW}$(t custom_git_req_3)${NC}" >&2
|
||||
echo -e " ${YELLOW}$(t custom_git_req_4)${NC}" >&2
|
||||
echo "" >&2
|
||||
echo -e " ${BOLD}${WHITE}$(t custom_git_examples)${NC}" >&2
|
||||
echo -e " ${DIM}$(t custom_git_ex_1)${NC}" >&2
|
||||
echo -e " ${DIM}$(t custom_git_ex_2)${NC}" >&2
|
||||
echo -e " ${DIM}${line}${NC}" >&2
|
||||
echo "" >&2
|
||||
}
|
||||
|
||||
# ── Download a custom git template ─────────────────────────────────────
|
||||
# Prompts user for a URL (unless passed), clones, detects index.html,
|
||||
# copies result into $output_dir/custom_<hash>, echoes the final path.
|
||||
download_custom_git_template() {
|
||||
local url="${1:-}"
|
||||
local output_dir="${2:-$TEMPLATES_CACHE}"
|
||||
|
||||
show_custom_git_help
|
||||
|
||||
if [ -z "$url" ]; then
|
||||
echo -ne " ${WHITE}$(t custom_git_enter_url)${NC} " >&2
|
||||
read -r url
|
||||
url=$(echo "$url" | tr -d '\r\n[:space:]')
|
||||
fi
|
||||
|
||||
if [ -z "$url" ]; then
|
||||
log_error "$(t custom_git_empty)"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! _validate_custom_git_url "$url"; then
|
||||
log_error "$(t custom_git_bad_url)"
|
||||
return 1
|
||||
fi
|
||||
|
||||
_parse_custom_git_url "$url"
|
||||
local clean_url="$CUSTOM_GIT_CLEAN"
|
||||
local branch="$CUSTOM_GIT_BRANCH"
|
||||
|
||||
# Stable-ish directory name from a hash of the original URL
|
||||
local hash
|
||||
hash=$(echo -n "$url" | md5sum 2>/dev/null | awk '{print $1}' | head -c 10)
|
||||
[ -z "$hash" ] && hash=$(date +%s)
|
||||
local tpl_id="custom_${hash}"
|
||||
local clone_dir="$output_dir/${tpl_id}"
|
||||
local tmp_clone="/tmp/custom_git_clone_$$"
|
||||
|
||||
rm -rf "$clone_dir" "$tmp_clone"
|
||||
mkdir -p "$clone_dir"
|
||||
|
||||
log_info "$(t custom_git_cloning)"
|
||||
|
||||
# Clone with timeout so a hung server can't freeze the installer
|
||||
local clone_status=0
|
||||
local git_args=("clone" "--depth" "1")
|
||||
[ -n "$branch" ] && git_args+=("--branch" "$branch")
|
||||
git_args+=("$clean_url" "$tmp_clone")
|
||||
|
||||
if command -v timeout &>/dev/null; then
|
||||
timeout "$CUSTOM_GIT_CLONE_TIMEOUT" git "${git_args[@]}" 2>/tmp/custom_git_err_$$
|
||||
clone_status=$?
|
||||
else
|
||||
git "${git_args[@]}" 2>/tmp/custom_git_err_$$
|
||||
clone_status=$?
|
||||
fi
|
||||
|
||||
if [ $clone_status -ne 0 ] || [ ! -d "$tmp_clone" ]; then
|
||||
local err_msg
|
||||
err_msg=$(head -3 "/tmp/custom_git_err_$$" 2>/dev/null | tr '\n' ' ')
|
||||
rm -f "/tmp/custom_git_err_$$"
|
||||
rm -rf "$tmp_clone" "$clone_dir"
|
||||
log_error "$(tf custom_git_clone_failed "${err_msg:-$clone_status}")"
|
||||
return 1
|
||||
fi
|
||||
rm -f "/tmp/custom_git_err_$$"
|
||||
|
||||
# Drop .git before measuring size (we only care about payload)
|
||||
rm -rf "$tmp_clone/.git"
|
||||
|
||||
# Size guard
|
||||
local size_mb
|
||||
size_mb=$(_clone_dir_size_mb "$tmp_clone")
|
||||
if [ -n "$size_mb" ] && [ "$size_mb" -gt "$CUSTOM_GIT_MAX_SIZE_MB" ]; then
|
||||
rm -rf "$tmp_clone" "$clone_dir"
|
||||
log_error "$(tf custom_git_too_big "${size_mb}MB")"
|
||||
return 1
|
||||
fi
|
||||
|
||||
log_info "$(t custom_git_scanning)"
|
||||
|
||||
# Priority list of common static-site output folders
|
||||
local candidates=("" "dist" "public" "build" "_site" "site" "docs" "out" "www")
|
||||
local found_dir=""
|
||||
for sub in "${candidates[@]}"; do
|
||||
local try_dir="$tmp_clone"
|
||||
[ -n "$sub" ] && try_dir="$tmp_clone/$sub"
|
||||
if [ -f "$try_dir/index.html" ]; then
|
||||
found_dir="$try_dir"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
# Fallback: search for any index.html in the repo (shallow depth first)
|
||||
if [ -z "$found_dir" ]; then
|
||||
local fallback_index
|
||||
fallback_index=$(find "$tmp_clone" -maxdepth 4 -name "index.html" -type f 2>/dev/null | head -1)
|
||||
if [ -n "$fallback_index" ]; then
|
||||
found_dir=$(dirname "$fallback_index")
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -z "$found_dir" ] || [ ! -f "$found_dir/index.html" ]; then
|
||||
rm -rf "$tmp_clone" "$clone_dir"
|
||||
log_error "$(t custom_git_no_index)"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Show what we found (human-friendly relative path)
|
||||
local rel_path="${found_dir#$tmp_clone}"
|
||||
rel_path="${rel_path#/}"
|
||||
[ -z "$rel_path" ] && rel_path="(root)"
|
||||
log_dim "$(tf custom_git_found_at "$rel_path")"
|
||||
|
||||
# Copy the detected directory as the new template
|
||||
cp -r "$found_dir"/* "$clone_dir/" 2>/dev/null
|
||||
cp -r "$found_dir"/.[!.]* "$clone_dir/" 2>/dev/null
|
||||
|
||||
rm -rf "$tmp_clone"
|
||||
|
||||
if [ ! -f "$clone_dir/index.html" ]; then
|
||||
rm -rf "$clone_dir"
|
||||
log_error "$(t custom_git_no_index)"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Remember the URL so users can see what template they used
|
||||
echo "$url" > "$clone_dir/.custom_git_source" 2>/dev/null
|
||||
|
||||
log_success "$(tf custom_git_installed "$url")"
|
||||
echo "$clone_dir"
|
||||
return 0
|
||||
}
|
||||
|
||||
# ── Full interactive template selection ───────────────────────────────
|
||||
interactive_template_selection() {
|
||||
load_catalog || return 1
|
||||
|
||||
# Выбор категории
|
||||
# Category selection
|
||||
local cat_id
|
||||
cat_id=$(select_category)
|
||||
[ $? -ne 0 ] && return 1
|
||||
|
||||
# Выбор шаблона
|
||||
# Custom git URL path
|
||||
if [ "$cat_id" = "__custom_git__" ]; then
|
||||
local template_dir
|
||||
template_dir=$(download_custom_git_template)
|
||||
[ $? -ne 0 ] && return 1
|
||||
echo "$template_dir"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Template selection
|
||||
local tpl_id
|
||||
tpl_id=$(select_template "$cat_id")
|
||||
[ $? -ne 0 ] && return 1
|
||||
|
||||
# Скачивание
|
||||
# Download
|
||||
local template_dir
|
||||
template_dir=$(download_template "$tpl_id")
|
||||
[ $? -ne 0 ] && return 1
|
||||
|
||||
@@ -9,9 +9,9 @@ install_nginx() {
|
||||
fi
|
||||
log_info "Установка nginx..."
|
||||
case "$(get_pkg_manager)" in
|
||||
apt) apt-get update -qq && apt-get install -y -qq nginx ;;
|
||||
dnf) dnf install -y -q nginx ;;
|
||||
yum) yum install -y -q nginx ;;
|
||||
apt) apt_update && apt_install nginx || return 1 ;;
|
||||
dnf) dnf install -y -q nginx || return 1 ;;
|
||||
yum) yum install -y -q nginx || return 1 ;;
|
||||
esac
|
||||
systemctl enable nginx 2>/dev/null
|
||||
}
|
||||
@@ -24,9 +24,9 @@ install_certbot() {
|
||||
fi
|
||||
log_info "Установка certbot..."
|
||||
case "$(get_pkg_manager)" in
|
||||
apt) apt-get install -y -qq certbot python3-certbot-nginx ;;
|
||||
dnf) dnf install -y -q certbot python3-certbot-nginx ;;
|
||||
yum) yum install -y -q certbot python3-certbot-nginx ;;
|
||||
apt) apt_install certbot python3-certbot-nginx || return 1 ;;
|
||||
dnf) dnf install -y -q certbot python3-certbot-nginx || return 1 ;;
|
||||
yum) yum install -y -q certbot python3-certbot-nginx || return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user