67 Commits

Author SHA1 Message Date
Виталий Литвинов
5714714d3c Add repository hygiene ignore rules 2026-04-25 22:59:41 +03:00
Виталий Литвинов
6840d309af Fix Telegram bot template callbacks and telemt version checks 2026-04-25 19:18:54 +03:00
Виталий Литвинов
2f3607e1e6 v2.5.0: harden telemt user reloads 2026-04-25 17:50:06 +03:00
Виталий Литвинов
5a6bc9f614 v2.5.0: read quoted main proxy secret 2026-04-25 16:45:57 +03:00
Виталий Литвинов
cae552ec87 v2.5.0: add admin auto refresh toggle 2026-04-25 16:38:19 +03:00
Виталий Литвинов
98e4be8831 v2.5.0: use live IP counts in key cards 2026-04-25 16:22:17 +03:00
Виталий Литвинов
d62991857f v2.5.0: replace keys table with responsive cards 2026-04-25 15:52:17 +03:00
Виталий Литвинов
7b1a79bba9 v2.5.0: align key table buttons 2026-04-25 15:44:36 +03:00
Виталий Литвинов
b58bc9e1ba v2.5.0: make key stats selection explicit 2026-04-25 15:35:13 +03:00
Виталий Литвинов
1e27d6ba71 v2.5.0: make key table responsive 2026-04-25 15:27:58 +03:00
Виталий Литвинов
a143c01a9a v2.5.0: keep admin scroll clear 2026-04-25 15:24:34 +03:00
Виталий Литвинов
507a2979e5 v2.5.0: align key controls and add IP limits 2026-04-25 15:19:28 +03:00
Виталий Литвинов
bd3fc1af18 v2.5.0: select key traffic by row click 2026-04-25 14:56:52 +03:00
Виталий Литвинов
817ea9ab9f v2.5.0: clarify encrypted restore in admin 2026-04-25 14:51:10 +03:00
Виталий Литвинов
d492e5eb69 v2.5.0: make backup names collision safe 2026-04-25 14:47:34 +03:00
Виталий Литвинов
b2ab0dca57 v2.5.0: add QR import and backup scheduling 2026-04-25 14:39:56 +03:00
Виталий Литвинов
c7540a97f7 v2.5.0: compact traffic history retention 2026-04-25 14:15:28 +03:00
Виталий Литвинов
63b564f70f v2.5.0: add shared 443 and per-user traffic 2026-04-25 14:07:47 +03:00
Виталий Литвинов
c1b5ffc5a7 v2.5.0: clarify 443 route map assets 2026-04-25 13:29:05 +03:00
Виталий Литвинов
7eaeef8b49 v2.5.0: show routed services behind port 443 2026-04-25 13:25:32 +03:00
Виталий Литвинов
5225811b3c v2.5.0: harden admin key and traffic flows 2026-04-25 12:28:33 +03:00
Виталий Литвинов
d74b05ccf8 v2.5.0: refine admin traffic and port status 2026-04-25 12:01:31 +03:00
Виталий Литвинов
d8ec62eb07 v2.5.0: update service branding 2026-04-25 09:58:23 +03:00
Виталий Литвинов
e54778c08c v2.5.0: add key disable switches and pro UI polish 2026-04-25 09:44:53 +03:00
Виталий Литвинов
6b89c3ea81 v2.5.0: redact admin overview secret 2026-04-24 23:08:02 +03:00
Виталий Литвинов
a393bd79ff v2.5.0: limit language api response 2026-04-24 23:03:06 +03:00
Виталий Литвинов
3b075a7ed7 v2.5.0: improve admin domain language logs 2026-04-24 22:59:21 +03:00
Виталий Литвинов
9c74a0d00f v2.5.0: guard web admin log loading race 2026-04-24 22:44:47 +03:00
Виталий Литвинов
3ab0f9d5c7 v2.5.0: bust web admin asset cache 2026-04-24 22:40:31 +03:00
Виталий Литвинов
dc7452930a v2.5.0: disable web admin static cache 2026-04-24 22:37:52 +03:00
Виталий Литвинов
d9e4831e44 v2.5.0: redesign local admin and repair stats 2026-04-24 22:30:09 +03:00
Виталий Литвинов
008143a617 v2.5.0: remove local web admin token gate 2026-04-24 22:00:08 +03:00
Виталий Литвинов
8804319e19 v2.5.0: wait for telemt readiness in web admin 2026-04-24 21:41:21 +03:00
Виталий Литвинов
fe6d91c3a5 v2.5.0: wait for telemt listener readiness 2026-04-24 21:39:02 +03:00
Виталий Литвинов
20103ccac8 v2.5.0: add local web admin dashboard 2026-04-24 19:19:12 +03:00
Виталий Литвинов
ed9073f28f v2.5.0: add legacy state migration 2026-04-24 18:58:52 +03:00
Codex
7afeb59261 v2.5.0: maintenance and bot user management 2026-04-24 18:50:43 +03:00
anten-ka
b10ea54ce9 install.sh: show promo QR only for tips, remove hoster QR codes 2026-04-10 20:48:35 +03:00
anten-ka
2f3045bcc0 bootstrap.sh: rotate PAT + support GOTELEGRAM_PAT env override 2026-04-10 20:44:20 +03:00
anten-ka
eb5175ccab v2.4.6: universal apt_lock_wait helper (fix nginx install on fresh VPS)
- lib/common.sh: add apt_lock_wait + apt_install + apt_update helpers
  * waits up to 300s for dpkg lock held by unattended-upgrades
  * uses DPkg::Lock::Timeout=120 native flag
  * captures stderr to show real error if install fails
- lib/website.sh: install_nginx/install_certbot use apt_install
  (fixes "Could not get lock /var/lib/dpkg/lock-frontend" during Pro setup)
- install.sh: bot_install uses apt_install (removes duplicated lock logic)
- lib/common.sh: ensure_deps uses apt_update + apt_install
2026-04-10 20:01:20 +03:00
anten-ka
3403975636 fix(v2.4.5): wait for apt lock + versioned python3.X-venv fallback
- bot_install: wait up to 300s for dpkg/apt lock (unattended-upgrades)
- use apt-get -o DPkg::Lock::Timeout=120 as extra safety
- detect python3 minor version and include python3.X-venv in package list
- stop silencing apt errors with 2>/dev/null — surface them to user
- python3-full is optional; fall back to core packages if unavailable
- venv fallback path also installs versioned package and retries
- templates_catalog.json: skip cp when source==dest (symlink install)
2026-04-10 15:59:46 +03:00
anten-ka
3919f201f5 fix(v2.4.4): robust venv creation + UTF-8 safe install frame
- install.sh bot_install: always ensure python3-venv+pip, verify pip exists, check pip install exit code, sanity-check imports
- install.sh: replace box-frame with simple bullet lines (printf %-Ns was byte-counting, breaking Cyrillic UTF-8)
- common.sh: 2.4.4
- bot.py: 2.4.4
2026-04-10 14:03:28 +03:00
anten-ka
4b63b79184 docs(v2.4.3): changelog + bot action bridge + flock race fix
- Header version bumped 2.4.1 -> 2.4.3 in both docs
- DOCS_HUMAN.md changelog: 2.4.2 (bot can change template/domain) + 2.4.3 (flock race fix)
- DOCS_AI.md section 11.1 (new): non-interactive action bridge contract - action list, JSON format, error codes, flock serialization semantics
- DOCS_AI.md section 10: bugs #24-28 (safe_edit_message TypeError, template_id field name, jq 1.5 compat, validation, flock race)
- DOCS_AI.md changelog: 2.4.2 + 2.4.3 entries
2026-04-10 13:39:10 +03:00
anten-ka
e9af6e969f fix(v2.4.3): flock serialization for bot_action_dispatch
Iteration 3 discovered a race condition: parallel CLI invocations of
change-lite-domain occasionally produced 'no secret in config' because
one process read config.json while another was mid-rewrite (jq -> tmp -> mv).
asyncio.Lock in bot.py only protects bot->bot races; CLI-level races and
bot+CLI mixed races were still possible.

- install.sh: bot_action_dispatch wraps dispatch in flock(1) on
  /var/lock/gotelegram-bot-action.lock, 30s timeout, EX_TEMPFAIL on timeout
- common.sh: flock added to critical deps; apt_pkg_for_cmd/dnf_pkg_for_cmd
  map flock -> util-linux
- common.sh: check_deps_present includes flock
- version bumped to 2.4.3 (common.sh + bot.py)
2026-04-10 13:35:22 +03:00
anten-ka
724eeb92d9 fix(v2.4.2): iter2 audit fixes
- bot.py: safe_edit_message now accepts disable_web_page_preview (CRIT: was TypeError in cb_pro_confirm success path)
- bot.py: status display uses template_id field (was 'template' — mismatch with save_gotelegram_config, template never showed)
- bot.py: cb_pro_confirm validates tpl_id against [A-Za-z0-9_-]{1,64} before subprocess (defense-in-depth)
- bot.py: cb_lite_domain validates domain shape
- bot.py: asyncio.Lock _BOT_ACTION_LOCK serializes concurrent change-template/change-lite-domain calls
- install.sh: bot_update_config_field uses shell `date -Iseconds` instead of jq's `now|todate` (jq 1.5 compat for Debian 10)
2026-04-10 13:30:47 +03:00
anten-ka
fc28a1a099 feat(v2.4.2): bot non-interactive install.sh actions
- install.sh: new bot_action_dispatch entry point for --action=X --json CLI invocation from the bot/scripts
- install.sh: bot_action_change_template — reuses download_template + deploy_template_to_nginx, updates config.json template_id
- install.sh: bot_action_change_lite_domain — regenerates telemt TOML with new fake-TLS mask domain, restarts telemt
- bot.py: run_bot_action() subprocess wrapper parses JSON result
- bot.py: cb_pro_confirm now performs real change-template when already in pro mode (fresh install still routes to CLI)
- bot.py: cb_lite_domain now performs real change-lite-domain when already in lite mode
- version -> 2.4.2
2026-04-10 13:19:26 +03:00
anten-ka
7b53566dad fix(bot): promo auto-delete + neutralize fake install/change stubs
Two user-reported bugs:

1. Promo spam: daily promo message from /start and the Promo menu
   button stayed in chat forever. Now they auto-delete after 30s.
   - new helper _delete_message_after()
   - cmd_start schedules deletion of the daily promo
   - cb_menu_promo sends promo as a SEPARATE ephemeral message
     (instead of editing the main menu in place), so the menu stays
     intact and only the promo self-destructs in 30s.

2. CRITICAL: 'install/change template from bot' was a stub that
   silently corrupted /opt/gotelegram/config.json. cb_lite_domain
   and cb_pro_confirm wrote a fake minimal config ({mode,template,
   port,installed_at}) without secret/domain/mask_host, and showed
   '[OK] installed!' — while never invoking install.sh, never
   downloading the template, never touching nginx. Proxy link
   generation then broke because secret was gone.
   User symptom: 'устанавливал другой шаблон через бота и всё
   повисло'.

   Fix: both callbacks now refuse cleanly and route the user to the
   CLI ('gotelegram' command → menu 1 → 1 or 7). Configuration is
   NOT touched. Full non-interactive install/change from the bot is
   left as future work.

Tested live on VPS:
- bot.py syntax OK (ast.parse on VPS)
- gotelegram-bot restarted, active
- corrupted config.json on VPS rebuilt from telemt TOML ground
  truth (mode=lite, secret, port, mask_host=google.com)
2026-04-10 13:00:04 +03:00
anten-ka
6b206a1697 fix(stats): add lib/stats.sh to bootstrap + auto-recreate history
Bug report: 'статистика не работает' — submenu_stats showed the
'stats module missing' fallback even though gotelegram-stats.service
was active. Root cause: bootstrap.sh FILES[] never included
lib/stats.sh, so on every (re)install the file was absent from
/opt/gotelegram/lib/ on disk. The running collector was sourcing it
from an in-memory bash process from a previous install, but
install.sh's `source stats.sh` at menu entry failed silently and
hit the 'stats_module_missing' branch.

Also: stats_collect silently skipped history writes if
stats_history.csv was deleted post-init (only stats_init created it
once). Now stats_collect auto-recreates the file with header.

- bootstrap.sh: add 'lib/stats.sh' to FILES[]
- lib/stats.sh: stats_collect auto-creates HISTORY_FILE if missing

Tested live on VPS: show_traffic_stats now outputs real counters
(23.1 MB proxy, 10 MB site, 394 KB/s peak).
2026-04-10 12:44:35 +03:00
anten-ka
6dc0000013 v2.4.1: full docs (DOCS_HUMAN.md + DOCS_AI.md)
- DOCS_HUMAN.md: user-facing guide (quick start, lite/pro, menu, bot,
  backup, requirements, FAQ, file locations, changelog)
- DOCS_AI.md: deep technical reference for AI agents (architecture,
  telemt v3 TOML format, subshell capture rules, bug history incl.
  start_telemt restart fix, github push workflow, VPS test workflow)
- lib/common.sh: bump GOTELEGRAM_VERSION 2.4.0 -> 2.4.1
2026-04-10 12:36:24 +03:00
anten-ka
663a5a2aae fix(install): restart telemt if already running so new config is applied
start_telemt() was a no-op when the service was already active, so
regenerating /etc/telemt/config.toml (e.g. reinstalling with a different
mask domain or switching lite<->pro) left the daemon running with the
OLD in-memory config. This caused the reported "lite-mode key does not
work" issue: after a fresh lite install over an existing pro install,
telemt kept tls_domain=anten-ka.com and dropped SNI=google.com clients
with unknown_sni_action=Drop even though /etc/telemt/config.toml said
tls_domain=google.com.

Fix: start_telemt now uses systemctl restart when the service is
already active, guaranteeing the fresh config is loaded.
2026-04-10 12:16:45 +03:00
anten-ka
32f204c871 v2.4.0 — internationalization (EN/RU) + custom git templates
- i18n engine (lib/i18n.sh, lib/lang/en.sh, lib/lang/ru.sh)
- first-run language picker, persisted to .language + config.json
- install.sh, common.sh, backup.sh, templates_catalog.sh wired through t()/tf()
- backup.sh preserves .language marker and records language in metadata.json
- custom git template feature (first item in pro template picker)
  * validates HTTPS URLs, rejects shell metachars
  * 100MB size guard, 90s clone timeout
  * auto-detects index.html in dist/public/build/_site/site/docs/out/www
- bot v2.4.0: i18n.py + lang/{en,ru}.json, /lang command, language toggle button
- bot: custom git template via text input with waiter gating
2026-04-10 11:48:37 +03:00
anten-ka
3495ab5b0f v2.4.0 — internationalization (EN/RU) + custom git templates
- i18n engine (lib/i18n.sh, lib/lang/en.sh, lib/lang/ru.sh)
- first-run language picker, persisted to .language + config.json
- install.sh, common.sh, backup.sh, templates_catalog.sh wired through t()/tf()
- backup.sh preserves .language marker and records language in metadata.json
- custom git template feature (first item in pro template picker)
  * validates HTTPS URLs, rejects shell metachars
  * 100MB size guard, 90s clone timeout
  * auto-detects index.html in dist/public/build/_site/site/docs/out/www
- bot v2.4.0: i18n.py + lang/{en,ru}.json, /lang command, language toggle button
- bot: custom git template via text input with waiter gating
2026-04-10 11:41:23 +03:00
anten-ka
194fb32fec v2.4.0 — internationalization (EN/RU) + custom git templates
- i18n engine (lib/i18n.sh, lib/lang/en.sh, lib/lang/ru.sh)
- first-run language picker, persisted to .language + config.json
- install.sh, common.sh, backup.sh, templates_catalog.sh wired through t()/tf()
- backup.sh preserves .language marker and records language in metadata.json
- custom git template feature (first item in pro template picker)
  * validates HTTPS URLs, rejects shell metachars
  * 100MB size guard, 90s clone timeout
  * auto-detects index.html in dist/public/build/_site/site/docs/out/www
- bot v2.4.0: i18n.py + lang/{en,ru}.json, /lang command, language toggle button
- bot: custom git template via text input with waiter gating
2026-04-10 11:37:28 +03:00
anten-ka
45e5cbabea v2.4.0 — internationalization (EN/RU) + custom git templates
- i18n engine (lib/i18n.sh, lib/lang/en.sh, lib/lang/ru.sh)
- first-run language picker, persisted to .language + config.json
- install.sh, common.sh, backup.sh, templates_catalog.sh wired through t()/tf()
- backup.sh preserves .language marker and records language in metadata.json
- custom git template feature (first item in pro template picker)
  * validates HTTPS URLs, rejects shell metachars
  * 100MB size guard, 90s clone timeout
  * auto-detects index.html in dist/public/build/_site/site/docs/out/www
- bot v2.4.0: i18n.py + lang/{en,ru}.json, /lang command, language toggle button
- bot: custom git template via text input with waiter gating
2026-04-10 11:29:23 +03:00
anten-ka
694f18c22f v2.4.0 — internationalization (EN/RU) + custom git templates
- i18n engine (lib/i18n.sh, lib/lang/en.sh, lib/lang/ru.sh)
- first-run language picker, persisted to .language + config.json
- install.sh, common.sh, backup.sh, templates_catalog.sh wired through t()/tf()
- backup.sh preserves .language marker and records language in metadata.json
- custom git template feature (first item in pro template picker)
  * validates HTTPS URLs, rejects shell metachars
  * 100MB size guard, 90s clone timeout
  * auto-detects index.html in dist/public/build/_site/site/docs/out/www
- bot v2.4.0: i18n.py + lang/{en,ru}.json, /lang command, language toggle button
- bot: custom git template via text input with waiter gating
2026-04-10 11:27:39 +03:00
anten-ka
0d087831d8 v2.4.0 — internationalization (EN/RU) + custom git templates
- i18n engine (lib/i18n.sh, lib/lang/en.sh, lib/lang/ru.sh)
- first-run language picker, persisted to .language + config.json
- install.sh, common.sh, backup.sh, templates_catalog.sh wired through t()/tf()
- backup.sh preserves .language marker and records language in metadata.json
- custom git template feature (first item in pro template picker)
  * validates HTTPS URLs, rejects shell metachars
  * 100MB size guard, 90s clone timeout
  * auto-detects index.html in dist/public/build/_site/site/docs/out/www
- bot v2.4.0: i18n.py + lang/{en,ru}.json, /lang command, language toggle button
- bot: custom git template via text input with waiter gating
2026-04-10 11:26:02 +03:00
anten-ka
9c084f37ec bugfix: {{NC}} typo, bot TOML v3 parsing, add_secret v3 format
- install.sh: fix {{NC}} -> ${NC} color escape on line 265
- bot.py: fix TOML parsing for telemt v3 [access.users] format
- bot.py: fix telemt config section [server].port instead of [config].listen_port
- telemt_config.sh: fix add_secret_to_config() for v3 format
2026-04-10 00:16:30 +03:00
anten-ka
63b2fc3717 fix: banner + credits frame alignment (common.sh) 2026-04-09 19:28:46 +03:00
anten-ka
fedc8f77fe fix: frame alignment, ee-prefix for lite mode, qrencode in deps
- All frame boxes: correct 54-char width with emoji compensation
- Lite mode links now include ee-prefix + mask_host hex (fake-TLS)
- Added qrencode to ensure_deps() so QR codes work after fresh install
- Centralized link generation in generate_proxy_link()
- Fixed bot.py get_proxy_link() for lite mode ee-prefix
2026-04-09 19:24:05 +03:00
anten-ka
d70e046035 add bootstrap.sh installer 2026-04-09 11:15:43 +03:00
anten-ka
c70cb36a2b GoTelegram v2.3.1: full push
- MTProxy manager with Telegram bot
- 1801 site templates, 18 categories
- Stealth mode (fake-TLS + nginx mask)
- Admin auto-registration in bot
- Fixed frame alignment, bot venv setup
2026-04-09 11:14:41 +03:00
anten-ka
b36fb5cf10 v2.3.1: fix frame alignment + bot venv setup 2026-04-09 11:04:23 +03:00
anten-ka
8b4b4892a4 v2.3.1: bot install with spinner waiting for first admin + auto/manual choice 2026-04-09 10:54:06 +03:00
anten-ka
046a08fdb6 v2.3.1: admin management (auto-register first admin, /addadmin, /deladmin, menu button) 2026-04-09 10:46:50 +03:00
anten-ka
a21d2ebea2 v2.3.1: fix QR top margin, use domain instead of IP for pro-mode links 2026-04-09 00:59:42 +03:00
anten-ka
96cbd243d9 v2.3.1: promo only on gotelegram launch (once/day), not after proxy setup; bot promo with 2 hosters 2026-04-08 23:08:04 +03:00
anten-ka
3f136ec8a0 v2.3.1: promo update (2 hosters, QR codes, once-per-day display, 5sec delay) 2026-04-08 22:42:29 +03:00
30 changed files with 29834 additions and 17138 deletions

51
.gitignore vendored Normal file
View File

@@ -0,0 +1,51 @@
# Local secrets and environment
.env
.env.*
!.env.example
*.pem
*.key
*.p12
id_rsa*
known_hosts
# Python/runtime cache
__pycache__/
*.py[cod]
.pytest_cache/
.mypy_cache/
.ruff_cache/
.coverage
htmlcov/
venv/
.venv/
# Node/runtime cache
node_modules/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Logs, temporary files, generated backups
*.log
*.tmp
*.bak
*.old
*~
backup/
backups/
dumps/
*.tar
*.tar.gz
*.zip
# OS/editor noise
.DS_Store
Thumbs.db
.idea/
.vscode/
# goTelegram local/generated state
gotelegram_state/
user_stats_history.csv
disabled_users.json
backup_schedule.json

726
DOCS_AI.md Normal file
View File

@@ -0,0 +1,726 @@
# goTelegram Pro — техническая документация для ИИ-агентов
**Версия:** 2.5.0
**Репозиторий:** `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 Pro (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 Pro | `/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).
- `[access.user_max_unique_ips]` — опциональная таблица `name = число`, ограничивает одновременные уникальные IP на ключ; в goTelegram Pro значение `0` в UI удаляет строку и означает безлимит.
### Почему `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 Pro 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`
- `/opt/gotelegram/disabled_users.json`
- `/var/www/gotelegram-site/` (если есть)
- `/opt/gotelegram/custom_templates/`, `/opt/gotelegram/templates_catalog.json`
- `/opt/gotelegram/stats_history.csv`, `/opt/gotelegram/user_stats_history.csv`, `/opt/gotelegram/shared-443.json`
- `/opt/gotelegram-bot/.env`, bot i18n/lang
- `/opt/gotelegram-admin/server.py`, `/opt/gotelegram-admin/static/`
- `/etc/letsencrypt/live/<domain>/` + `/etc/letsencrypt/archive/<domain>/` + `/etc/letsencrypt/renewal/<domain>.conf` (если Pro)
- `/etc/nginx/sites-available/gotelegram` (если есть)
Складывает в `/opt/gotelegram/backups/gotelegram_backup_YYYYMMDD_HHMMSS.tar.gz`.
`restore_backup <file> [password] [yes]` разворачивает архив обратно, перезапускает telemt, nginx, bot и admin. Третий аргумент `yes` нужен для неинтерактивного restore из web-admin/бота; перед ним они создают свежий safety backup. Restore понимает legacy-архивы раннего бота `backup_*.tar.gz`, где внутри лежали только `opt/gotelegram/config.json` и `etc/telemt/config.toml`.
Расписания бекапов: `set_backup_schedule off|daily|weekly|monthly` пишет `/etc/systemd/system/gotelegram-backup.{service,timer}` и `/opt/gotelegram/backup_schedule.json`. OnCalendar: daily `*-*-* 03:20:00`, weekly `Sun 03:20:00`, monthly `*-*-01 03:20:00`. Автоматическая чистка держит 30 последних `.tar.gz*` архивов.
### 13.1 Local Web Admin (v2.5.0)
Локальная web-админка находится в `admin-web/` и устанавливается в `/opt/gotelegram-admin`:
- backend: `admin-web/server.py`, Python stdlib only, без pip/npm dependencies;
- frontend: `admin-web/static/`, vanilla JS/CSS, SVG-график без CDN;
- systemd service: `gotelegram-admin`;
- bind: `127.0.0.1:1984`, доступ только через SSH tunnel;
- токена нет: после SSH tunnel открывается `http://127.0.0.1:1984/`;
- write-запросы дополнительно требуют `X-GoTelegram-Admin: 1`, фронтенд добавляет его автоматически.
- язык панели читается из `config.json.language`, затем из `/opt/gotelegram/.language`, fallback `en`; `POST /api/settings/language` сохраняет RU/EN в общий конфиг, marker file и bot `.env`; `gotelegram-bot/i18n.py` использует тот же источник как default до per-user override;
- UI построен вкладками (`dashboard`, `traffic`, `keys`, `backups`, `logs`, `settings`), есть иконки меню, полезный overview-блок по реальным TCP/UDP-слушателям 443, light/dark theme в `localStorage` и promo-modal раз в 24 часа через `localStorage`;
- `/api/overview` отдаёт `stats_status`, `admin_bind`, `site_status`, `port_443` и `backup_schedule`; `/api/site/check` проверяет `https://config.domain/` и считает OK только HTTP 200; `/api/stats?range=15m|1h|24h|month` отдаёт выбранное окно и `summary_rows`; `/api/users/<name>/traffic?range=15m|1h|24h|month` отдаёт per-user историю по `telemt total_octets`; `/api/users/<name>/qr` отдаёт PNG QR для Telegram proxy link через `qrencode`; `POST /api/users/<name>/max-ips` пишет `[access.user_max_unique_ips]` (`0` удаляет строку и означает безлимит); `/api/stats/collect` делает разовый сбор, `/api/stats/repair` устанавливает/перезапускает `gotelegram-stats`.
Функции: overview, проверка сайта на HTTP 200, service status/restart, чтение/запись `[access.users]`, enable/disable ключей через `/api/users/<name>/enabled`, per-user лимит одновременных уникальных IP через `[access.user_max_unique_ips]`, генерация proxy links и QR, общий traffic history из `/opt/gotelegram/stats_history.csv`, per-user traffic history из `/opt/gotelegram/user_stats_history.csv` с периодами 15m/1h/24h/month, current stats из `/run/gotelegram/stats_current.json`, список/создание backup, `POST /api/backups/schedule`, `POST /api/backups/restore` (background restore job, только basename из backup dir), структурированные journal logs (`service`, `ok`, `exit_code`, `line_count`, `text`).
Traffic retention: обе CSV-истории хранятся максимум 365 дней. Последние 31 день остаются с поминутным разрешением, более старые точки автоматически уплотняются до одной последней cumulative-точки в час. Чистка/уплотнение запускается не чаще одного раза в час, а per-user сбор пишет данные не чаще одного раза в минуту, даже если systemd-loop вызывает `stats_collect` каждую секунду. Параметры можно переопределить переменными `STATS_RETENTION_DAYS`, `STATS_MINUTE_RETENTION_DAYS`, `STATS_CLEANUP_INTERVAL`.
### 13.1.1 Shared TCP/443 with 3x-ui/Xray
`lib/shared443.sh` добавляет управляемую схему shared-443 через nginx stream `ssl_preread`:
- публичный вход: `0.0.0.0:443` принадлежит nginx stream dispatcher;
- default backend: `127.0.0.1:7443` (`telemt`), а `general.links.public_port` остаётся `443`;
- сайт остаётся за telemt через `dns_overrides` на `127.0.0.1:8443`;
- Xray/3x-ui должен быть перенесён в панели на внутренний target, например `127.0.0.1:9443`, после чего `shared443_enable <domain> <xray-sni> 127.0.0.1:9443` пишет `/opt/gotelegram/shared-443.json` и `/etc/nginx/stream-conf.d/gotelegram-shared443.conf`.
Автоматически переписывать SQLite/JSON 3x-ui нельзя: панель может перегенерировать Xray config и потерять ручные правки. Поэтому goTelegram Pro показывает конфликт прямого bind на `443`, даёт маршрут и включает dispatcher только на уровне собственных конфигов/nginx.
Отключённые ключи хранятся в `/opt/gotelegram/disabled_users.json`: active keys остаются в `/etc/telemt/config.toml` под `[access.users]`, disabled keys удаляются из active block и могут быть возвращены обратно без потери secret. `main` защищён от удаления и отключения. Лимит уникальных IP сохраняется в `[access.user_max_unique_ips]` и не теряется при disable/enable; при удалении ключа строка лимита удаляется. Операции с ключами в web-admin и Telegram-боте берут общий file lock `/run/gotelegram/admin-users.lock`, TOML пишется через temp+replace и quoted keys (`"a.b"`), а telemt restart для add/delete/enable/disable/IP-limit ставится через `systemctl --no-block restart`, чтобы switch в UI не зависал на `wait_tcp_port`.
`install_admin_web` вызывается при установке Telegram-бота. `auto_install_admin_web_if_possible` подхватывает админку после bootstrap/update, если Python уже установлен и файлы отличаются. При установке админки скрипт пытается установить/перезапустить `gotelegram-stats`; если это не удалось, оператор может нажать Restart collector в Traffic. Backup v1.6 сохраняет `admin_web/server.py`, `admin_web/static/`, `disabled_users.json`, `stats_history.csv`, `user_stats_history.csv`, `backup_schedule.json` и `shared-443.json`, restore возвращает их, удаляет legacy `admin_web/token` и пробует перезапустить `gotelegram-admin`.
### 13.2 Upgrade migration (v2.5.0)
`install.sh` вызывает `auto_migrate_legacy_state` перед интерактивным меню и перед non-interactive `--action=...` для бота. Цель — комфортно обновляться даже с кривой старой логики без переустановки прокси:
- перед изменениями создаётся `/opt/gotelegram/backups/preupgrade_2.5.0_*.tar.gz`;
- из старого `/etc/telemt/config.toml` вытаскиваются все строки `[access.users]`, а не только `main`;
- если `main` отсутствует, он добавляется с первым найденным секретом, чтобы старые ссылки не потерялись;
- сохраняются порт, `tls_domain`, `mask_port`, режим Lite/Pro, домен, язык, `stats_enabled`;
- старый telemt TOML нормализуется под v2.5.0 с `[server.api]` и metrics, затем в него возвращается полный users block;
- если сайт уже развёрнут, но template id неизвестен или старый `config.json` врёт, в `/var/www/gotelegram-site/.gotelegram_template_id` пишется `deployed_site`;
- `config.json` переписывается в актуальный формат, но с сохранением `installed_at` и пользовательских настроек;
- если telemt был активен и TOML был изменён, сервис перезапускается один раз.
Инвариант: миграция должна быть идемпотентной. Маркер `/opt/gotelegram/.migrated_2.5.0` предотвращает повторную нормализацию без причины.
---
## 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 = os.environ["GOTELEGRAM_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.5.0 (2026-04-24)** — крупный maintenance pass в ветке `codex`: единая версия `2.5.0` в runtime и документации; удалён дефолтный PAT из `bootstrap.sh` (токен теперь только через `GOTELEGRAM_PAT`); `generate_telemt_toml` добавляет `[server.api]` на `127.0.0.1:9091` и metrics на `127.0.0.1:9090`, что нужно для управления пользователями и статистики; Telegram-бот получил меню `🔑 Keys` для `[access.users]` (добавить/отключить/включить/удалить/показать ссылку/QR/runtime info/IP-limit); добавлена локальная web-админка goTelegram Pro `gotelegram-admin` на `127.0.0.1:1984` с SSH-tunnel инструкцией в боте без отдельного web-admin токена, вкладочной UI-навигацией, иконками, блоком реальных TCP/UDP-слушателей 443, promo-modal раз в 24 часа, i18n от языка установки, ручным переключателем RU/EN, site check на HTTP 200, structured journal logs, light/dark theme, адаптивом, быстрыми switch-переключателями ключей, настройкой `[access.user_max_unique_ips]`, QR-кодами, traffic history 15m/1h/24h/month с переключением график/строки, per-user traffic history из `telemt total_octets` и stats collector restart endpoint; исправлено чтение traffic CSV в боте (header больше не ломает parsing); бот сам делает `stats_collect` перед показом статистики; `iptables` добавлен в optional deps и stats collector пытается установить его; CLI-смена шаблона теперь обновляет `config.json.template_id`, чтобы бот не показывал первый установленный шаблон; backup/restore версии `1.6` сохраняет bot `.env`, bot lang files, disabled user keys, backup schedule, web-admin server/static, custom templates, templates catalog, stats history, user stats history, shared-443 config и полноценную структуру Let's Encrypt (`live/archive/renewal`) для переезда на новый сервер; добавлен безопасный детект 3x-ui/Xray на 443 и управляемый nginx stream shared-443 dispatcher.
- **2.4.6 (2026-04-10)** — universal `apt_lock_wait` helper: ожидание dpkg/apt lock при unattended-upgrades, исправляет установку nginx/certbot/python на свежих VPS.
- **2.4.3 (2026-04-10)** — iter3-фикс: `bot_action_dispatch` оборачивается во `flock -w 30` на `/var/lock/gotelegram-bot-action.lock`. Обнаружена гонка: параллельные `change-lite-domain` получали `"no secret in config"`, потому что один процесс читал `config.json`, пока другой делал `jq ... > tmp && mv`. `util-linux` (содержит `flock`) добавлен в `critical` deps, `check_deps_present` и маппинги `apt_pkg_for_cmd`/`dnf_pkg_for_cmd`.
- **2.4.2 (2026-04-10)** — реализация non-interactive `bot_action_*` в install.sh (change-template + change-lite-domain с JSON-ответом). bot.py подключает `run_bot_action()` и делает реальную работу вместо stub'ов. Критфиксы: (a) `safe_edit_message` принимает `disable_web_page_preview` (иначе TypeError в success-пути cb_pro_confirm); (b) чтение/запись `config['template_id']` вместо `config['template']` (save_gotelegram_config всегда писал `template_id`, бот смотрел не туда); (c) `bot_update_config_field` использует shell `date -Iseconds` вместо `jq now|todate` (jq 1.5 совместимость для Debian 10); (d) `asyncio.Lock _BOT_ACTION_LOCK` сериализует callback'и в процессе бота; (e) валидация `_TPL_ID_RE`/`_DOMAIN_RE` до subprocess. Полный аудит и автоустановка зависимостей: `ensure_deps` разделяет critical/optional, дедуплицирует пакеты, re-verify после install; `apt_pkg_for_cmd` и `dnf_pkg_for_cmd` мапят команды на пакеты (dig→dnsutils/bind-utils, xxd→xxd/vim-common, flock→util-linux). `check_deps_present` — быстрый чек без `apt-get update`.
- **2.4.1 (2026-04-10)** — баг #23: `start_telemt` делает `restart` если сервис активен (иначе stale in-memory config после переустановки Lite поверх Pro). Полная документация проекта — `DOCS_HUMAN.md` и `DOCS_AI.md` (этот файл).
- **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.**

270
DOCS_HUMAN.md Normal file
View File

@@ -0,0 +1,270 @@
# goTelegram Pro — руководство пользователя
**Версия:** 2.5.0
**Репозиторий:** `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")
```
Для приватного репозитория безопаснее передавать токен через переменную окружения:
```bash
GOTELEGRAM_PAT=YOUR_PAT bash <(curl -sL "https://raw.githubusercontent.com/anten-ka/gotelegram_pro/test/bootstrap.sh")
```
`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`, `/opt/gotelegram/disabled_users.json`, данные nginx-сайта, сертификаты Let's Encrypt (`live/archive/renewal`), пользовательские шаблоны, каталог шаблонов, историю трафика, состояние Telegram-бота и локальной web-админки. По умолчанию архивы лежат в `/opt/gotelegram/backups/`.
Пункт 9 принимает такой архив и восстанавливает всё обратно (конфиг, сервис, ссылка — всё то же, что было). Перед восстановлением из админки или Telegram-бота автоматически создаётся свежий safety-бекап текущего состояния.
В web-админке и Telegram-боте есть сценарии автобэкапов: выключено, каждый день, каждую неделю или каждый месяц. Это systemd timer `gotelegram-backup.timer`; автоматическая чистка оставляет последние 30 архивов, чтобы каталог не рос бесконечно. Старые простые архивы `backup_*.tar.gz`, которые создавал ранний Telegram-бот только из двух конфигов, тоже распознаются при восстановлении как legacy-формат.
---
## 8. Локальная web-админка
Начиная с **2.5.0** вместе с ботом ставится локальная web-админка:
- systemd service: `gotelegram-admin`;
- слушает только `127.0.0.1:1984`;
- наружу не публикуется и рассчитана на доступ через SSH tunnel;
- после туннеля открывается обычным URL `http://127.0.0.1:1984/`;
- язык берётся из `config.json.language` / `/opt/gotelegram/.language`, как в CLI и Telegram-боте; в верхней панели можно переключить RU/EN, выбор сохраняется в общий конфиг;
- есть светлая/тёмная тема, вкладки и адаптивная вёрстка под desktop/mobile;
- Telegram-бот показывает инструкцию для Termius и обычную команду `ssh -L 1984:127.0.0.1:1984 root@SERVER`.
В админке есть dashboard, проверка сайта `https://домен/` на HTTP 200, статус сервисов, полезный блок «кто слушает порт 443» по данным `ss`, управление ключами `[access.users]` с добавлением, удалением и быстрым отключением через switch, лимит одновременных уникальных IP на ключ через `[access.user_max_unique_ips]` (`0` — безлимит), генерация ссылок и QR-кодов для импорта в Telegram, traffic history по периодам 15 минут / 1 час / 24 часа / месяц с переключателем график/строки, такая же статистика по каждому ключу, кнопка разового обновления статистики, кнопка перезапуска сборщика, список/создание/восстановление бекапов, расписание автобэкапов и просмотр логов с количеством строк и статусом `journalctl`.
История трафика хранится максимум 1 год. Чтобы файлы не разрастались, последние 31 день пишутся поминутно, а более старая история автоматически уплотняется до одной точки в час. Для обычного просмотра 15 минут / 1 час / 24 часа / месяц детализация остаётся полной.
## 8.1 3x-ui / VLESS на том же 443
Один порт `443` не могут одновременно слушать `telemt` и Xray напрямую. Для совместной работы используется схема shared-443: публичный `443` занимает nginx stream-диспетчер, goTelegram `telemt` переносится на `127.0.0.1:7443`, сайт остаётся на `127.0.0.1:8443`, а inbound 3x-ui/Xray нужно в панели перенести на внутренний адрес, например `127.0.0.1:9443`.
После переноса Xray-входа можно включить маршрут:
```bash
source /opt/gotelegram/lib/shared443.sh
shared443_enable my-domain.com xray-domain.com 127.0.0.1:9443
```
goTelegram Pro не переписывает базу 3x-ui автоматически, потому что панель может перегенерировать Xray-конфиг. Админка показывает карту `443`: публичный edge, telemt, сайт и Xray-маршруты.
Отключённые ключи убираются из активного telemt-конфига и сохраняются в `/opt/gotelegram/disabled_users.json`, поэтому их можно включить обратно без потери secret. Основной ключ `main` защищён от удаления и отключения.
Лимит IP хранится в `/etc/telemt/config.toml` в секции `[access.user_max_unique_ips]`. Значение `0` в UI означает отсутствие строки в этой секции и безлимит. Значение `1`, `2` и выше ограничивает количество одновременно активных уникальных IP для выбранного ключа; если лимит уже занят, новый IP не проходит, пока один из старых не отключится.
---
## 9. Обновление
Два типа обновлений:
- **Обновление ядра telemt** (пункт 10) — тянет свежий бинарник с GitHub Releases `telemt/telemt`, сохраняет старый в `.bak`, перезапускает сервис. Конфиг остаётся как был.
- **Обновление самого goTelegram Pro** (пункт 1) — переустанавливает скрипты и lib/. Файл `config.json` не трогается, ключ и домен не меняются.
Bootstrap.sh умеет сам обновлять всё, если запустить его повторно.
Начиная с **2.5.0** первый запуск новой версии делает автоматическую миграцию старого состояния: создаёт pre-upgrade архив в `/opt/gotelegram/backups/`, вытаскивает все ключи из `[access.users]`, сохраняет домен, порт, режим, язык, историю статистики и фактически развёрнутый сайт. Если старый `template_id` был неправильным или отсутствовал, сайт помечается как `deployed_site`, чтобы Telegram-бот не показывал первый установленный шаблон вместо текущего.
---
## 10. Удаление
Пункт **13) Удалить всё** даёт выбор:
- Удалить только прокси (оставить бота).
- Удалить только бота.
- Удалить всё целиком, включая `/opt/gotelegram`, `/etc/telemt`, symlink `gotelegram`, systemd юниты, nginx-конфиг, бекапы и сайт из `/var/www/gotelegram-site`.
После «удалить всё» VPS возвращается к состоянию до установки (кроме скачанных пакетов типа `jq`, `nginx`, `certbot` — они остаются).
---
## 11. Требования к VPS
- **ОС:** Ubuntu 20.04+ или Debian 11+ (протестировано на Ubuntu 22.04).
- **RAM:** 512 МБ минимум, 1 ГБ комфортно (telemt сам по себе ест мало, но рядом nginx + бот).
- **Диск:** 2 ГБ (в основном под каталог шаблонов и бекапы).
- **Права:** root или sudo.
- **Порты:** в обычной схеме 443 должен быть свободен. Для совместной работы с 3x-ui/Xray используйте shared-443 и переносите Xray inbound на внутренний порт, например `127.0.0.1:9443`.
- **Для Pro-режима:** домен с настроенным A-record на IP VPS. DNS должен отвечать ДО установки, иначе Let's Encrypt не выдаст сертификат.
---
## 12. Частые вопросы
**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: Да, через shared-443: nginx stream слушает публичный `443`, goTelegram `telemt` работает на `127.0.0.1:7443`, а Xray/3x-ui — на внутреннем порту вроде `127.0.0.1:9443`. Напрямую два процесса на `0.0.0.0:443` работать не будут.
**Q: Это легально?**
A: Сам MTProxy — да, это публичная технология из исходников Telegram. Запуск прокси, чтобы твои друзья могли пользоваться Telegram там, где он заблокирован — в большинстве юрисдикций легально. Проверь локальные законы.
---
## 12. Где что лежит
| Что | Где |
| --- | --- |
| Скрипты (`install.sh`, `lib/`) | `/opt/gotelegram/` |
| Симлинк запуска | `/usr/local/bin/gotelegram` |
| Конфиг telemt | `/etc/telemt/config.toml` |
| Конфиг goTelegram Pro (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 Pro | `/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.5.0** — единая версия по коду и документации; удалён дефолтный PAT из `bootstrap.sh`; исправлена статистика в боте (CSV header больше не ломает чтение истории, бот сам обновляет snapshot); CLI-смена шаблона теперь обновляет `config.json.template_id`, поэтому бот показывает текущий шаблон; telemt TOML включает локальный API `127.0.0.1:9091` и metrics на `127.0.0.1:9090`; добавлено меню Telegram-бота для отдельных ключей пользователей (`[access.users]`): список, добавление, отключение/включение, удаление, ссылка, QR-код, текущий runtime, лимит уникальных IP через `[access.user_max_unique_ips]` и история трафика по ключу; добавлена локальная web-админка goTelegram Pro на `127.0.0.1:1984` под SSH tunnel без отдельного токена, с вкладками, иконками, promo-разделом раз в 24 часа, i18n от языка установки, ручным переключателем RU/EN, проверкой сайта на HTTP 200, тёмной темой, адаптивом, быстрыми switch-переключателями ключей, настройкой IP-лимита, QR-кодами, блоком реальных TCP/UDP-слушателей 443, подсказками к техническим терминам, traffic history по периодам 15 минут / 1 час / 24 часа / месяц и per-user traffic history; backup/restore сохраняет bot `.env`, языки бота, отключённые ключи, web-admin server/static, custom templates, stats history, user stats history, shared-443 config и структуру Let's Encrypt для переезда на новый VPS; добавлены ручные/ежедневные/еженедельные/ежемесячные бэкапы, восстановление из админки/бота с safety-бекапом и legacy-restore для старых `backup_*.tar.gz`; добавлен безопасный детект 3x-ui/Xray на 443 и управляемый nginx stream shared-443 dispatcher.
- **2.4.6** — ожидание apt/dpkg lock на свежих Ubuntu/Debian, чтобы установка nginx/certbot/Python не падала во время unattended-upgrades.
- **2.4.3** — фикс гонки в `bot_action_dispatch`: параллельные вызовы `change-lite-domain`/`change-template` (например, два пользователя бота одновременно) могли получить ошибку «no secret in config», если один процесс читал `config.json` в момент, когда другой его перезаписывал через `jq`. Теперь диспетчер оборачивается в `flock(1)` с таймаутом 30 с; `util-linux` (содержит `flock`) добавлен в критические зависимости.
- **2.4.2** — смена шаблона и домена маскировки **прямо из Telegram-бота** без SSH. Раньше эти пункты меню показывали сообщение «сделай через CLI», теперь бот вызывает `install.sh --action=change-template --json` / `--action=change-lite-domain --json` и разбирает ответ. Плюс: безопасный `safe_edit_message` принимает `disable_web_page_preview`; поле статуса шаблона наконец-то отображается (раньше читалось не из того ключа JSON); полный аудит и автоустановка системных зависимостей при первом запуске (`curl jq openssl git xxd tar dig flock` + опциональные `qrencode bc`); `asyncio.Lock` в боте сериализует параллельные callback'и; валидация tpl\_id (`[A-Za-z0-9_-]{1,64}`) и домена до subprocess.
- **2.4.1** — фикс: `start_telemt` теперь делает `restart` если сервис уже запущен. Раньше переустановка Lite поверх Pro оставляла в памяти старый конфиг, и клиенты получали «Unknown TLS SNI drop». Плюс полная документация проекта (этот файл и `DOCS_AI.md`).
- **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`.

1665
admin-web/server.py Normal file

File diff suppressed because it is too large Load Diff

1755
admin-web/static/app.js Normal file

File diff suppressed because it is too large Load Diff

398
admin-web/static/index.html Normal file
View File

@@ -0,0 +1,398 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>goTelegram Pro Admin</title>
<script>
(function () {
var stored = localStorage.getItem("gotelegram-theme");
var theme = stored || (matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light");
document.documentElement.dataset.theme = theme;
}());
</script>
<link rel="stylesheet" href="/styles.css?v=2.5.0-admin18">
</head>
<body>
<div class="app-shell">
<aside class="sidebar" id="sidebar">
<div class="brand">
<div class="brand-mark">GT</div>
<div>
<strong>goTelegram Pro</strong>
<span data-i18n="brandSubtitle">Local Admin</span>
</div>
</div>
<nav class="nav-tabs" aria-label="Admin sections" data-i18n-aria-label="ariaAdminSections">
<button type="button" class="nav-item active" data-nav="dashboard"><span class="nav-icon"></span><span data-i18n="navDashboard">Dashboard</span></button>
<button type="button" class="nav-item" data-nav="traffic"><span class="nav-icon"></span><span data-i18n="navTraffic">Traffic</span></button>
<button type="button" class="nav-item" data-nav="keys"><span class="nav-icon"></span><span data-i18n="navKeys">Keys</span></button>
<button type="button" class="nav-item" data-nav="backups"><span class="nav-icon"></span><span data-i18n="navBackups">Backups</span></button>
<button type="button" class="nav-item" data-nav="logs"><span class="nav-icon"></span><span data-i18n="navLogs">Logs</span></button>
<button type="button" class="nav-item" data-nav="settings"><span class="nav-icon"></span><span data-i18n="navSettings">Settings</span></button>
</nav>
<div class="sidebar-foot">
<span id="sidebarVersion">v--</span>
<span id="sidebarBind">127.0.0.1:1984</span>
</div>
</aside>
<div class="workspace">
<header class="topbar">
<button id="menuBtn" class="icon-btn mobile-only" type="button" aria-label="Menu" data-i18n-aria-label="ariaMenu"></button>
<div class="title-block">
<p class="eyebrow" id="pageKicker">Local Admin</p>
<h1 id="pageTitle">Dashboard</h1>
<small id="lastRefresh">--</small>
</div>
<div class="top-actions">
<select id="languageSelect" class="language-select" aria-label="Language" data-i18n-aria-label="ariaLanguage">
<option value="en">EN</option>
<option value="ru">RU</option>
</select>
<button id="themeToggle" class="ghost" type="button">Theme</button>
<button id="refreshBtn" type="button" data-i18n="refresh">Refresh</button>
<button id="autoRefreshToggle" class="auto-refresh-toggle" type="button" aria-pressed="true" data-i18n-title="autoRefresh" title="Auto refresh">
<span class="auto-refresh-icon" aria-hidden="true"></span>
<span class="auto-refresh-track" aria-hidden="true"><span></span></span>
<span class="auto-refresh-state">5s</span>
</button>
</div>
</header>
<main class="content">
<section class="page-panel active" data-page="dashboard">
<section class="visual-overview">
<div>
<p class="eyebrow">goTelegram Pro</p>
<h2 id="visualTitle">Port 443</h2>
<p id="visualText">Website, MTProxy and local admin status in one operational view.</p>
</div>
<div class="port-map" id="port443Map">
<div class="port-map-head">
<div class="port-badge">
<span id="port443Number">443</span>
<small id="port443Configured">public</small>
</div>
<strong id="port443Summary">--</strong>
</div>
<div id="port443List" class="port-list"></div>
</div>
</section>
<div class="metric-grid">
<article class="metric-card accent-blue">
<span data-i18n="metricMode">Mode</span>
<strong id="metricMode">--</strong>
<small id="metricDomain">--</small>
<small id="siteStatus" class="metric-status">--</small>
</article>
<article class="metric-card accent-green">
<span data-i18n="metricKeys">Keys</span>
<strong id="metricUsers">0</strong>
<small data-i18n="configuredUsers">configured users</small>
</article>
<article class="metric-card accent-violet">
<span data-i18n="metricProxyTraffic">Proxy Traffic</span>
<strong id="metricProxyTraffic">0 B</strong>
<small id="metricProxyPackets">0 packets</small>
</article>
<article class="metric-card accent-amber">
<span data-i18n="metricSiteTraffic">Site Traffic</span>
<strong id="metricSiteTraffic">0 B</strong>
<small id="metricSitePackets">0 packets</small>
</article>
</div>
<div class="grid-two">
<section class="panel">
<div class="panel-head">
<div>
<p class="eyebrow" data-i18n="servicesEyebrow">Services</p>
<h2 class="with-help"><span data-i18n="servicesTitle">Service health</span><span class="info-hint" tabindex="0" data-i18n-title="servicesHelp" title="Service health">?</span></h2>
</div>
</div>
<div class="service-grid" id="services"></div>
</section>
<section class="panel">
<div class="panel-head">
<div>
<p class="eyebrow" data-i18n="runtimeEyebrow">Runtime</p>
<h2 class="with-help"><span data-i18n="runtimeTitle">telemt summary</span><span class="info-hint" tabindex="0" data-i18n-title="runtimeHelp" title="Runtime data">?</span></h2>
</div>
</div>
<div id="runtimeCards" class="runtime-grid"></div>
<div id="runtimeIssues" class="issue-list"></div>
</section>
</div>
</section>
<section class="page-panel" data-page="traffic">
<div class="panel">
<div class="panel-head">
<div>
<p class="eyebrow" data-i18n="trafficEyebrow">Traffic</p>
<h2 data-i18n="trafficTitle">History</h2>
</div>
<div class="panel-actions">
<span id="statsHealth" class="status-pill">--</span>
<button id="collectStatsBtn" class="ghost" type="button" data-i18n="collectStats" data-i18n-title="collectStatsHelp">Collect</button>
<button id="repairStatsBtn" type="button" data-i18n="repairStats" data-i18n-title="repairStatsHelp">Restart collector</button>
</div>
</div>
<div class="traffic-summary">
<article>
<span data-i18n="collector">Collector</span>
<strong id="collectorState">--</strong>
</article>
<article>
<span data-i18n="lastPoint">Last point</span>
<strong id="lastStatsPoint">--</strong>
</article>
<article>
<span data-i18n="historyRows">History rows</span>
<strong id="historyRows">0</strong>
</article>
</div>
<div class="traffic-controls">
<div class="segmented" id="trafficRange" aria-label="Traffic range" data-i18n-aria-label="ariaTrafficRange">
<button type="button" data-traffic-range="15m" data-i18n="range15m">15 min</button>
<button type="button" data-traffic-range="1h" data-i18n="range1h">1 hour</button>
<button type="button" data-traffic-range="24h" data-i18n="range24h">24 hours</button>
<button type="button" data-traffic-range="month" data-i18n="rangeMonth">Month</button>
</div>
<div class="segmented" id="trafficView" aria-label="Traffic view" data-i18n-aria-label="ariaTrafficView">
<button type="button" data-traffic-view="chart" data-i18n="viewChart">Chart</button>
<button type="button" data-traffic-view="table" data-i18n="viewRows">Rows</button>
</div>
</div>
<div id="trafficChart" class="traffic-chart"></div>
<div class="table-wrap" id="trafficTableWrap">
<table>
<thead>
<tr>
<th data-i18n="tablePeriod">Period</th>
<th data-i18n="tableProxyDelta">Proxy delta</th>
<th data-i18n="tableSiteDelta">Site delta</th>
<th data-i18n="tableProxyTotal">Proxy total</th>
<th data-i18n="tableSiteTotal">Site total</th>
</tr>
</thead>
<tbody id="historyTable"></tbody>
</table>
</div>
</div>
</section>
<section class="page-panel" data-page="keys">
<div class="panel">
<div class="panel-head">
<div>
<p class="eyebrow" data-i18n="keysEyebrow">Access</p>
<h2 data-i18n="keysTitle">User keys</h2>
</div>
<form id="addUserForm" class="inline-form">
<input id="userName" autocomplete="off" placeholder="client-name" data-i18n-placeholder="userPlaceholder">
<button type="submit" data-i18n="addKey">Add key</button>
</form>
</div>
<div class="keys-wrap">
<div class="keys-list" id="usersTable" aria-live="polite"></div>
</div>
</div>
<div class="panel user-traffic-panel" id="userTrafficPanel">
<div class="panel-head">
<div>
<p class="eyebrow" data-i18n="userTrafficEyebrow">Per user</p>
<h2 id="userTrafficTitle" data-i18n="userTrafficTitle">User traffic</h2>
</div>
<div class="panel-actions">
<span id="userTrafficHealth" class="status-pill">--</span>
</div>
</div>
<div class="traffic-summary compact">
<article>
<span data-i18n="trafficTotal">Total</span>
<strong id="userTrafficTotal">--</strong>
</article>
<article>
<span data-i18n="currentConnections">Connections</span>
<strong id="userTrafficConnections">--</strong>
</article>
<article>
<span data-i18n="activeIps">Active IPs</span>
<strong id="userTrafficIps">--</strong>
</article>
</div>
<div class="traffic-controls">
<div class="segmented" id="userTrafficRange" aria-label="User traffic range" data-i18n-aria-label="ariaTrafficRange">
<button type="button" data-user-traffic-range="15m" data-i18n="range15m">15 min</button>
<button type="button" data-user-traffic-range="1h" data-i18n="range1h">1 hour</button>
<button type="button" data-user-traffic-range="24h" data-i18n="range24h">24 hours</button>
<button type="button" data-user-traffic-range="month" data-i18n="rangeMonth">Month</button>
</div>
<div class="segmented" id="userTrafficView" aria-label="User traffic view" data-i18n-aria-label="ariaTrafficView">
<button type="button" data-user-traffic-view="chart" data-i18n="viewChart">Chart</button>
<button type="button" data-user-traffic-view="table" data-i18n="viewRows">Rows</button>
</div>
</div>
<div id="userTrafficChart" class="traffic-chart"></div>
<div class="table-wrap" id="userTrafficTableWrap">
<table>
<thead>
<tr>
<th data-i18n="tablePeriod">Period</th>
<th data-i18n="tableTrafficDelta">Traffic delta</th>
<th data-i18n="tableTrafficTotal">Total</th>
</tr>
</thead>
<tbody id="userTrafficTable"></tbody>
</table>
</div>
</div>
</section>
<section class="page-panel" data-page="backups">
<div class="grid-two">
<section class="panel">
<div class="panel-head">
<div>
<p class="eyebrow" data-i18n="backupsEyebrow">Snapshots</p>
<h2 data-i18n="backupsTitle">Backups</h2>
</div>
<button id="createBackupBtn" type="button" data-i18n="createBackup">Create backup</button>
</div>
<div class="backup-schedule">
<div>
<strong data-i18n="backupScheduleTitle">Automatic backups</strong>
<span id="backupScheduleMeta" data-i18n="backupScheduleLoading">Loading schedule...</span>
</div>
<div class="segmented compact" role="group" aria-label="Backup schedule">
<button type="button" data-backup-schedule="off" data-i18n="scheduleOff">Off</button>
<button type="button" data-backup-schedule="daily" data-i18n="scheduleDaily">Daily</button>
<button type="button" data-backup-schedule="weekly" data-i18n="scheduleWeekly">Weekly</button>
<button type="button" data-backup-schedule="monthly" data-i18n="scheduleMonthly">Monthly</button>
</div>
</div>
<div class="backup-includes">
<strong data-i18n="backupIncludesTitle">Backup contents</strong>
<span data-i18n="backupIncludesText">telemt config, goTelegram settings, keys, disabled keys, site, templates, SSL certificates, bot, admin panel and traffic history.</span>
</div>
<div id="backupsList" class="backup-list"></div>
</section>
<aside class="panel">
<div class="panel-head">
<div>
<p class="eyebrow" data-i18n="eventsEyebrow">Events</p>
<h2 data-i18n="eventsTitle">Activity</h2>
</div>
</div>
<div id="events" class="events-list"></div>
</aside>
</div>
</section>
<section class="page-panel" data-page="logs">
<div class="panel">
<div class="panel-head">
<div>
<p class="eyebrow" data-i18n="logsEyebrow">Journal</p>
<h2 data-i18n="logsTitle">Logs</h2>
</div>
<div class="inline-form">
<select id="logService">
<option value="telemt">telemt</option>
<option value="nginx">nginx</option>
<option value="gotelegram-bot">bot</option>
<option value="gotelegram-stats">stats</option>
<option value="gotelegram-admin">admin</option>
</select>
<button id="loadLogsBtn" type="button" data-i18n="loadLogs">Load</button>
</div>
</div>
<div id="logsMeta" class="logs-meta"></div>
<pre id="logsBox" class="logs"></pre>
</div>
</section>
<section class="page-panel" data-page="settings">
<div class="grid-two">
<section class="panel">
<div class="panel-head">
<div>
<p class="eyebrow" data-i18n="settingsEyebrow">Settings</p>
<h2 data-i18n="settingsTitle">Panel preferences</h2>
</div>
</div>
<div class="settings-list">
<div>
<span data-i18n="panelLanguage">Panel language</span>
<strong id="settingsLanguage">--</strong>
</div>
<div>
<span data-i18n="theme">Theme</span>
<strong id="settingsTheme">--</strong>
</div>
<div>
<span data-i18n="bindAddress">Bind address</span>
<strong id="settingsBind">127.0.0.1:1984</strong>
</div>
</div>
</section>
<section class="panel">
<div class="panel-head">
<div>
<p class="eyebrow" data-i18n="configEyebrow">Config</p>
<h2 data-i18n="configTitle">Installation state</h2>
</div>
</div>
<div id="configList" class="settings-list"></div>
</section>
</div>
</section>
</main>
</div>
</div>
<div id="toast" class="toast"></div>
<div id="qrModal" class="promo-modal" hidden>
<div class="promo-card qr-card" role="dialog" aria-modal="true" aria-labelledby="qrTitle">
<button id="qrClose" class="icon-btn ghost" type="button" aria-label="Close" data-i18n-aria-label="ariaClose">×</button>
<p class="eyebrow" data-i18n="qrEyebrow">QR import</p>
<h2 id="qrTitle" data-i18n="qrTitle">Scan Telegram proxy</h2>
<div class="qr-frame">
<img id="qrImage" alt="Telegram proxy QR">
</div>
<p id="qrMeta" class="modal-note"></p>
<button id="qrCopyBtn" type="button" class="soft" data-i18n="copyLink">Copy link</button>
</div>
</div>
<div id="promoModal" class="promo-modal" hidden>
<div class="promo-card" role="dialog" aria-modal="true" aria-labelledby="promoTitle">
<button id="promoClose" class="icon-btn ghost" type="button" aria-label="Close" data-i18n-aria-label="ariaClose">×</button>
<p class="eyebrow" data-i18n="promoEyebrow">Promo</p>
<h2 id="promoTitle" data-i18n="promoTitle">Support goTelegram Pro</h2>
<div class="promo-grid">
<a href="https://vk.cc/ct29NQ" target="_blank" rel="noreferrer">
<strong data-i18n="promoHosting1">Hosting #1</strong>
<span>OFF60 · antenka20 · antenka6</span>
</a>
<a href="https://vk.cc/cUxAhj" target="_blank" rel="noreferrer">
<strong data-i18n="promoHosting2">Hosting #2</strong>
<span>OFF60</span>
</a>
<a href="https://pay.cloudtips.ru/p/7410814f" target="_blank" rel="noreferrer">
<strong data-i18n="promoTips">Tips</strong>
<span>CloudTips</span>
</a>
</div>
</div>
</div>
<script src="/app.js?v=2.5.0-admin18" type="module"></script>
</body>
</html>

1708
admin-web/static/styles.css Normal file

File diff suppressed because it is too large Load Diff

139
bootstrap.sh Executable file
View File

@@ -0,0 +1,139 @@
#!/bin/bash
# GoTelegram Pro — Bootstrap installer for private repo
# Downloads all files and launches install.sh
set -euo pipefail
REPO="anten-ka/gotelegram_pro"
BRANCH="${GOTELEGRAM_BRANCH:-test}"
PAT="${GOTELEGRAM_PAT:-}"
INSTALL_DIR="/opt/gotelegram"
# Use raw.githubusercontent.com (CDN) — faster and avoids Contents API caching
# issues that occasionally return 404 for recently added files on non-default branches.
RAW="https://raw.githubusercontent.com/${REPO}/${BRANCH}"
RED='\033[0;31m'
GREEN='\033[0;32m'
CYAN='\033[0;36m'
YELLOW='\033[1;33m'
NC='\033[0m'
BOLD='\033[1m'
echo ""
echo -e " ${YELLOW}╔══════════════════════════════════════════════════════╗${NC}"
echo -e " ${YELLOW}${NC} ${BOLD}GoTelegram Pro — Установка${NC} ${YELLOW}${NC}"
echo -e " ${YELLOW}${NC} ${YELLOW}${NC}"
echo -e " ${YELLOW}${NC} MTProxy менеджер с Telegram-ботом ${YELLOW}${NC}"
echo -e " ${YELLOW}${NC} Stealth-режим, 1800+ шаблонов сайтов ${YELLOW}${NC}"
echo -e " ${YELLOW}╚══════════════════════════════════════════════════════╝${NC}"
echo ""
# Check root
if [ "$(id -u)" -ne 0 ]; then
echo -e " ${RED}${NC} Запустите от root: ${CYAN}sudo bash bootstrap.sh${NC}"
exit 1
fi
if [ -z "$PAT" ]; then
echo -e " ${RED}${NC} Не задан GitHub token."
echo -e " ${YELLOW}Запустите так:${NC} ${CYAN}GOTELEGRAM_PAT=YOUR_PAT sudo -E bash bootstrap.sh${NC}"
exit 1
fi
# Check dependencies
for cmd in curl jq; do
if ! command -v "$cmd" &>/dev/null; then
echo -e " ${CYAN}${NC} Установка $cmd..."
apt-get update -qq && apt-get install -y -qq "$cmd" >/dev/null 2>&1
fi
done
download_file() {
local remote_path="$1"
local local_path="$2"
local dir
dir=$(dirname "$local_path")
mkdir -p "$dir"
# 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
echo -e " ${RED}${NC} Ошибка загрузки ${remote_path} (HTTP ${http_code})"
return 1
}
# File list
FILES=(
"install.sh"
"install_gotelegram_bot.sh"
"templates_catalog.json"
"lib/common.sh"
"lib/telemt.sh"
"lib/telemt_config.sh"
"lib/backup.sh"
"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"
"admin-web/server.py"
"admin-web/static/index.html"
"admin-web/static/styles.css"
"admin-web/static/app.js"
)
echo -e " ${CYAN}${NC} Загрузка файлов в ${INSTALL_DIR}..."
mkdir -p "${INSTALL_DIR}/lib/lang" "${INSTALL_DIR}/gotelegram-bot/lang" "${INSTALL_DIR}/admin-web/static"
failed=0
for f in "${FILES[@]}"; do
if download_file "$f" "${INSTALL_DIR}/${f}"; then
echo -e " ${GREEN}${NC} ${f}"
else
failed=$((failed + 1))
fi
done
if [ "$failed" -gt 0 ]; then
echo ""
echo -e " ${RED}${NC} Не удалось загрузить ${failed} файл(ов)"
echo -e " ${YELLOW}Проверьте токен доступа и подключение к сети${NC}"
exit 1
fi
# Fix permissions and line endings
echo -e " ${CYAN}${NC} Настройка прав..."
chmod +x "${INSTALL_DIR}/install.sh" "${INSTALL_DIR}/install_gotelegram_bot.sh"
chmod +x "${INSTALL_DIR}"/lib/*.sh
chmod +x "${INSTALL_DIR}/admin-web/server.py" 2>/dev/null || true
sed -i 's/\r$//' "${INSTALL_DIR}/install.sh" "${INSTALL_DIR}/install_gotelegram_bot.sh" "${INSTALL_DIR}"/lib/*.sh "${INSTALL_DIR}"/lib/lang/*.sh 2>/dev/null || true
sed -i 's/\r$//' "${INSTALL_DIR}"/gotelegram-bot/*.py "${INSTALL_DIR}"/gotelegram-bot/lang/*.json 2>/dev/null || true
sed -i 's/\r$//' "${INSTALL_DIR}"/admin-web/*.py "${INSTALL_DIR}"/admin-web/static/* 2>/dev/null || true
# Create symlink
ln -sf "${INSTALL_DIR}/install.sh" /usr/local/bin/gotelegram
echo -e " ${GREEN}${NC} Команда ${CYAN}gotelegram${NC} доступна"
echo ""
echo -e " ${GREEN}${NC} Установка завершена! Запуск..."
echo ""
# Launch
exec bash "${INSTALL_DIR}/install.sh"

View File

@@ -1,4 +1,4 @@
# GoTelegram v2.2 Bot
# goTelegram Pro v2.5.0 Bot
Production-quality Telegram bot for managing MTProxy (telemt engine) on Linux servers.
@@ -19,6 +19,9 @@ Production-quality Telegram bot for managing MTProxy (telemt engine) on Linux se
- Promotional links
- **Template Browsing** - Browse categories → templates → preview → install
- **Per-user MTProxy Keys** - Manage telemt `[access.users]` from inline bot menus
- **Per-user QR Import** - Show QR codes for every Telegram proxy key
- **Local Web Admin** - Shows SSH tunnel instructions for the 127.0.0.1:1984 dashboard
- **V1 Migration** - Detects old mtg Docker container and offers migration
- **Access Control** - ALLOWED_IDS from .env
- **Async/Await** - Full async support via python-telegram-bot v21+
@@ -65,7 +68,7 @@ For systemd service:
```bash
[Unit]
Description=GoTelegram Bot
Description=goTelegram Pro Bot
After=network.target
[Service]
@@ -94,6 +97,7 @@ WantedBy=multi-user.target
- `TELEMT_SERVICE` - `telemt` (systemd service name)
- `WEBSITE_ROOT` - `/var/www/gotelegram-site`
- `BACKUP_DIR` - `/opt/gotelegram/backups`
- `BACKUP_SCHEDULE_FILE` - `/opt/gotelegram/backup_schedule.json`
- `TEMPLATES_CATALOG` - `/opt/gotelegram/templates_catalog.json`
## Architecture
@@ -112,6 +116,7 @@ Organized by feature:
- Installation (quick/stealth modes)
- Status monitoring
- Backup/restore
- Backup schedules: off, daily, weekly, monthly
- SSL management
- Updates
- Removal
@@ -144,4 +149,4 @@ code, stdout, stderr = await sh("command", "arg1", "arg2")
## License
GoTelegram v2.2 - Open source community project
goTelegram Pro v2.5.0 - Open source community project

File diff suppressed because it is too large Load Diff

View File

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

167
gotelegram-bot/i18n.py Normal file
View File

@@ -0,0 +1,167 @@
"""
goTelegram Pro v2.5.0 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")
GOTELEGRAM_CONFIG = Path("/opt/gotelegram/config.json")
GOTELEGRAM_LANG_MARKER = Path("/opt/gotelegram/.language")
# Supported codes; keep in sync with lang/*.json
SUPPORTED_LANGS = ("en", "ru")
def _detect_default_lang() -> str:
candidates = []
try:
if GOTELEGRAM_CONFIG.exists():
with open(GOTELEGRAM_CONFIG, "r", encoding="utf-8") as f:
data = json.load(f)
if isinstance(data, dict):
candidates.extend([data.get("language"), data.get("lang")])
except Exception as e:
logger.warning("failed to read goTelegram Pro language config: %s", e)
try:
if GOTELEGRAM_LANG_MARKER.exists():
candidates.append(GOTELEGRAM_LANG_MARKER.read_text(encoding="utf-8").strip()[:2])
except Exception as e:
logger.warning("failed to read goTelegram Pro language marker: %s", e)
candidates.append(os.getenv("BOT_LANG", ""))
for raw in candidates:
code = str(raw or "").strip().lower()
if code in SUPPORTED_LANGS:
return code
return "en"
DEFAULT_LANG = _detect_default_lang()
LANG_NAMES = {
"en": "English",
"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 _detect_default_lang()
return _USER_LANGS.get(int(user_id), _detect_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

118
gotelegram-bot/lang/en.json Normal file
View File

@@ -0,0 +1,118 @@
{
"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 Pro 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 Pro Bot — Commands",
"help_lines": "/start — Main menu\n/help — This help\n/status — Quick status\n/logs — Latest logs\n/lang — Change language\n/addadmin ID — Add admin\n/deladmin ID — Remove admin\n\nUse the menu buttons for other operations.",
"menu_install": "⚙️ Install",
"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_users": "🔑 Keys",
"menu_admin_web": "🖥 Web Admin",
"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"
}

118
gotelegram-bot/lang/ru.json Normal file
View File

@@ -0,0 +1,118 @@
{
"lang_english": "English",
"lang_russian": "Русский",
"lang_title": "🌐 Язык",
"lang_current": "Текущий язык: %s",
"lang_saved": "Язык сохранён: %s",
"lang_choose": "Выберите язык:",
"welcome_title": "goTelegram Pro 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 Pro 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_users": "🔑 Ключи",
"menu_admin_web": "🖥 Веб-админка",
"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 (маскировка + сайт)",
"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"
}

1469
install.sh

File diff suppressed because it is too large Load Diff

173
install_gotelegram_bot.sh Executable file
View File

@@ -0,0 +1,173 @@
#!/bin/bash
# GoTelegram v2.5.0 — Установка Telegram-бота
# Создаёт venv, ставит зависимости, настраивает systemd
set -e
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
NC='\033[0m'
BOT_DIR="/opt/gotelegram-bot"
SERVICE_NAME="gotelegram-bot"
GOTELEGRAM_DIR="/opt/gotelegram"
ADMIN_WEB_DIR="/opt/gotelegram-admin"
ADMIN_WEB_SERVICE="gotelegram-admin"
ADMIN_WEB_PORT="1984"
if [ "$EUID" -ne 0 ]; then
echo -e "${RED}Запустите с sudo.${NC}"
exit 1
fi
echo -e "${CYAN}╔═══════════════════════════════════════════╗${NC}"
echo -e "${CYAN}${NC} ${GREEN}GoTelegram v2.5.0 — Установка бота${NC} ${CYAN}${NC}"
echo -e "${CYAN}╚═══════════════════════════════════════════╝${NC}"
echo ""
# ── Python ───────────────────────────────────────────────────────────────────
if ! command -v python3 &>/dev/null; then
echo -e "${YELLOW}[*] Установка python3...${NC}"
if command -v apt-get &>/dev/null; then
apt-get update -qq && apt-get install -y -qq python3 python3-pip python3-venv
elif command -v dnf &>/dev/null; then
dnf install -y -q python3 python3-pip
elif command -v yum &>/dev/null; then
yum install -y -q python3 python3-pip
fi
fi
# ── Каталог бота ─────────────────────────────────────────────────────────────
mkdir -p "$BOT_DIR"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
[ -f "$SCRIPT_DIR/lib/common.sh" ] && source "$SCRIPT_DIR/lib/common.sh" || true
[ -f "$SCRIPT_DIR/lib/stats.sh" ] && source "$SCRIPT_DIR/lib/stats.sh" || true
if [ -f "$SCRIPT_DIR/gotelegram-bot/bot.py" ]; then
echo -e "${GREEN}[*] Копирование файлов бота...${NC}"
cp "$SCRIPT_DIR/gotelegram-bot/bot.py" "$BOT_DIR/"
cp "$SCRIPT_DIR/gotelegram-bot/requirements.txt" "$BOT_DIR/"
[ -f "$SCRIPT_DIR/gotelegram-bot/config.example.env" ] && cp "$SCRIPT_DIR/gotelegram-bot/config.example.env" "$BOT_DIR/"
else
echo -e "${RED}Файлы бота не найдены в $SCRIPT_DIR/gotelegram-bot/${NC}"
exit 1
fi
# Копируем каталог шаблонов
if [ -f "$SCRIPT_DIR/templates_catalog.json" ]; then
mkdir -p "$GOTELEGRAM_DIR"
cp "$SCRIPT_DIR/templates_catalog.json" "$GOTELEGRAM_DIR/"
echo -e "${GREEN}[*] Каталог шаблонов скопирован${NC}"
fi
# ── Virtual environment ──────────────────────────────────────────────────────
if [ ! -d "$BOT_DIR/venv" ]; then
echo -e "${GREEN}[*] Создание виртуального окружения...${NC}"
python3 -m venv "$BOT_DIR/venv"
fi
echo -e "${GREEN}[*] Установка зависимостей...${NC}"
"$BOT_DIR/venv/bin/pip" install -r "$BOT_DIR/requirements.txt" -q
# ── Конфигурация ─────────────────────────────────────────────────────────────
if [ ! -f "$BOT_DIR/.env" ]; then
echo ""
echo -e "${YELLOW}Введите BOT_TOKEN от @BotFather:${NC}"
TOKEN=""
while [ -z "$TOKEN" ]; do
read -r TOKEN
TOKEN=$(echo "$TOKEN" | tr -d '[:space:]')
[ -z "$TOKEN" ] && echo -e "${RED}Токен не может быть пустым.${NC}"
done
echo -ne "${YELLOW}ID администратора (Enter = доступ для всех):${NC} "
read -r ADMIN_ID
{
echo "BOT_TOKEN=$TOKEN"
[ -n "$ADMIN_ID" ] && echo "ALLOWED_IDS=$ADMIN_ID"
} > "$BOT_DIR/.env"
chmod 600 "$BOT_DIR/.env"
echo -e "${GREEN}[*] .env создан${NC}"
else
echo -e "${GREEN}[*] .env уже существует${NC}"
fi
# ── Systemd ──────────────────────────────────────────────────────────────────
cat > "/etc/systemd/system/${SERVICE_NAME}.service" << EOF
[Unit]
Description=GoTelegram v2.5.0 Telegram Bot
After=network.target
[Service]
Type=simple
WorkingDirectory=$BOT_DIR
ExecStart=$BOT_DIR/venv/bin/python $BOT_DIR/bot.py
Restart=always
RestartSec=5
Environment=PATH=$BOT_DIR/venv/bin:/usr/bin
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable "$SERVICE_NAME"
systemctl restart "$SERVICE_NAME" 2>/dev/null || systemctl start "$SERVICE_NAME"
# ── Local Web Admin ──────────────────────────────────────────────────────────
if [ -f "$SCRIPT_DIR/admin-web/server.py" ]; then
echo -e "${GREEN}[*] Установка локальной web-админки...${NC}"
mkdir -p "$ADMIN_WEB_DIR/static"
cp "$SCRIPT_DIR/admin-web/server.py" "$ADMIN_WEB_DIR/server.py"
cp -a "$SCRIPT_DIR/admin-web/static/." "$ADMIN_WEB_DIR/static/"
chmod 700 "$ADMIN_WEB_DIR"
chmod 755 "$ADMIN_WEB_DIR/server.py" "$ADMIN_WEB_DIR/static"
rm -f "$ADMIN_WEB_DIR/token" 2>/dev/null || true
PYTHON_BIN=$(command -v python3)
cat > "/etc/systemd/system/${ADMIN_WEB_SERVICE}.service" << EOF
[Unit]
Description=GoTelegram v2.5.0 Local Web Admin
After=network.target
[Service]
Type=simple
WorkingDirectory=$ADMIN_WEB_DIR
ExecStart=$PYTHON_BIN $ADMIN_WEB_DIR/server.py
Restart=always
RestartSec=5
Environment=GOTELEGRAM_ADMIN_HOST=127.0.0.1
Environment=GOTELEGRAM_ADMIN_PORT=$ADMIN_WEB_PORT
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable "$ADMIN_WEB_SERVICE"
systemctl restart "$ADMIN_WEB_SERVICE" 2>/dev/null || systemctl start "$ADMIN_WEB_SERVICE"
if type install_stats_collector &>/dev/null; then
install_stats_collector >/dev/null 2>&1 || echo -e "${YELLOW}[!] Сборщик статистики не запущен; откройте Traffic в Web Admin и нажмите Repair.${NC}"
fi
fi
echo ""
echo -e "${GREEN}╔═══════════════════════════════════════════╗${NC}"
echo -e "${GREEN}║ ✅ Бот установлен и запущен! ║${NC}"
echo -e "${GREEN}╚═══════════════════════════════════════════╝${NC}"
echo ""
echo -e "Проверка: ${CYAN}systemctl status $SERVICE_NAME${NC}"
echo -e "Логи: ${CYAN}journalctl -u $SERVICE_NAME -f${NC}"
echo -e "Настройки: ${CYAN}$BOT_DIR/.env${NC}"
echo ""
# Благодарности
echo -e "${CYAN}─────────────────────────────────────────────${NC}"
echo -e "💜 Спасибо авторам открытых проектов:"
echo -e " ${CYAN}telemt${NC} — MTProxy engine (Rust)"
echo -e " ${CYAN}HTML5 UP${NC} — шаблоны сайтов (CC BY 3.0)"
echo -e " ${CYAN}learning-zone${NC} — 150+ HTML5 шаблонов"
echo -e " ${CYAN}Start Bootstrap${NC} — Bootstrap шаблоны (MIT)"
echo -e "${CYAN}─────────────────────────────────────────────${NC}"

View File

@@ -1,5 +1,5 @@
#!/bin/bash
# GoTelegram v2.2 — Бекап и восстановление конфигурации
# goTelegram Pro v2.5.0 — backup and restore (i18n-aware)
# ── Создание бекапа ──────────────────────────────────────────────────────────
create_backup() {
@@ -7,77 +7,146 @@ create_backup() {
local output_dir="${2:-$BACKUP_DIR}"
local timestamp
timestamp=$(date +%Y%m%d_%H%M%S)
local backup_name="gotelegram_backup_${timestamp}"
local tmp_dir="/tmp/${backup_name}"
local backup_name tmp_dir suffix=0
mkdir -p "$tmp_dir" "$output_dir"
mkdir -p "$output_dir"
while true; do
if [ "$suffix" -eq 0 ]; then
backup_name="gotelegram_backup_${timestamp}"
else
backup_name="gotelegram_backup_${timestamp}_${suffix}"
fi
tmp_dir="/tmp/${backup_name}"
if [ ! -e "$tmp_dir" ] && \
[ ! -e "${output_dir}/${backup_name}.tar.gz" ] && \
[ ! -e "${output_dir}/${backup_name}.tar.gz.enc" ]; then
break
fi
suffix=$((suffix + 1))
done
mkdir -p "$tmp_dir"
# Собираем файлы
log_info "Собираю конфигурацию..."
log_info "$(_t_or backup_collecting 'Собираю конфигурацию...')"
# telemt конфиг
if [ -f "$TELEMT_CONFIG" ]; then
cp "$TELEMT_CONFIG" "$tmp_dir/config.toml"
fi
# GoTelegram конфиг
# goTelegram Pro конфиг
if [ -f "$GOTELEGRAM_CONFIG" ]; then
cp "$GOTELEGRAM_CONFIG" "$tmp_dir/gotelegram.json"
fi
if [ -f "$GOTELEGRAM_DIR/disabled_users.json" ]; then
cp "$GOTELEGRAM_DIR/disabled_users.json" "$tmp_dir/disabled_users.json" 2>/dev/null
fi
if [ -f "$GOTELEGRAM_DIR/backup_schedule.json" ]; then
cp "$GOTELEGRAM_DIR/backup_schedule.json" "$tmp_dir/backup_schedule.json" 2>/dev/null
fi
# Language marker (i18n)
if [ -f "$GOTELEGRAM_DIR/.language" ]; then
cp "$GOTELEGRAM_DIR/.language" "$tmp_dir/.language"
fi
# nginx конфиг (stealth mode)
if [ -f "$NGINX_SITE_CONF" ]; then
cp "$NGINX_SITE_CONF" "$tmp_dir/nginx.conf"
fi
# SSL сертификаты
# SSL сертификаты и renewal metadata для переносов между VPS
local domain
domain=$(config_get domain 2>/dev/null)
if [ -n "$domain" ] && [ -d "/etc/letsencrypt/live/$domain" ]; then
mkdir -p "$tmp_dir/certs"
cp "/etc/letsencrypt/live/$domain/fullchain.pem" "$tmp_dir/certs/" 2>/dev/null
cp "/etc/letsencrypt/live/$domain/privkey.pem" "$tmp_dir/certs/" 2>/dev/null
mkdir -p "$tmp_dir/letsencrypt/live" "$tmp_dir/letsencrypt/archive" "$tmp_dir/letsencrypt/renewal"
cp -a "/etc/letsencrypt/live/$domain" "$tmp_dir/letsencrypt/live/" 2>/dev/null
[ -d "/etc/letsencrypt/archive/$domain" ] && \
cp -a "/etc/letsencrypt/archive/$domain" "$tmp_dir/letsencrypt/archive/" 2>/dev/null
[ -f "/etc/letsencrypt/renewal/$domain.conf" ] && \
cp -a "/etc/letsencrypt/renewal/$domain.conf" "$tmp_dir/letsencrypt/renewal/" 2>/dev/null
log_dim "SSL сертификаты включены"
fi
# Шаблон сайта (если есть)
if [ -d "$WEBSITE_ROOT" ] && [ -f "$WEBSITE_ROOT/index.html" ]; then
mkdir -p "$tmp_dir/site"
cp -r "$WEBSITE_ROOT"/* "$tmp_dir/site/"
log_dim "Шаблон сайта включён"
cp -a "$WEBSITE_ROOT/." "$tmp_dir/site/"
log_dim "$(_t_or backup_site_included 'Шаблон сайта включён')"
fi
# Custom templates and catalog
if [ -d "$GOTELEGRAM_DIR/custom_templates" ]; then
mkdir -p "$tmp_dir/custom_templates"
cp -a "$GOTELEGRAM_DIR/custom_templates/." "$tmp_dir/custom_templates/" 2>/dev/null
fi
if [ -f "$GOTELEGRAM_DIR/templates_catalog.json" ]; then
cp "$GOTELEGRAM_DIR/templates_catalog.json" "$tmp_dir/templates_catalog.json" 2>/dev/null
fi
# Bot state (.env has BotFather token, so encrypted backups are strongly recommended)
if [ -d "$BOT_DIR" ]; then
mkdir -p "$tmp_dir/bot"
[ -f "$BOT_DIR/.env" ] && cp "$BOT_DIR/.env" "$tmp_dir/bot/.env" 2>/dev/null
[ -f "$BOT_DIR/i18n.py" ] && cp "$BOT_DIR/i18n.py" "$tmp_dir/bot/i18n.py" 2>/dev/null
[ -d "$BOT_DIR/lang" ] && cp -a "$BOT_DIR/lang" "$tmp_dir/bot/" 2>/dev/null
fi
# Local web admin state
if [ -d "$ADMIN_WEB_DIR" ]; then
mkdir -p "$tmp_dir/admin_web"
[ -f "$ADMIN_WEB_DIR/server.py" ] && cp "$ADMIN_WEB_DIR/server.py" "$tmp_dir/admin_web/server.py" 2>/dev/null
[ -d "$ADMIN_WEB_DIR/static" ] && cp -a "$ADMIN_WEB_DIR/static" "$tmp_dir/admin_web/" 2>/dev/null
fi
# Traffic history
if [ -f "$GOTELEGRAM_DIR/stats_history.csv" ]; then
cp "$GOTELEGRAM_DIR/stats_history.csv" "$tmp_dir/stats_history.csv" 2>/dev/null
fi
if [ -f "$GOTELEGRAM_DIR/user_stats_history.csv" ]; then
cp "$GOTELEGRAM_DIR/user_stats_history.csv" "$tmp_dir/user_stats_history.csv" 2>/dev/null
fi
if [ -f "$GOTELEGRAM_DIR/shared-443.json" ]; then
cp "$GOTELEGRAM_DIR/shared-443.json" "$tmp_dir/shared-443.json" 2>/dev/null
fi
# Метаданные
local ip mode engine
local ip mode engine lang port domain
ip=$(get_server_ip)
mode=$(config_get mode 2>/dev/null || echo "unknown")
engine=$(config_get engine 2>/dev/null || echo "telemt")
lang=$(type get_language &>/dev/null && get_language 2>/dev/null || echo "en")
port=$(config_get port 2>/dev/null || echo "443")
# Ensure port is numeric; fall back to 443 if garbage
[[ "$port" =~ ^[0-9]+$ ]] || port=443
domain=$(config_get domain 2>/dev/null || echo "")
cat > "$tmp_dir/metadata.json" << EOMETA
{
"backup_version": "1.0",
"backup_version": "1.6",
"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"
}
EOMETA
# Архивируем
local tar_file="/tmp/${backup_name}.tar.gz"
if ! tar czf "$tar_file" -C /tmp "$backup_name" 2>/dev/null; then
log_error "Ошибка создания архива"
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
@@ -88,13 +157,13 @@ EOMETA
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 "Ошибка шифрования"
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"
@@ -108,7 +177,11 @@ EOMETA
local size
size=$(du -h "$final_file" | cut -f1)
log_success "Бекап создан: $final_file ($size)"
if type tf &>/dev/null; then
log_success "$(tf backup_created_fmt "$final_file" "$size")"
else
log_success "Бекап создан: $final_file ($size)"
fi
echo "$final_file"
return 0
}
@@ -117,9 +190,14 @@ EOMETA
restore_backup() {
local backup_file="$1"
local password="$2"
local assume_yes="$3"
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
@@ -130,14 +208,14 @@ restore_backup() {
local tar_file=""
if echo "$backup_file" | grep -q '\.enc$'; then
if [ -z "$password" ]; then
echo -ne " Введите пароль от бекапа: "
echo -ne " $(_t_or backup_enter_pass 'Введите пароль от бекапа'): "
read -rs password
echo ""
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 "Неверный пароль или повреждённый файл"
log_error "$(_t_or backup_bad_pass 'Неверный пароль или повреждённый файл')"
rm -rf "$tmp_dir" "$tar_file"
return 1
fi
@@ -148,7 +226,7 @@ restore_backup() {
# Распаковываем
tar xzf "$tar_file" -C "$tmp_dir" 2>/dev/null
if [ $? -ne 0 ]; then
log_error "Ошибка распаковки архива"
log_error "$(_t_or backup_extract_err 'Ошибка распаковки архива')"
rm -rf "$tmp_dir"
return 1
fi
@@ -158,20 +236,34 @@ restore_backup() {
backup_dir=$(find "$tmp_dir" -maxdepth 1 -type d -name "gotelegram_backup_*" | head -1)
[ -z "$backup_dir" ] && backup_dir="$tmp_dir"
# Legacy bot backups before v2.5.0 stored absolute paths directly in tar:
# opt/gotelegram/config.json and etc/telemt/config.toml.
if [ ! -f "$backup_dir/config.toml" ] && [ -f "$tmp_dir/etc/telemt/config.toml" ]; then
cp "$tmp_dir/etc/telemt/config.toml" "$backup_dir/config.toml" 2>/dev/null || true
fi
if [ ! -f "$backup_dir/gotelegram.json" ] && [ -f "$tmp_dir/opt/gotelegram/config.json" ]; then
cp "$tmp_dir/opt/gotelegram/config.json" "$backup_dir/gotelegram.json" 2>/dev/null || true
fi
if [ ! -f "$backup_dir/disabled_users.json" ] && [ -f "$tmp_dir/opt/gotelegram/disabled_users.json" ]; then
cp "$tmp_dir/opt/gotelegram/disabled_users.json" "$backup_dir/disabled_users.json" 2>/dev/null || true
fi
# Проверяем метаданные
if [ -f "$backup_dir/metadata.json" ]; then
local bk_version bk_mode bk_ip
local bk_version bk_mode bk_ip bk_lang bk_date
bk_version=$(jq -r '.gotelegram_version // "unknown"' "$backup_dir/metadata.json")
bk_mode=$(jq -r '.mode // "unknown"' "$backup_dir/metadata.json")
bk_ip=$(jq -r '.ip // "unknown"' "$backup_dir/metadata.json")
bk_lang=$(jq -r '.language // "-"' "$backup_dir/metadata.json")
bk_date=$(jq -r '.created_at // "-"' "$backup_dir/metadata.json")
echo ""
echo -e " ${BOLD}${WHITE}📦 Бекап:${NC}"
echo -e " Версия: $bk_version | Режим: $bk_mode | IP: $bk_ip"
echo -e " Дата: $(jq -r '.created_at' "$backup_dir/metadata.json")"
echo -e " ${BOLD}${WHITE}📦 $(_t_or backup_label 'Бекап'):${NC}"
echo -e " $(_t_or backup_version_label 'Версия'): $bk_version | $(_t_or backup_mode_label 'Режим'): $bk_mode | IP: $bk_ip | $(_t_or backup_lang_label 'Язык'): $bk_lang"
echo -e " $(_t_or backup_date_label 'Дата'): $bk_date"
echo ""
fi
if ! confirm "Восстановить конфигурацию? Текущие настройки будут перезаписаны."; then
if [ "$assume_yes" != "yes" ] && ! confirm "$(_t_or backup_confirm_restore 'Восстановить конфигурацию? Текущие настройки будут перезаписаны.')"; then
rm -rf "$tmp_dir"
return 0
fi
@@ -185,14 +277,38 @@ restore_backup() {
mkdir -p /etc/telemt
cp "$backup_dir/config.toml" "$TELEMT_CONFIG"
chmod 600 "$TELEMT_CONFIG"
log_success "telemt конфиг восстановлен"
log_success "$(_t_or backup_restored_telemt 'telemt конфиг восстановлен')"
fi
# Восстанавливаем GoTelegram конфиг
# Восстанавливаем goTelegram Pro конфиг
if [ -f "$backup_dir/gotelegram.json" ]; then
mkdir -p "$GOTELEGRAM_DIR"
cp "$backup_dir/gotelegram.json" "$GOTELEGRAM_CONFIG"
log_success "GoTelegram конфиг восстановлен"
log_success "$(_t_or backup_restored_gotelegram 'GoTelegram конфиг восстановлен')"
fi
if [ -f "$backup_dir/disabled_users.json" ]; then
mkdir -p "$GOTELEGRAM_DIR"
cp "$backup_dir/disabled_users.json" "$GOTELEGRAM_DIR/disabled_users.json"
chmod 600 "$GOTELEGRAM_DIR/disabled_users.json" 2>/dev/null || true
fi
if [ -f "$backup_dir/backup_schedule.json" ]; then
mkdir -p "$GOTELEGRAM_DIR"
cp "$backup_dir/backup_schedule.json" "$GOTELEGRAM_DIR/backup_schedule.json" 2>/dev/null
chmod 600 "$GOTELEGRAM_DIR/backup_schedule.json" 2>/dev/null || true
if command -v jq >/dev/null 2>&1; then
local restored_schedule
restored_schedule=$(jq -r '.frequency // "off"' "$GOTELEGRAM_DIR/backup_schedule.json" 2>/dev/null || echo "off")
case "$restored_schedule" in
off|daily|weekly|monthly) set_backup_schedule "$restored_schedule" >/dev/null 2>&1 || true ;;
esac
fi
fi
# Восстанавливаем language marker (i18n)
if [ -f "$backup_dir/.language" ]; then
mkdir -p "$GOTELEGRAM_DIR"
cp "$backup_dir/.language" "$GOTELEGRAM_DIR/.language"
log_success "$(_t_or backup_restored_lang 'Язык интерфейса восстановлен')"
fi
# Восстанавливаем nginx конфиг
@@ -200,40 +316,90 @@ restore_backup() {
mkdir -p /etc/nginx/sites-available /etc/nginx/sites-enabled
cp "$backup_dir/nginx.conf" "$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
# Восстанавливаем SSL / Let's Encrypt structure
if [ -d "$backup_dir/letsencrypt" ]; then
mkdir -p /etc/letsencrypt/live /etc/letsencrypt/archive /etc/letsencrypt/renewal
[ -d "$backup_dir/letsencrypt/live" ] && cp -a "$backup_dir/letsencrypt/live/." /etc/letsencrypt/live/ 2>/dev/null
[ -d "$backup_dir/letsencrypt/archive" ] && cp -a "$backup_dir/letsencrypt/archive/." /etc/letsencrypt/archive/ 2>/dev/null
[ -d "$backup_dir/letsencrypt/renewal" ] && cp -a "$backup_dir/letsencrypt/renewal/." /etc/letsencrypt/renewal/ 2>/dev/null
log_success "$(_t_or backup_restored_ssl 'SSL сертификаты восстановлены')"
elif [ -d "$backup_dir/certs" ]; then
local domain
domain=$(config_get domain 2>/dev/null)
if [ -n "$domain" ]; then
local cert_dir="/etc/letsencrypt/live/$domain"
mkdir -p "$cert_dir"
cp "$backup_dir/certs/"* "$cert_dir/" 2>/dev/null
log_success "SSL сертификаты восстановлены"
log_success "$(_t_or backup_restored_ssl 'SSL сертификаты восстановлены')"
fi
fi
# Восстанавливаем шаблон сайта
if [ -d "$backup_dir/site" ]; then
mkdir -p "$WEBSITE_ROOT"
cp -r "$backup_dir/site"/* "$WEBSITE_ROOT/"
cp -a "$backup_dir/site/." "$WEBSITE_ROOT/"
chown -R www-data:www-data "$WEBSITE_ROOT" 2>/dev/null
log_success "Шаблон сайта восстановлен"
log_success "$(_t_or backup_restored_site 'Шаблон сайта восстановлен')"
fi
# Восстанавливаем custom templates/catalog/statistics
if [ -d "$backup_dir/custom_templates" ]; then
mkdir -p "$GOTELEGRAM_DIR/custom_templates"
cp -a "$backup_dir/custom_templates/." "$GOTELEGRAM_DIR/custom_templates/" 2>/dev/null
log_success "Пользовательские шаблоны восстановлены"
fi
if [ -f "$backup_dir/templates_catalog.json" ]; then
cp "$backup_dir/templates_catalog.json" "$GOTELEGRAM_DIR/templates_catalog.json" 2>/dev/null
fi
if [ -f "$backup_dir/stats_history.csv" ]; then
cp "$backup_dir/stats_history.csv" "$GOTELEGRAM_DIR/stats_history.csv" 2>/dev/null
log_success "История статистики восстановлена"
fi
if [ -f "$backup_dir/user_stats_history.csv" ]; then
cp "$backup_dir/user_stats_history.csv" "$GOTELEGRAM_DIR/user_stats_history.csv" 2>/dev/null
log_success "История статистики пользователей восстановлена"
fi
if [ -f "$backup_dir/shared-443.json" ]; then
cp "$backup_dir/shared-443.json" "$GOTELEGRAM_DIR/shared-443.json" 2>/dev/null
fi
# Восстанавливаем состояние бота
if [ -d "$backup_dir/bot" ]; then
mkdir -p "$BOT_DIR"
[ -f "$backup_dir/bot/.env" ] && cp "$backup_dir/bot/.env" "$BOT_DIR/.env" 2>/dev/null && chmod 600 "$BOT_DIR/.env"
[ -d "$backup_dir/bot/lang" ] && cp -a "$backup_dir/bot/lang" "$BOT_DIR/" 2>/dev/null
[ -f "$backup_dir/bot/i18n.py" ] && cp "$backup_dir/bot/i18n.py" "$BOT_DIR/i18n.py" 2>/dev/null
log_success "Конфигурация Telegram-бота восстановлена"
fi
# Восстанавливаем состояние локальной web-админки
if [ -d "$backup_dir/admin_web" ]; then
mkdir -p "$ADMIN_WEB_DIR"
[ -f "$backup_dir/admin_web/server.py" ] && cp "$backup_dir/admin_web/server.py" "$ADMIN_WEB_DIR/server.py" 2>/dev/null
[ -d "$backup_dir/admin_web/static" ] && cp -a "$backup_dir/admin_web/static" "$ADMIN_WEB_DIR/" 2>/dev/null
rm -f "$ADMIN_WEB_DIR/token" 2>/dev/null || true
log_success "Конфигурация web-админки восстановлена"
fi
# Запускаем сервисы
if is_telemt_installed && [ ! -f "/etc/systemd/system/${TELEMT_SERVICE}.service" ]; then
install_telemt_service
fi
if is_telemt_installed; then
start_telemt
fi
systemctl start nginx 2>/dev/null
command -v nginx &>/dev/null && systemctl start nginx 2>/dev/null
systemctl restart gotelegram-bot 2>/dev/null || true
systemctl restart gotelegram-admin 2>/dev/null || true
# Очистка
rm -rf "$tmp_dir"
[ "$tar_file" != "$backup_file" ] && rm -f "$tar_file"
log_success "Восстановление завершено!"
log_success "$(_t_or backup_restore_done 'Восстановление завершено!')"
show_proxy_info
return 0
}
@@ -241,16 +407,16 @@ restore_backup() {
# ── Список бекапов ───────────────────────────────────────────────────────────
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 " ${BOLD}${WHITE}📦 $(_t_or backup_list_title 'Доступные бекапы'):${NC}"
echo -e " ${DIM}$(printf '─%.0s' {1..60})${NC}"
local i=1
for f in "$BACKUP_DIR"/gotelegram_backup_*.tar.gz*; do
for f in "$BACKUP_DIR"/*.tar.gz*; do
[ -f "$f" ] || continue
[[ "$f" == *.sha256 ]] && continue
local size date_str name
@@ -269,38 +435,120 @@ list_backups() {
cleanup_old_backups() {
local keep="${1:-5}"
local count
count=$(find "$BACKUP_DIR" -name "gotelegram_backup_*.tar.gz*" ! -name "*.sha256" 2>/dev/null | wc -l)
count=$(find "$BACKUP_DIR" -name "*.tar.gz*" ! -name "*.sha256" 2>/dev/null | wc -l)
if [ "$count" -gt "$keep" ]; then
local to_delete=$((count - keep))
find "$BACKUP_DIR" -name "gotelegram_backup_*.tar.gz*" ! -name "*.sha256" 2>/dev/null | sort | head -n "$to_delete" | while read -r f; do
find "$BACKUP_DIR" -name "*.tar.gz*" ! -name "*.sha256" 2>/dev/null | sort | head -n "$to_delete" | while read -r f; do
rm -f "$f" "${f}.sha256"
done
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
}
# ── Расписание бекапов ───────────────────────────────────────────────────────
backup_schedule_calendar() {
case "${1:-off}" in
off) echo "" ;;
daily) echo "*-*-* 03:20:00" ;;
weekly) echo "Sun 03:20:00" ;;
monthly) echo "*-*-01 03:20:00" ;;
*) return 1 ;;
esac
}
set_backup_schedule() {
local frequency="${1:-off}"
local calendar
if ! calendar=$(backup_schedule_calendar "$frequency"); then
log_error "Unsupported backup schedule: $frequency"
return 1
fi
mkdir -p "$GOTELEGRAM_DIR" "$BACKUP_DIR"
if [ "$frequency" = "off" ]; then
systemctl disable --now gotelegram-backup.timer >/dev/null 2>&1 || true
rm -f /etc/systemd/system/gotelegram-backup.timer /etc/systemd/system/gotelegram-backup.service
systemctl daemon-reload >/dev/null 2>&1 || true
else
cat > /etc/systemd/system/gotelegram-backup.service << 'EOSVC'
[Unit]
Description=goTelegram Pro backup
Wants=network-online.target
After=network-online.target
[Service]
Type=oneshot
Environment=GOTELEGRAM_BACKUP_KEEP=30
ExecStart=/bin/bash -lc 'source /opt/gotelegram/lib/common.sh; source /opt/gotelegram/lib/i18n.sh; source /opt/gotelegram/lib/telemt.sh; source /opt/gotelegram/lib/website.sh; source /opt/gotelegram/lib/backup.sh; load_language "$(detect_language 2>/dev/null || echo en)"; create_backup ""; cleanup_old_backups "${GOTELEGRAM_BACKUP_KEEP:-30}"'
EOSVC
cat > /etc/systemd/system/gotelegram-backup.timer << EOTIMER
[Unit]
Description=goTelegram Pro scheduled backup
[Timer]
OnCalendar=$calendar
Persistent=true
RandomizedDelaySec=15m
Unit=gotelegram-backup.service
[Install]
WantedBy=timers.target
EOTIMER
systemctl daemon-reload >/dev/null 2>&1 || return 1
systemctl enable --now gotelegram-backup.timer >/dev/null 2>&1 || return 1
fi
cat > "$GOTELEGRAM_DIR/backup_schedule.json" << EOSCHEDULE
{
"frequency": "$frequency",
"calendar": "$calendar",
"keep": 30,
"updated_at": "$(date -Iseconds)"
}
EOSCHEDULE
chmod 600 "$GOTELEGRAM_DIR/backup_schedule.json" 2>/dev/null || true
log_success "Backup schedule: $frequency"
echo "$frequency"
}
backup_schedule_status() {
local frequency="off" calendar=""
if [ -f "$GOTELEGRAM_DIR/backup_schedule.json" ] && command -v jq >/dev/null 2>&1; then
frequency=$(jq -r '.frequency // "off"' "$GOTELEGRAM_DIR/backup_schedule.json" 2>/dev/null || echo "off")
calendar=$(jq -r '.calendar // ""' "$GOTELEGRAM_DIR/backup_schedule.json" 2>/dev/null || echo "")
fi
echo "frequency=$frequency calendar=$calendar"
systemctl list-timers gotelegram-backup.timer --no-pager 2>/dev/null || true
}
# ── Интерактивный бекап ──────────────────────────────────────────────────────
interactive_backup() {
echo ""
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
@@ -313,13 +561,13 @@ interactive_backup() {
interactive_restore() {
list_backups || return 1
echo -ne " Номер бекапа (или путь к файлу): "
echo -ne " $(_t_or backup_pick_prompt 'Номер бекапа (или путь к файлу)'): "
read -r choice
local backup_file=""
if [[ "$choice" =~ ^[0-9]+$ ]]; then
local i=1
for f in "$BACKUP_DIR"/gotelegram_backup_*.tar.gz*; do
for f in "$BACKUP_DIR"/*.tar.gz*; do
[ -f "$f" ] || continue
[[ "$f" == *.sha256 ]] && continue
if [ "$i" -eq "$choice" ]; then
@@ -333,7 +581,7 @@ interactive_restore() {
fi
if [ -z "$backup_file" ]; then
log_error "Бекап не найден"
log_error "$(_t_or backup_not_found 'Бекап не найден')"
return 1
fi

View File

@@ -1,10 +1,10 @@
#!/bin/bash
# GoTelegram v2.3 — Общие утилиты
# Цвета, логирование, спиннер, системные функции, совместимость с v1
# goTelegram Pro v2.5.0 — common utilities
# Colors, logging, spinner, system helpers, v1 compat, i18n-aware
# ── Версия ────────────────────────────────────────────────────────────────────
GOTELEGRAM_VERSION="2.3.0"
GOTELEGRAM_NAME="GoTelegram"
# ── Version ───────────────────────────────────────────────────────────────────
GOTELEGRAM_VERSION="2.5.0"
GOTELEGRAM_NAME="goTelegram Pro"
# ── Пути ──────────────────────────────────────────────────────────────────────
GOTELEGRAM_DIR="/opt/gotelegram"
@@ -18,6 +18,10 @@ WEBSITE_ROOT="/var/www/gotelegram-site"
BACKUP_DIR="$GOTELEGRAM_DIR/backups"
LOG_FILE="/var/log/gotelegram.log"
BOT_DIR="/opt/gotelegram-bot"
ADMIN_WEB_DIR="/opt/gotelegram-admin"
ADMIN_WEB_SERVICE="gotelegram-admin"
ADMIN_WEB_HOST="127.0.0.1"
ADMIN_WEB_PORT="1984"
# ── V1 совместимость ─────────────────────────────────────────────────────────
V1_CONTAINER_NAME="mtproto-proxy"
@@ -49,10 +53,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 +101,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 +112,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 Pro v${GOTELEGRAM_VERSION}${NC}"
echo -e " ${DIM}MTProxy powered by telemt (Rust + Tokio)${NC}"
echo -e " ${DIM}Anti-DPI • Fake TLS • TCP Splice • JA3/JA4${NC}"
fi
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 +182,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 +244,217 @@ 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" ;;
iptables) echo "iptables" ;;
*) 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" ;;
iptables) echo "iptables" ;;
*) 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 iptables)
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 +469,63 @@ check_port() {
return 1 # свободен
}
detect_3xui() {
if systemctl list-unit-files 2>/dev/null | grep -Eq '^(x-ui|3x-ui)\.service'; then
return 0
fi
[ -d /etc/x-ui ] || [ -d /usr/local/x-ui ] || [ -f /etc/x-ui/x-ui.db ]
}
detect_3xui_443_listener() {
ss -ltnp 2>/dev/null | grep -E '(:|])443[[:space:]]' | grep -Eiq '(xray|x-ui|3x-ui)'
}
warn_3xui_443_conflict() {
detect_3xui_443_listener || return 1
log_warning "Обнаружен 3x-ui/Xray, который уже слушает TCP/443."
log_warning "goTelegram Pro не будет молча останавливать или переписывать 3x-ui."
log_dim "Для настоящего shared-443 нужен один фронтовой TLS/SNI-диспетчер и разные SNI-домены для Xray и goTelegram Pro."
mkdir -p "$GOTELEGRAM_DIR" 2>/dev/null
cat > "$GOTELEGRAM_DIR/shared-443-3xui.md" <<'EOF' 2>/dev/null || true
# goTelegram Pro + 3x-ui on one TCP/443
goTelegram Pro detected that 3x-ui/Xray already owns TCP/443. Two independent
processes cannot bind the same IP:port at the same time. A safe shared setup
needs one front TLS/SNI dispatcher on 443 and internal backends, for example:
- dispatcher: 0.0.0.0:443 (nginx stream ssl_preread)
- goTelegram Pro telemt: 127.0.0.1:7443
- 3x-ui/Xray inbound: 127.0.0.1:9443
- goTelegram Pro nginx mask site: 127.0.0.1:8443
The dispatcher routes Xray SNI domains to Xray. Everything else goes to telemt;
telemt then decides whether the session is MTProxy or regular HTTPS and forwards
the website to nginx through dns_overrides.
goTelegram Pro can generate the dispatcher with:
source /opt/gotelegram/lib/shared443.sh
shared443_enable <gotelegram-domain> <xray-sni-domain> 127.0.0.1:9443
Move the 3x-ui/Xray inbound from 0.0.0.0:443 to 127.0.0.1:9443 in the panel first,
or nginx will not be able to own the public 443 socket. goTelegram Pro intentionally
does not rewrite the 3x-ui SQLite database or generated Xray config without explicit
operator confirmation, because 3x-ui can overwrite manual JSON edits on the next
panel change.
EOF
return 0
}
check_disk_space() {
local min_mb="${1:-500}"
local avail_mb
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 +534,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 +546,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 +566,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 +630,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 +640,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 +713,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
View File

@@ -0,0 +1,140 @@
#!/bin/bash
# GoTelegram v2.5.0 — 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
}

377
lib/lang/en.sh Executable file
View File

@@ -0,0 +1,377 @@
#!/bin/bash
# goTelegram Pro v2.5.0 — 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 Pro v%s"
I18N[banner_subtitle]="MTProxy powered by telemt (Rust + Tokio)"
I18N[banner_features]="Anti-DPI • Fake TLS • TCP Splice • JA3/JA4"
I18N[credits_title]="Credits / Thanks"
# ── 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."
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 Pro:"
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 Pro v%s installed! (%s mode)"
I18N[install_arch_desc1]="telemt accepts all traffic on 443 (HTTPS masquerade)"
I18N[install_arch_desc2]="nginx serves the site on internal port %s"
I18N[install_arch_desc3]="ISP only sees HTTPS traffic to %s:443"
# ── 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 Pro 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 Pro"
I18N[remove_proxy_only]="Remove proxy only (telemt)"
I18N[remove_bot_only]="Remove Telegram bot only"
I18N[remove_all]="Remove everything (proxy + bot + settings)"
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 Pro 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_youtube_title]="▶ YouTube Channel"
I18N[promo_link_label]="Link:"
I18N[promo_off60]="60%% discount on the first month"
I18N[promo_ant20]="20%% + 3%% when paid for 3 months"
I18N[promo_ant6]="15%% + 5%% when paid for 6 months"
I18N[promo_qr_host1]="── QR: Hosting #1 ──"
I18N[promo_qr_host2]="── QR: Hosting #2 ──"
I18N[promo_qr_tips]="── QR: Donate / Tips ──"
I18N[promo_qr_youtube]="── QR: YouTube Channel ──"
I18N[promo_menu_in]="Menu in %d sec..."
# ── Stats ───────────────────────────────────────────────────────────────
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 Pro 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"
# ── 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 Pro v1 (mtg) installation detected"
I18N[v1_container]="Container: %s"
I18N[v1_migration_step]="Migrating from v1 (mtg) to v2 (telemt)"
I18N[v1_found_title]="Found v1 (mtg) installation:"
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."

377
lib/lang/ru.sh Executable file
View File

@@ -0,0 +1,377 @@
#!/bin/bash
# goTelegram Pro v2.5.0 — 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 Pro v%s"
I18N[banner_subtitle]="MTProxy на ядре telemt (Rust + Tokio)"
I18N[banner_features]="Anti-DPI • Fake TLS • TCP Splice • JA3/JA4"
I18N[credits_title]="Благодарности / Credits"
# ── 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."
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 Pro:"
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 Pro 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 Pro 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 Pro"
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 Pro полностью удалён (прокси + бот)"
# ── 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_youtube_title]="▶ YouTube-канал"
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_qr_youtube]="── QR: YouTube-канал ──"
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 Pro конфиг восстановлен"
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]="Бекап не найден"
# ── 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 Pro 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 освобождён."

248
lib/shared443.sh Normal file
View File

@@ -0,0 +1,248 @@
#!/bin/bash
# goTelegram Pro v2.5.0 — shared TCP/443 dispatcher helpers
SHARED443_CONFIG="${SHARED443_CONFIG:-/opt/gotelegram/shared-443.json}"
SHARED443_STREAM_CONF="${SHARED443_STREAM_CONF:-/etc/nginx/stream-conf.d/gotelegram-shared443.conf}"
SHARED443_TELEMT_PORT="${SHARED443_TELEMT_PORT:-7443}"
SHARED443_PUBLIC_PORT="${SHARED443_PUBLIC_PORT:-443}"
SHARED443_LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
type log_error >/dev/null 2>&1 || source "$SHARED443_LIB_DIR/common.sh"
type install_nginx >/dev/null 2>&1 || source "$SHARED443_LIB_DIR/website.sh"
shared443_detect_nginx_stream() {
nginx -V 2>&1 | grep -Eq -- '--with-stream|ngx_stream_module|ngx_stream_ssl_preread_module'
}
shared443_install_stream_module() {
if shared443_detect_nginx_stream; then
return 0
fi
case "$(get_pkg_manager 2>/dev/null || echo unknown)" in
apt)
apt_update >/dev/null 2>&1 || true
apt_install libnginx-mod-stream || return 1
;;
dnf|yum)
install_pkg nginx-mod-stream || true
;;
esac
shared443_detect_nginx_stream
}
shared443_ensure_nginx_include() {
mkdir -p /etc/nginx/stream-conf.d
if nginx -T 2>/dev/null | grep -q '/etc/nginx/stream-conf.d/\*.conf'; then
return 0
fi
if grep -Eq '^[[:space:]]*stream[[:space:]]*\{' /etc/nginx/nginx.conf 2>/dev/null; then
log_warning "В nginx уже есть stream-блок, но нет include /etc/nginx/stream-conf.d/*.conf"
log_dim "Добавьте include вручную или перенесите $SHARED443_STREAM_CONF в существующий stream-блок."
return 1
fi
cp /etc/nginx/nginx.conf "/etc/nginx/nginx.conf.gotelegram.$(date +%Y%m%d_%H%M%S).bak" 2>/dev/null || true
cat >> /etc/nginx/nginx.conf <<'EOF'
# goTelegram Pro shared TCP/443 routes
stream {
include /etc/nginx/stream-conf.d/*.conf;
}
EOF
}
shared443_rewrite_telemt_bind() {
local listen_port="${1:-$SHARED443_TELEMT_PORT}"
local public_port="${2:-$SHARED443_PUBLIC_PORT}"
local listen_addr="${3:-127.0.0.1}"
command -v python3 >/dev/null 2>&1 || {
log_error "python3 нужен для безопасного изменения $TELEMT_CONFIG"
return 1
}
python3 - "$TELEMT_CONFIG" "$listen_port" "$public_port" "$listen_addr" <<'PY'
import sys
from pathlib import Path
path = Path(sys.argv[1])
listen_port = sys.argv[2]
public_port = sys.argv[3]
listen_addr = sys.argv[4]
lines = path.read_text(encoding="utf-8", errors="ignore").splitlines() if path.exists() else []
out = []
section = ""
server_seen = False
server_port_seen = False
server_addr_seen = False
links_seen = False
public_seen = False
def flush_section(next_line=None):
global section, server_port_seen, server_addr_seen, public_seen
if section == "server":
if not server_port_seen:
out.append(f"port = {listen_port}")
if not server_addr_seen:
out.append(f'listen_addr_ipv4 = "{listen_addr}"')
if section == "general.links" and not public_seen:
out.append(f"public_port = {public_port}")
if next_line is not None:
out.append(next_line)
for raw in lines:
stripped = raw.strip()
if stripped.startswith("[") and stripped.endswith("]"):
flush_section(raw)
section = stripped.strip("[]")
if section == "server":
server_seen = True
server_port_seen = False
server_addr_seen = False
elif section == "general.links":
links_seen = True
public_seen = False
continue
if section == "server" and stripped.startswith("port") and "=" in stripped:
out.append(f"port = {listen_port}")
server_port_seen = True
continue
if section == "server" and stripped.startswith("listen_addr_ipv4") and "=" in stripped:
out.append(f'listen_addr_ipv4 = "{listen_addr}"')
server_addr_seen = True
continue
if section == "general.links" and stripped.startswith("public_port") and "=" in stripped:
out.append(f"public_port = {public_port}")
public_seen = True
continue
out.append(raw)
flush_section()
if not links_seen:
if out and out[-1].strip():
out.append("")
out.extend(["[general.links]", f"public_port = {public_port}"])
if not server_seen:
if out and out[-1].strip():
out.append("")
out.extend(["[server]", f"port = {listen_port}", f'listen_addr_ipv4 = "{listen_addr}"'])
tmp = path.with_suffix(path.suffix + ".tmp")
tmp.write_text("\n".join(out).rstrip() + "\n", encoding="utf-8")
tmp.chmod(0o600)
tmp.replace(path)
PY
}
shared443_write_stream_config() {
local domain="$1"
local xray_domain="${2:-}"
local xray_target="${3:-}"
local telemt_target="${4:-127.0.0.1:${SHARED443_TELEMT_PORT}}"
mkdir -p "$(dirname "$SHARED443_STREAM_CONF")"
{
echo "# goTelegram Pro shared TCP/443 dispatcher"
echo "# Browser/Telegram for goTelegram domain goes to telemt; telemt masks the site to nginx."
echo "map \$ssl_preread_server_name \$gotelegram_shared443_backend {"
echo " hostnames;"
if [[ -n "$xray_domain" && -n "$xray_target" ]]; then
echo " ${xray_domain} ${xray_target};"
fi
echo " default ${telemt_target};"
echo "}"
echo ""
echo "server {"
echo " listen 0.0.0.0:${SHARED443_PUBLIC_PORT};"
echo " proxy_pass \$gotelegram_shared443_backend;"
echo " ssl_preread on;"
echo " proxy_connect_timeout 5s;"
echo " proxy_timeout 10m;"
echo "}"
} > "$SHARED443_STREAM_CONF"
mkdir -p "$(dirname "$SHARED443_CONFIG")"
if command -v jq >/dev/null 2>&1; then
jq -n \
--arg domain "$domain" \
--arg telemt "$telemt_target" \
--arg xdomain "$xray_domain" \
--arg xtarget "$xray_target" \
--arg updated "$(date -Iseconds)" \
--argjson public_port "$SHARED443_PUBLIC_PORT" \
'{
enabled: true,
dispatcher: "nginx-stream",
public_port: $public_port,
domain: $domain,
telemt_target: $telemt,
site_target: "127.0.0.1:8443",
xray_routes: (if ($xdomain != "" and $xtarget != "") then [{public: ($xdomain + ":443"), target: $xtarget}] else [] end),
updated_at: $updated
}' > "$SHARED443_CONFIG"
else
cat > "$SHARED443_CONFIG" <<EOF
{"enabled":true,"dispatcher":"nginx-stream","public_port":${SHARED443_PUBLIC_PORT},"domain":"${domain}","telemt_target":"${telemt_target}","site_target":"127.0.0.1:8443","xray_routes":[],"updated_at":"$(date -Iseconds)"}
EOF
fi
chmod 600 "$SHARED443_CONFIG" 2>/dev/null || true
}
shared443_enable() {
local domain="$1"
local xray_domain="${2:-}"
local xray_target="${3:-}"
local telemt_target="127.0.0.1:${SHARED443_TELEMT_PORT}"
[[ -n "$domain" ]] || domain="$(config_get domain 2>/dev/null || echo "")"
[[ -n "$domain" ]] || {
log_error "Не указан домен goTelegram Pro для shared-443"
return 1
}
install_nginx || return 1
shared443_install_stream_module || {
log_error "nginx stream/ssl_preread недоступен"
return 1
}
shared443_ensure_nginx_include || return 1
shared443_rewrite_telemt_bind "$SHARED443_TELEMT_PORT" "$SHARED443_PUBLIC_PORT" "127.0.0.1" || return 1
systemctl restart "$TELEMT_SERVICE" 2>/dev/null || true
shared443_write_stream_config "$domain" "$xray_domain" "$xray_target" "$telemt_target"
if nginx -t 2>/dev/null; then
systemctl restart nginx
log_success "shared-443 включён: 0.0.0.0:${SHARED443_PUBLIC_PORT} -> nginx stream -> telemt ${telemt_target}"
if [[ -n "$xray_domain" && -n "$xray_target" ]]; then
log_success "Xray route: ${xray_domain}:443 -> ${xray_target}"
fi
else
log_error "nginx -t не прошёл после настройки shared-443"
nginx -t
return 1
fi
}
shared443_detect_direct_conflict() {
ss -ltnp 2>/dev/null | grep -E '(:|])443[[:space:]]' | grep -Eiv '(nginx|telemt)' || true
}
shared443_status() {
echo "shared-443 config: $SHARED443_CONFIG"
[ -f "$SHARED443_CONFIG" ] && cat "$SHARED443_CONFIG" || echo "not enabled"
local conflict
conflict="$(shared443_detect_direct_conflict)"
if [[ -n "$conflict" ]]; then
echo ""
echo "direct 443 listeners that need migration behind dispatcher:"
echo "$conflict"
fi
}
export -f shared443_detect_nginx_stream shared443_install_stream_module shared443_ensure_nginx_include
export -f shared443_rewrite_telemt_bind shared443_write_stream_config shared443_enable
export -f shared443_detect_direct_conflict shared443_status

214
lib/stats.sh Executable file → Normal file
View File

@@ -1,5 +1,5 @@
#!/bin/bash
# stats.sh — Traffic statistics module for GoTelegram
# stats.sh — Traffic statistics module for GoTelegram v2.5.0
# Tracks proxy (telemt port 443) and site (nginx port 8443) traffic
# Uses iptables counters + real-time snapshots + historical CSV
@@ -12,12 +12,24 @@ NC='\033[0m' # No Color
STATS_DIR="/run/gotelegram"
HISTORY_FILE="/opt/gotelegram/stats_history.csv"
USER_HISTORY_FILE="/opt/gotelegram/user_stats_history.csv"
SNAPSHOTS_DIR="$STATS_DIR/snapshots"
CURRENT_SNAPSHOT="$STATS_DIR/stats_current.json"
CONFIG_FILE="/opt/gotelegram/config.json"
TELEMT_CONFIG_FILE="/etc/telemt/config.toml"
STATS_RETENTION_DAYS="${STATS_RETENTION_DAYS:-365}"
STATS_MINUTE_RETENTION_DAYS="${STATS_MINUTE_RETENTION_DAYS:-31}"
STATS_CLEANUP_INTERVAL="${STATS_CLEANUP_INTERVAL:-3600}"
STATS_CLEANUP_STAMP="$STATS_DIR/last_history_cleanup"
USER_STATS_COLLECT_STAMP="$STATS_DIR/last_user_stats_minute"
# Initialize stats infrastructure
stats_init() {
if ! command -v iptables &>/dev/null; then
log_warning "iptables не найден: установите пакет iptables или запустите установку зависимостей"
return 1
fi
# Create runtime directory
mkdir -p "$STATS_DIR" "$SNAPSHOTS_DIR" 2>/dev/null
chmod 755 "$STATS_DIR" "$SNAPSHOTS_DIR" 2>/dev/null
@@ -46,6 +58,9 @@ stats_init() {
if [[ ! -f "$HISTORY_FILE" ]]; then
echo "epoch,proxy_bytes,site_bytes" > "$HISTORY_FILE" 2>/dev/null
fi
if [[ ! -f "$USER_HISTORY_FILE" ]]; then
echo "epoch,user,total_octets,current_connections,active_unique_ips,recent_unique_ips" > "$USER_HISTORY_FILE" 2>/dev/null
fi
# Write initial snapshot
stats_collect
@@ -57,6 +72,13 @@ stats_collect() {
local ts=$(date +%s)
local temp_file=$(mktemp)
if ! command -v iptables &>/dev/null; then
mkdir -p "$STATS_DIR" 2>/dev/null
echo "{\"ts\":$ts,\"proxy_bytes\":0,\"proxy_pkts\":0,\"site_bytes\":0,\"site_pkts\":0,\"error\":\"iptables_missing\"}" > "$CURRENT_SNAPSHOT" 2>/dev/null
rm -f "$temp_file" 2>/dev/null
return 1
fi
# Parse iptables output: format is "pkts bytes target"
# We need to extract bytes (2nd column) for each rule
local iptables_output=$(iptables -L GOTELEGRAM_STATS -v -n -x 2>/dev/null)
@@ -91,6 +113,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)
@@ -100,14 +129,76 @@ EOF
if [[ "$last_ts" -eq 0 ]] || [[ $((current_minute - last_ts)) -ge 60 ]]; then
echo "$current_minute,$proxy_bytes,$site_bytes" >> "$HISTORY_FILE" 2>/dev/null
# Cleanup old entries (keep only 365 days)
# Cleanup/compact history at most once per hour.
stats_cleanup_history
fi
fi
stats_collect_users "$ts"
rm -f "$temp_file" 2>/dev/null
}
# Print active telemt usernames from [access.users]. Usernames are restricted by
# goTelegram to A-Z/a-z/0-9/_.- so they are safe in URLs and CSV fields.
stats_active_users() {
[[ -f "$TELEMT_CONFIG_FILE" ]] || return 0
awk '
/^\[access\.users\]$/ { in_users=1; next }
in_users && /^\[/ { exit }
in_users && /^[[:space:]]*#/ { next }
in_users && /=/ {
key=$1
gsub(/^[[:space:]]+|[[:space:]]+$/, "", key)
gsub(/^"|"$/, "", key)
if (key ~ /^[A-Za-z0-9_.-]{1,48}$/) print key
}
' "$TELEMT_CONFIG_FILE" 2>/dev/null
}
stats_collect_users() {
local ts="${1:-$(date +%s)}"
local current_minute=$((ts - (ts % 60)))
mkdir -p "$(dirname "$USER_HISTORY_FILE")" 2>/dev/null
if [[ ! -f "$USER_HISTORY_FILE" ]]; then
echo "epoch,user,total_octets,current_connections,active_unique_ips,recent_unique_ips" > "$USER_HISTORY_FILE" 2>/dev/null
fi
command -v curl &>/dev/null || return 0
command -v jq &>/dev/null || return 0
if [[ -f "$USER_STATS_COLLECT_STAMP" ]] && [[ "$(cat "$USER_STATS_COLLECT_STAMP" 2>/dev/null)" == "$current_minute" ]]; then
return 0
fi
local existing_users=""
existing_users=$(awk -F, -v ts="$current_minute" '$1 == ts { print $2 }' "$USER_HISTORY_FILE" 2>/dev/null || true)
local user payload total conns active_ips recent_ips
while IFS= read -r user; do
[[ -n "$user" ]] || continue
if printf '%s\n' "$existing_users" | grep -Fxq "$user"; then
continue
fi
payload=$(curl -sS --max-time 2 "http://127.0.0.1:9091/v1/users/${user}" 2>/dev/null || true)
[[ -n "$payload" ]] || continue
total=$(echo "$payload" | jq -r '.data.total_octets // .total_octets // 0' 2>/dev/null)
conns=$(echo "$payload" | jq -r '.data.current_connections // .current_connections // 0' 2>/dev/null)
active_ips=$(echo "$payload" | jq -r '.data.active_unique_ips // .active_unique_ips // 0' 2>/dev/null)
recent_ips=$(echo "$payload" | jq -r '.data.recent_unique_ips // .recent_unique_ips // 0' 2>/dev/null)
[[ "$total" =~ ^[0-9]+$ ]] || total=0
[[ "$conns" =~ ^[0-9]+$ ]] || conns=0
[[ "$active_ips" =~ ^[0-9]+$ ]] || active_ips=0
[[ "$recent_ips" =~ ^[0-9]+$ ]] || recent_ips=0
echo "$current_minute,$user,$total,$conns,$active_ips,$recent_ips" >> "$USER_HISTORY_FILE" 2>/dev/null
done < <(stats_active_users)
echo "$current_minute" > "$USER_STATS_COLLECT_STAMP" 2>/dev/null || true
stats_cleanup_user_history
}
# Read current snapshot as JSON
stats_read_current() {
if [[ -f "$CURRENT_SNAPSHOT" ]]; then
@@ -270,25 +361,103 @@ show_traffic_stats() {
} >&2
}
# Clean up history older than 365 days
_stats_positive_int() {
local value="${1:-0}"
[[ "$value" =~ ^[0-9]+$ ]] && [[ "$value" -gt 0 ]] && echo "$value" || echo "$2"
}
stats_should_cleanup() {
local stamp="$1"
local now last interval
mkdir -p "$STATS_DIR" 2>/dev/null || true
now=$(date +%s)
interval=$(_stats_positive_int "$STATS_CLEANUP_INTERVAL" 3600)
last=$(cat "$stamp" 2>/dev/null || echo 0)
[[ "$last" =~ ^[0-9]+$ ]] || last=0
if (( now - last < interval )); then
return 1
fi
echo "$now" > "$stamp" 2>/dev/null || true
return 0
}
stats_retention_cutoffs() {
local now retention_days minute_days
now=$(date +%s)
retention_days=$(_stats_positive_int "$STATS_RETENTION_DAYS" 365)
minute_days=$(_stats_positive_int "$STATS_MINUTE_RETENTION_DAYS" 31)
if (( minute_days > retention_days )); then
minute_days="$retention_days"
fi
echo "$((now - retention_days * 86400)) $((now - minute_days * 86400))"
}
# Keep history for at most one year. Recent points stay per-minute; older
# points are compacted to one last cumulative snapshot per hour.
stats_cleanup_history() {
if [[ ! -f "$HISTORY_FILE" ]]; then
return
fi
local now=$(date +%s)
local ts_365d=$((now - 31536000))
local temp_file=$(mktemp)
stats_should_cleanup "$STATS_CLEANUP_STAMP" || return 0
local retention_cutoff minute_cutoff temp_file
read -r retention_cutoff minute_cutoff <<< "$(stats_retention_cutoffs)"
temp_file=$(mktemp)
# Keep header + entries from last 365 days
{
head -1 "$HISTORY_FILE"
awk -F, -v ts="$ts_365d" '$1 >= ts' "$HISTORY_FILE" | tail -n +2
awk -F, -v keep="$retention_cutoff" -v minute="$minute_cutoff" '
BEGIN { OFS="," }
NR == 1 { next }
$1 !~ /^[0-9]+$/ { next }
$1 < keep { next }
$1 >= minute { print $1, $2, $3; next }
{
bucket = int($1 / 3600)
compact[bucket] = $1 OFS $2 OFS $3
}
END {
for (bucket in compact) print compact[bucket]
}
' "$HISTORY_FILE" | sort -t, -k1,1n
} > "$temp_file" 2>/dev/null
mv "$temp_file" "$HISTORY_FILE" 2>/dev/null
}
stats_cleanup_user_history() {
if [[ ! -f "$USER_HISTORY_FILE" ]]; then
return
fi
stats_should_cleanup "${STATS_CLEANUP_STAMP}.users" || return 0
local retention_cutoff minute_cutoff temp_file
read -r retention_cutoff minute_cutoff <<< "$(stats_retention_cutoffs)"
temp_file=$(mktemp)
{
head -1 "$USER_HISTORY_FILE"
awk -F, -v keep="$retention_cutoff" -v minute="$minute_cutoff" '
BEGIN { OFS="," }
NR == 1 { next }
$1 !~ /^[0-9]+$/ { next }
$1 < keep { next }
$1 >= minute { print $1, $2, $3, $4, $5, $6; next }
{
bucket = $2 SUBSEP int($1 / 3600)
compact[bucket] = $1 OFS $2 OFS $3 OFS $4 OFS $5 OFS $6
}
END {
for (bucket in compact) print compact[bucket]
}
' "$USER_HISTORY_FILE" | sort -t, -k1,1n -k2,2
} > "$temp_file" 2>/dev/null
mv "$temp_file" "$USER_HISTORY_FILE" 2>/dev/null
}
# Toggle stats collection on/off
toggle_stats() {
local current_state="false"
@@ -343,6 +512,14 @@ install_stats_collector() {
return 1
fi
if ! command -v iptables &>/dev/null; then
log_info "Установка iptables для подсчёта трафика..."
install_pkg "$(apt_pkg_for_cmd iptables)" || {
echo "Не удалось установить iptables" >&2
return 1
}
fi
# Get script directory (resolve symlinks)
local script_dir=$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")
local lib_dir=$(dirname "$script_dir")
@@ -350,7 +527,7 @@ install_stats_collector() {
# Create systemd service file
cat > "$service_file" <<'EOF'
[Unit]
Description=GoTelegram Traffic Stats Collector
Description=goTelegram Pro Traffic Stats Collector
After=network.target
Wants=network-online.target
@@ -370,7 +547,18 @@ EOF
chmod 644 "$service_file"
systemctl daemon-reload
systemctl enable gotelegram-stats.service
systemctl start gotelegram-stats.service
systemctl restart gotelegram-stats.service
if [[ -f "$CONFIG_FILE" ]] && command -v jq &>/dev/null; then
local tmp
tmp=$(mktemp)
if jq '.stats_enabled = true' "$CONFIG_FILE" > "$tmp" 2>/dev/null; then
mv "$tmp" "$CONFIG_FILE"
chmod 600 "$CONFIG_FILE" 2>/dev/null || true
else
rm -f "$tmp" 2>/dev/null
fi
fi
echo "Сервис gotelegram-stats установлен и запущен" >&2
}
@@ -394,13 +582,13 @@ remove_stats_collector() {
# Clean up directories and files
rm -rf "$STATS_DIR" 2>/dev/null
rm -f "$HISTORY_FILE" 2>/dev/null
rm -f "$HISTORY_FILE" "$USER_HISTORY_FILE" 2>/dev/null
echo "Сервис статистики удалён" >&2
}
# Export functions for external use
export -f stats_init stats_collect stats_read_current stats_calculate_rates
export -f stats_init stats_collect stats_collect_users stats_active_users stats_read_current stats_calculate_rates
export -f show_traffic_stats format_bytes format_rate toggle_stats
export -f stats_cleanup_history install_stats_collector remove_stats_collector
export -f stats_cleanup_history stats_cleanup_user_history stats_should_cleanup stats_retention_cutoffs install_stats_collector remove_stats_collector
export -f json_get

View File

@@ -1,5 +1,5 @@
#!/bin/bash
# GoTelegram v2.2 — Управление telemt binary
# GoTelegram v2.5.0 — Управление telemt binary
# Скачивание, обновление, запуск, остановка через systemd
TELEMT_GITHUB="telemt/telemt"
@@ -159,7 +159,7 @@ install_telemt_service() {
cat > "/etc/systemd/system/${TELEMT_SERVICE}.service" << EOF
[Unit]
Description=GoTelegram MTProxy (telemt engine)
Description=goTelegram Pro MTProxy (telemt engine)
Documentation=https://github.com/telemt/telemt
After=network-online.target
Wants=network-online.target
@@ -187,14 +187,53 @@ 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.
wait_telemt_ready() {
local timeout="${1:-90}"
local port elapsed=0
port=$(awk '
/^\[server\]/ { in_server=1; next }
/^\[/ && in_server { exit }
in_server && $1 == "port" {
sub(/^[^=]*=[[:space:]]*/, "")
gsub(/[[:space:]]/, "")
print
exit
}
' "$TELEMT_CONFIG" 2>/dev/null)
[[ "$port" =~ ^[0-9]+$ ]] || port=443
while [ "$elapsed" -lt "$timeout" ]; do
if ! systemctl is-active --quiet "$TELEMT_SERVICE" 2>/dev/null; then
return 1
fi
if ss -ltnp 2>/dev/null | grep -E ":${port}\b" | grep -q "telemt"; then
return 0
fi
sleep 1
elapsed=$((elapsed + 1))
done
return 1
}
start_telemt() {
systemctl start "$TELEMT_SERVICE" 2>/dev/null
sleep 2
if systemctl is-active --quiet "$TELEMT_SERVICE"; then
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
if wait_telemt_ready 90; then
log_success "telemt запущен"
return 0
else
log_error "telemt не запустился"
log_error "telemt не запустился или не открыл порт"
journalctl -u "$TELEMT_SERVICE" --no-pager -n 10 2>/dev/null
return 1
fi
@@ -211,12 +250,12 @@ stop_telemt() {
restart_telemt() {
systemctl restart "$TELEMT_SERVICE" 2>/dev/null
sleep 2
if systemctl is-active --quiet "$TELEMT_SERVICE"; then
if wait_telemt_ready 90; then
log_success "telemt перезапущен"
return 0
else
log_error "telemt не перезапустился"
log_error "telemt не перезапустился или не открыл порт"
journalctl -u "$TELEMT_SERVICE" --no-pager -n 10 2>/dev/null
return 1
fi
}

View File

@@ -1,5 +1,5 @@
#!/bin/bash
# GoTelegram v2.2 — Генерация TOML конфигурации для telemt
# GoTelegram v2.5.0 — Генерация TOML конфигурации для telemt
# ── Популярные домены (не заблокированные в РФ) ──────────────────────────────
QUICK_DOMAINS=(
@@ -48,15 +48,38 @@ generate_telemt_toml() {
# Сгенерировано: $(date -Iseconds)
# Режим: ${mask_mode}
[general]
use_middle_proxy = true
log_level = "normal"
[general.modes]
classic = false
secure = false
tls = true
[general.links]
show = "*"
public_port = ${port}
[server]
port = ${port}
listen_addr_ipv4 = "0.0.0.0"
metrics_listen = "127.0.0.1:9090"
metrics_whitelist = ["127.0.0.1/32", "::1/128"]
[server.api]
enabled = true
listen = "127.0.0.1:9091"
whitelist = ["127.0.0.1/32", "::1/128"]
minimal_runtime_enabled = false
minimal_runtime_cache_ttl_ms = 1000
[censorship]
tls_domain = "${mask_domain}"
mask = true
mask_port = ${mask_port}
tls_emulation = $([ "$mask_mode" = "pro" ] && echo "false" || echo "true")
unknown_sni_action = "mask"
[access.users]
main = "${secret}"
@@ -81,13 +104,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"
}
@@ -102,25 +129,157 @@ get_config_value() {
case "$key" in
secret)
# [access.users] main = "..."
grep -A5 '\[access.users\]' "$config" | grep -m1 '=' | sed 's/.*=\s*"\(.*\)"/\1/' | tr -d ' '
awk '
/^\[access\.users\]/ { in_users=1; next }
/^\[/ && in_users { exit }
in_users && /^[[:space:]]*[^#[:space:]][^=]*=/ {
user_key=$1
gsub(/"/, "", user_key)
if (user_key != "main") next
value=$0
sub(/^[^=]*=[[:space:]]*/, "", value)
sub(/^"/, "", value)
sub(/".*$/, "", value)
gsub(/[[:space:]]/, "", value)
if (value != "") {
print value
found=1
}
exit
}
END { exit found ? 0 : 1 }
' "$config"
;;
port)
# [server] port = 443
grep -A5 '\[server\]' "$config" | grep 'port\s*=' | head -1 | sed 's/.*=\s*\([0-9]*\)/\1/' | tr -d ' '
awk '
/^\[server\]/ { in_server=1; next }
/^\[/ && in_server { exit }
in_server && $1 == "port" {
sub(/^[^=]*=[[:space:]]*/, "")
gsub(/[[:space:]]/, "")
print
exit
}
' "$config"
;;
mask_host|tls_domain)
# [censorship] tls_domain = "..."
grep -A10 '\[censorship\]' "$config" | grep 'tls_domain\s*=' | sed 's/.*=\s*"\(.*\)"/\1/'
awk '
/^\[censorship\]/ { in_cens=1; next }
/^\[/ && in_cens { exit }
in_cens && $1 == "tls_domain" {
sub(/^[^=]*=[[:space:]]*"/, "")
sub(/".*$/, "")
print
exit
}
' "$config"
;;
mask_port)
grep -A10 '\[censorship\]' "$config" | grep 'mask_port\s*=' | sed 's/.*=\s*\([0-9]*\)/\1/' | tr -d ' '
awk '
/^\[censorship\]/ { in_cens=1; next }
/^\[/ && in_cens { exit }
in_cens && $1 == "mask_port" {
sub(/^[^=]*=[[:space:]]*/, "")
gsub(/[[:space:]]/, "")
print
exit
}
' "$config"
;;
*)
grep "$key" "$config" | head -1 | sed 's/.*=\s*"\?\(.*\)"\?/\1/' | tr -d ' "'
grep "$key" "$config" | head -1 | sed 's/^[^=]*=[[:space:]]*//; s/^"//; s/"$//' | tr -d ' '
;;
esac
}
get_telemt_users_block() {
local config="${1:-$TELEMT_CONFIG}"
[ -f "$config" ] || return 1
awk '
/^\[access\.users\]/ { in_users=1; next }
/^\[/ && in_users { exit }
in_users && /^[[:space:]]*[^#[:space:]][^=]*=/ { print }
' "$config"
}
first_telemt_user_secret() {
local config="${1:-$TELEMT_CONFIG}"
get_telemt_users_block "$config" | head -1 | sed 's/^[^=]*=[[:space:]]*//; s/^"//; s/".*$//' | tr -d ' '
}
telemt_users_block_has_main() {
local users_block="$1"
printf '%s\n' "$users_block" | awk -F= '
/^[[:space:]]*#/ || ! /=/ { next }
{
key=$1
gsub(/^[[:space:]]+|[[:space:]]+$/, "", key)
if (key ~ /^".*"$/ || key ~ /^\047.*\047$/) {
key=substr(key, 2, length(key) - 2)
}
if (key == "main") {
found=1
exit
}
}
END { exit found ? 0 : 1 }
'
}
replace_telemt_users_block() {
local users_block="$1"
local config="${2:-$TELEMT_CONFIG}"
[ -f "$config" ] || return 1
[ -n "$users_block" ] || return 0
local tmp
tmp=$(mktemp) || return 1
awk -v users="$users_block" '
BEGIN { split(users, lines, "\n") }
/^\[access\.users\]/ {
found=1
print
for (i = 1; i in lines; i++) {
if (lines[i] != "") print lines[i]
}
in_users=1
next
}
/^\[/ && in_users { in_users=0 }
in_users { next }
{ print }
END {
if (!found) {
print ""
print "[access.users]"
for (i = 1; i in lines; i++) {
if (lines[i] != "") print lines[i]
}
}
}
' "$config" > "$tmp" && mv "$tmp" "$config"
chmod 600 "$config"
}
toml_bool_value() {
local table="$1"
local key="$2"
local config="${3:-$TELEMT_CONFIG}"
awk -v table="$table" -v key="$key" '
$0 == "[" table "]" { in_table=1; next }
/^\[/ && in_table { exit }
in_table && $1 == key {
sub(/^[^=]*=[[:space:]]*/, "")
gsub(/[[:space:]]/, "")
print
exit
}
' "$config"
}
# ── Валидация конфига ────────────────────────────────────────────────────────
validate_telemt_config() {
local config="${1:-$TELEMT_CONFIG}"
@@ -242,10 +401,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}"
}
# ── Вывод информации о прокси ────────────────────────────────────────────────
@@ -257,11 +425,18 @@ show_proxy_info() {
port=$(get_config_value port "$config")
mask_host=$(get_config_value mask_host "$config")
ip=$(get_server_ip)
link=$(generate_proxy_link "$ip" "$port" "$secret")
status=$(telemt_status)
local mode
local mode domain
mode=$(config_get mode 2>/dev/null || echo "lite")
domain=$(config_get domain 2>/dev/null || echo "")
# Генерация ссылки: оба режима используют ee-секрет с mask_host
if [ "$mode" = "pro" ] && [ -n "$domain" ]; then
link=$(generate_proxy_link "$domain" "$port" "$secret" "$domain")
else
link=$(generate_proxy_link "$ip" "$port" "$secret" "$mask_host")
fi
local status_icon status_text
case "$status" in
@@ -274,7 +449,11 @@ show_proxy_info() {
echo -e " ${BOLD}${WHITE}${status_icon} Статус прокси: ${status_text}${NC}"
echo -e " ${DIM}$(printf '─%.0s' {1..50})${NC}"
echo -e " ${WHITE}Ядро:${NC} telemt (Rust)"
echo -e " ${WHITE}IP:${NC} ${CYAN}${ip}${NC}"
if [ "$mode" = "pro" ] && [ -n "$domain" ]; then
echo -e " ${WHITE}Домен:${NC} ${CYAN}${domain}${NC}"
else
echo -e " ${WHITE}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}"

View File

@@ -1,20 +1,29 @@
#!/bin/bash
# GoTelegram v2.2 — Каталог шаблонов сайтов
# Выбор из ~200 шаблонов, превью-ссылки, скачивание через git sparse-checkout
# GoTelegram v2.5.0 — website templates catalog
# Pick from ~1800 templates, preview links, git sparse-checkout downloads,
# + custom git URL templates (user-supplied public repos)
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

View File

@@ -1,5 +1,5 @@
#!/bin/bash
# GoTelegram v2.2 — Управление сайтом (nginx + certbot + шаблоны)
# GoTelegram v2.5.0 — Управление сайтом (nginx + certbot + шаблоны)
# ── Установка nginx ──────────────────────────────────────────────────────────
install_nginx() {
@@ -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
}
@@ -39,7 +39,7 @@ generate_nginx_config() {
mkdir -p /etc/nginx/sites-available /etc/nginx/sites-enabled
cat > "$NGINX_SITE_CONF" << 'EONGINX'
# GoTelegram v2.3 — nginx config
# GoTelegram v2.5.0 — nginx config
# Pro: nginx на 127.0.0.1:8443 (внутренний), telemt на 0.0.0.0:443 (внешний)
# Обычный браузер → :443 → telemt → 127.0.0.1:8443 → nginx (сайт)
@@ -237,11 +237,15 @@ get_ssl_expiry() {
# ── Деплой шаблона сайта ─────────────────────────────────────────────────────
deploy_template_to_nginx() {
local template_dir="$1"
local template_id="${2:-}"
local source_url=""
if [ ! -d "$template_dir" ] || [ ! -f "$template_dir/index.html" ]; then
log_error "Шаблон не содержит index.html: $template_dir"
return 1
fi
[ -z "$template_id" ] && template_id=$(basename "$template_dir")
[ -f "$template_dir/.custom_git_source" ] && source_url=$(head -1 "$template_dir/.custom_git_source" 2>/dev/null || echo "")
# Бекапим старый сайт
if [ -d "$WEBSITE_ROOT" ] && [ "$(ls -A "$WEBSITE_ROOT" 2>/dev/null)" ]; then
@@ -251,7 +255,10 @@ deploy_template_to_nginx() {
fi
mkdir -p "$WEBSITE_ROOT"
cp -r "$template_dir"/* "$WEBSITE_ROOT/"
cp -a "$template_dir/." "$WEBSITE_ROOT/"
rm -f "$WEBSITE_ROOT/.custom_git_source" 2>/dev/null || true
echo "$template_id" > "$WEBSITE_ROOT/.gotelegram_template_id" 2>/dev/null || true
[ -n "$source_url" ] && echo "$source_url" > "$WEBSITE_ROOT/.gotelegram_template_source" 2>/dev/null || true
chown -R www-data:www-data "$WEBSITE_ROOT" 2>/dev/null || chown -R nginx:nginx "$WEBSITE_ROOT" 2>/dev/null
chmod -R 755 "$WEBSITE_ROOT"
@@ -334,7 +341,7 @@ remove_pro_mode() {
# ── Смена шаблона ────────────────────────────────────────────────────────────
switch_template() {
local new_template_dir="$1"
deploy_template_to_nginx "$new_template_dir"
deploy_template_to_nginx "$new_template_dir" "$(basename "$new_template_dir")"
# nginx не требует перезапуска — статика обновилась на месте
log_success "Шаблон сайта обновлён"
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,291 @@
import importlib.util
import json
import os
import re
import shlex
import subprocess
import sys
import tempfile
import unittest
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
SERVER_PATH = ROOT / "admin-web" / "server.py"
def load_server(tmpdir: Path):
os.environ["GOTELEGRAM_BACKUP_DIR"] = str(tmpdir / "backups")
os.environ["GOTELEGRAM_DIR"] = str(tmpdir / "gotelegram")
os.environ["TELEMT_CONFIG"] = str(tmpdir / "etc" / "telemt" / "config.toml")
os.environ["GOTELEGRAM_DISABLED_USERS"] = str(tmpdir / "gotelegram" / "disabled_users.json")
module_name = "gotelegram_admin_server_test"
sys.modules.pop(module_name, None)
spec = importlib.util.spec_from_file_location(module_name, SERVER_PATH)
module = importlib.util.module_from_spec(spec)
assert spec.loader is not None
spec.loader.exec_module(module)
return module
class AdminFeatureTests(unittest.TestCase):
def test_backup_path_accepts_only_local_archives(self):
with tempfile.TemporaryDirectory() as raw:
tmpdir = Path(raw)
server = load_server(tmpdir)
server.BACKUP_DIR.mkdir(parents=True)
good = server.BACKUP_DIR / "gotelegram_backup_20260425_120000.tar.gz"
good.write_text("backup", encoding="utf-8")
encrypted = server.BACKUP_DIR / "gotelegram_backup_20260425_120001.tar.gz.enc"
encrypted.write_text("backup", encoding="utf-8")
legacy = server.BACKUP_DIR / "backup_20260425_120002.tar.gz"
legacy.write_text("backup", encoding="utf-8")
self.assertEqual(server.safe_backup_path(good.name), good.resolve())
self.assertEqual(server.safe_backup_path(encrypted.name), encrypted.resolve())
self.assertEqual(server.safe_backup_path(legacy.name), legacy.resolve())
with self.assertRaises(ValueError):
server.safe_backup_path("../outside.tar.gz")
with self.assertRaises(ValueError):
server.safe_backup_path("gotelegram_backup_20260425_120000.tar.gz.sha256")
with self.assertRaises(FileNotFoundError):
server.safe_backup_path("missing.tar.gz")
def test_backup_schedule_calendar_rejects_unknown_values(self):
with tempfile.TemporaryDirectory() as raw:
server = load_server(Path(raw))
self.assertEqual(server.backup_schedule_calendar("daily"), "*-*-* 03:20:00")
self.assertEqual(server.backup_schedule_calendar("weekly"), "Sun 03:20:00")
self.assertEqual(server.backup_schedule_calendar("monthly"), "*-*-01 03:20:00")
self.assertIsNone(server.backup_schedule_calendar("off"))
with self.assertRaises(ValueError):
server.backup_schedule_calendar("hourly")
def test_user_records_include_active_disabled_and_ip_limits(self):
with tempfile.TemporaryDirectory() as raw:
tmpdir = Path(raw)
server = load_server(tmpdir)
server.TELEMT_CONFIG.parent.mkdir(parents=True)
server.TELEMT_CONFIG.write_text(
"\n".join([
"[access.users]",
'main = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"',
'client = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"',
"",
"[access.user_max_unique_ips]",
"main = 0",
"client = 2",
"disabled = 1",
"",
]),
encoding="utf-8",
)
server.DISABLED_USERS_FILE.parent.mkdir(parents=True)
server.DISABLED_USERS_FILE.write_text(
json.dumps({"users": {"disabled": "cccccccccccccccccccccccccccccccc"}}),
encoding="utf-8",
)
records = server.read_user_records()
self.assertTrue(records["client"]["enabled"])
self.assertEqual(records["client"]["max_unique_ips"], 2)
self.assertFalse(records["disabled"]["enabled"])
self.assertEqual(records["disabled"]["max_unique_ips"], 1)
self.assertEqual(records["main"]["max_unique_ips"], 0)
def test_write_user_max_unique_ips_preserves_other_toml_sections(self):
with tempfile.TemporaryDirectory() as raw:
server = load_server(Path(raw))
server.TELEMT_CONFIG.parent.mkdir(parents=True)
server.TELEMT_CONFIG.write_text(
"\n".join([
"[server]",
"port = 443",
"",
"[access.users]",
'main = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"',
'client = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"',
"",
"[access.user_max_unique_ips]",
"client = 3",
"old = 4",
"",
"[network]",
'dns_overrides = ["example.com:8443:127.0.0.1"]',
"",
]),
encoding="utf-8",
)
server.write_user_max_unique_ips({"main": 1, "client": 0, "new": 5})
text = server.TELEMT_CONFIG.read_text(encoding="utf-8")
self.assertIn("[server]\nport = 443", text)
self.assertIn("[network]\ndns_overrides", text)
self.assertIn("[access.user_max_unique_ips]", text)
self.assertIn('"main" = 1', text)
self.assertIn('"new" = 5', text)
self.assertNotIn("client = 3", text)
self.assertNotIn("old = 4", text)
def test_key_card_traffic_uses_live_ip_counts_not_stale_history(self):
with tempfile.TemporaryDirectory() as raw:
server = load_server(Path(raw))
stale_history = {
"epoch": 1000,
"total_octets": 100,
"current_connections": 16,
"active_unique_ips": 8,
"recent_unique_ips": 5,
}
original = server.runtime_user_traffic
server.runtime_user_traffic = lambda name, enabled=True: {
"ok": True,
"enabled": True,
"total_octets": 200,
"current_connections": 0,
"active_unique_ips": 0,
"recent_unique_ips": 0,
}
try:
snapshot = server.current_user_traffic_snapshot("client", True, stale_history, now=2000)
finally:
server.runtime_user_traffic = original
self.assertEqual(snapshot["epoch"], 2000)
self.assertEqual(snapshot["total_octets"], 200)
self.assertEqual(snapshot["current_connections"], 0)
self.assertEqual(snapshot["active_unique_ips"], 0)
self.assertEqual(snapshot["recent_unique_ips"], 0)
def test_key_card_traffic_fallback_keeps_only_historical_total(self):
with tempfile.TemporaryDirectory() as raw:
server = load_server(Path(raw))
stale_history = {
"epoch": 1000,
"total_octets": 100,
"current_connections": 16,
"active_unique_ips": 8,
"recent_unique_ips": 5,
}
original = server.runtime_user_traffic
server.runtime_user_traffic = lambda name, enabled=True: {"ok": False}
try:
snapshot = server.current_user_traffic_snapshot("client", True, stale_history, now=2000)
finally:
server.runtime_user_traffic = original
self.assertEqual(snapshot["epoch"], 1000)
self.assertEqual(snapshot["total_octets"], 100)
self.assertEqual(snapshot["current_connections"], 0)
self.assertEqual(snapshot["active_unique_ips"], 0)
self.assertEqual(snapshot["recent_unique_ips"], 0)
def test_keys_view_uses_card_layout_without_horizontal_table(self):
app_js = (ROOT / "admin-web" / "static" / "app.js").read_text(encoding="utf-8")
styles = (ROOT / "admin-web" / "static" / "styles.css").read_text(encoding="utf-8")
index = (ROOT / "admin-web" / "static" / "index.html").read_text(encoding="utf-8")
self.assertNotIn('class="actions"', app_js)
self.assertIn('class="key-card', app_js)
self.assertIn('class="key-card-actions"', app_js)
self.assertIn('class="action-buttons"', app_js)
self.assertIn('class="key-name-button"', app_js)
self.assertIn(".key-card .mini-actions,\n.key-card .action-buttons", styles)
self.assertIn("grid-template-columns: 1fr", styles)
self.assertIn("min-height: 44px", styles)
self.assertNotIn("td.actions", styles)
self.assertIn('class="keys-list" id="usersTable"', index)
self.assertNotIn('class="keys-table"', index)
def test_topbar_has_five_second_auto_refresh_toggle(self):
app_js = (ROOT / "admin-web" / "static" / "app.js").read_text(encoding="utf-8")
styles = (ROOT / "admin-web" / "static" / "styles.css").read_text(encoding="utf-8")
index = (ROOT / "admin-web" / "static" / "index.html").read_text(encoding="utf-8")
self.assertIn('id="autoRefreshToggle"', index)
self.assertIn('data-i18n-title="autoRefresh"', index)
self.assertIn("gotelegram-auto-refresh", app_js)
self.assertIn("AUTO_REFRESH_MS = 5000", app_js)
self.assertIn("setInterval", app_js)
self.assertIn("clearInterval", app_js)
self.assertIn(".auto-refresh-toggle", styles)
def test_get_config_value_secret_accepts_quoted_main_user(self):
with tempfile.TemporaryDirectory() as raw:
config = Path(raw) / "config.toml"
config.write_text(
"\n".join([
"[access.users]",
'"main" = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"',
'"client" = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"',
"",
]),
encoding="utf-8",
)
script = "\n".join([
"set -e",
f"source {shlex.quote(str(ROOT / 'lib' / 'telemt_config.sh'))}",
f"get_config_value secret {shlex.quote(str(config))}",
])
result = subprocess.run(
["bash", "-lc", script],
cwd=ROOT,
text=True,
capture_output=True,
check=False,
)
self.assertEqual(result.returncode, 0, result.stderr)
self.assertEqual(result.stdout.strip(), "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")
def test_telemt_users_block_detects_quoted_main_user(self):
script = "\n".join([
"set -e",
f"source {shlex.quote(str(ROOT / 'lib' / 'telemt_config.sh'))}",
"block=$'\"main\" = \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"\\n\"client\" = \"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\"'",
"telemt_users_block_has_main \"$block\"",
])
result = subprocess.run(
["bash", "-lc", script],
cwd=ROOT,
text=True,
capture_output=True,
check=False,
)
self.assertEqual(result.returncode, 0, result.stderr)
def test_telemt_restart_requests_are_debounced(self):
with tempfile.TemporaryDirectory() as raw:
server = load_server(Path(raw))
calls = []
original_run = server.run
original_status = server.service_status
try:
server._LAST_TELEMT_RESTART = 0.0
server.TELEMT_RESTART_DEBOUNCE_SECONDS = 30.0
server.service_status = lambda name: "running"
def fake_run(cmd, timeout=8):
calls.append(cmd)
return 0, "", ""
server.run = fake_run
self.assertTrue(server.request_service_restart("telemt"))
self.assertTrue(server.request_service_restart("telemt"))
finally:
server.run = original_run
server.service_status = original_status
restart_calls = [cmd for cmd in calls if cmd[:3] == ["systemctl", "--no-block", "restart"]]
self.assertEqual(len(restart_calls), 1)
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,84 @@
import json
import re
import unittest
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
BOT_PATH = ROOT / "gotelegram-bot" / "bot.py"
CATALOG_PATH = ROOT / "templates_catalog.json"
class BotFeatureTests(unittest.TestCase):
def test_catalog_contains_template_ids_that_break_raw_callback_data(self):
catalog = json.loads(CATALOG_PATH.read_text(encoding="utf-8"))
raw_lengths = [
len(f"pro_tpl_{tpl['id']}".encode("utf-8"))
for cat in catalog.get("categories", [])
for tpl in cat.get("templates", [])
]
self.assertTrue(any(length > 64 for length in raw_lengths))
def test_bot_uses_short_template_callback_keys(self):
source = BOT_PATH.read_text(encoding="utf-8")
self.assertNotIn("callback_data=f\"pro_tpl_{tpl['id']}\"", source)
self.assertNotIn('callback_data=f"pro_confirm_{tpl_id}"', source)
self.assertIn("pro_template_map", source)
self.assertIn("resolve_pro_template_id", source)
def test_template_callbacks_are_restart_safe_hashes(self):
source = BOT_PATH.read_text(encoding="utf-8")
category_body = re.search(
r"async def cb_pro_category\(.*?(?=\n\n(?:async )?def |\n\n#)",
source,
flags=re.S,
)
resolve_body = re.search(
r"def resolve_pro_template_id\(.*?(?=\n\n(?:async )?def |\n\n#)",
source,
flags=re.S,
)
self.assertIsNotNone(category_body)
self.assertIsNotNone(resolve_body)
self.assertIn('pro_template_key_for_id(context, tpl["id"])', category_body.group(0))
self.assertNotIn("enumerate(templates)", category_body.group(0))
self.assertNotIn("mapping.clear()", category_body.group(0))
self.assertIn("load_json(TEMPLATES_CATALOG)", resolve_body.group(0))
self.assertIn("hashlib.sha1", resolve_body.group(0))
def test_telemt_version_checks_systemd_path_fallbacks(self):
source = BOT_PATH.read_text(encoding="utf-8")
version_body = re.search(
r"async def get_telemt_version\(\).*?(?=\n\n(?:async )?def |\n\n#)",
source,
flags=re.S,
)
self.assertIsNotNone(version_body)
body = version_body.group(0)
self.assertIn('"--version"', body)
self.assertIn('"-V"', body)
self.assertIn('"/usr/local/bin/telemt"', body)
self.assertIn("for command in", body)
self.assertIn("for args in", body)
self.assertNotIn('"-v"', body)
def test_telemt_update_menu_reuses_version_helper(self):
source = BOT_PATH.read_text(encoding="utf-8")
update_body = re.search(
r"async def cb_menu_update\(.*?(?=\n\n(?:async )?def |\n\n#)",
source,
flags=re.S,
)
self.assertIsNotNone(update_body)
body = update_body.group(0)
self.assertIn("await get_telemt_version()", body)
self.assertNotIn('sh("telemt", "--version")', body)
if __name__ == "__main__":
unittest.main()