diff --git a/DOCS_AI.md b/DOCS_AI.md
index b862b98..3460874 100644
--- a/DOCS_AI.md
+++ b/DOCS_AI.md
@@ -1,6 +1,6 @@
# GoTelegram Pro — техническая документация для ИИ-агентов
-**Версия:** 2.4.3
+**Версия:** 2.5.0
**Репозиторий:** `anten-ka/gotelegram_pro`
**Активная ветка:** `alfa-test` (ветка `test` заморожена и содержит stable для конечных пользователей)
**Целевая ОС:** Ubuntu 20.04+, Debian 11+
@@ -391,7 +391,7 @@ Fallback: если по известным правилам index.html не на
Коды ошибок: `missing_arg`, `invalid_domain`, `wrong_mode`, `unknown_template`, `download_failed`, `deploy_failed`, `no_secret`, `gen_failed`, `validate_failed`, `restart_failed`, `unknown_action`, `lock_timeout`.
-**Сериализация (v2.4.3):** `bot_action_dispatch` оборачивает вызов в `flock -w 30 9 < /var/lock/gotelegram-bot-action.lock`. Это защищает от гонок при:
+**Сериализация (v2.4.3+):** `bot_action_dispatch` оборачивает вызов в `flock -w 30 9 < /var/lock/gotelegram-bot-action.lock`. Это защищает от гонок при:
1. Одновременных callback'ах внутри бота (asyncio.Lock уже ловит это, но flock — defense-in-depth).
2. Параллельных CLI-вызовах (бот + ручной SSH, или два бот-процесса — теоретически).
@@ -446,7 +446,7 @@ switch_language ru|en
3. Напиши `C:\Temp\push_<описание>.py`:
```python
import os, base64, json, urllib.request, ssl
- TOKEN = "github_pat_..."
+ TOKEN = os.environ["GOTELEGRAM_PAT"]
REPO = "anten-ka/gotelegram_pro"
BRANCH = "alfa-test"
API = f"https://api.github.com/repos/{REPO}"
@@ -596,6 +596,8 @@ with socket.create_connection(("95.163.176.222", 443), timeout=5) as s:
## 17. Changelog
+- **2.5.0 (2026-04-24)** — крупный maintenance pass в ветке `codex`: единая версия `2.5.0` в runtime и документации; удалён дефолтный PAT из `bootstrap.sh` (токен теперь только через `GOTELEGRAM_PAT`); `generate_telemt_toml` добавляет `[server.api]` на `127.0.0.1:9091` и metrics на `127.0.0.1:9090`, что нужно для управления пользователями и статистики; Telegram-бот получил меню `🔑 Keys` для `[access.users]` (добавить/удалить/показать ссылку/runtime info); исправлено чтение traffic CSV в боте (header больше не ломает parsing); бот сам делает `stats_collect` перед показом статистики; `iptables` добавлен в optional deps и stats collector пытается установить его; CLI-смена шаблона теперь обновляет `config.json.template_id`, чтобы бот не показывал первый установленный шаблон; backup/restore версии `1.2` сохраняет bot `.env`, bot lang files, custom templates, templates catalog, stats history и полноценную структуру Let's Encrypt (`live/archive/renewal`) для переезда на новый сервер; добавлен безопасный детект 3x-ui/Xray на 443 и генерируется `/opt/gotelegram/shared-443-3xui.md` с объяснением shared-443 ограничений.
+- **2.4.6 (2026-04-10)** — universal `apt_lock_wait` helper: ожидание dpkg/apt lock при unattended-upgrades, исправляет установку nginx/certbot/python на свежих VPS.
- **2.4.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` (этот файл).
diff --git a/DOCS_HUMAN.md b/DOCS_HUMAN.md
index d4ba91c..c982040 100644
--- a/DOCS_HUMAN.md
+++ b/DOCS_HUMAN.md
@@ -1,6 +1,6 @@
# GoTelegram Pro — руководство пользователя
-**Версия:** 2.4.3
+**Версия:** 2.5.0
**Репозиторий:** `anten-ka/gotelegram_pro`
**Для кого:** владельцы VPS, которым нужен надёжный MTProxy для Telegram с маскировкой под обычный HTTPS-сайт.
@@ -26,6 +26,12 @@ GoTelegram Pro — это готовый менеджер прокси-серв
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` уже будет работать откуда угодно.
Дальше в меню:
@@ -214,6 +220,8 @@ A: Сам MTProxy — да, это публичная технология из
## Changelog (коротко)
+- **2.5.0** — единая версия по коду и документации; удалён дефолтный PAT из `bootstrap.sh`; исправлена статистика в боте (CSV header больше не ломает чтение истории, бот сам обновляет snapshot); CLI-смена шаблона теперь обновляет `config.json.template_id`, поэтому бот показывает текущий шаблон; telemt TOML включает локальный API `127.0.0.1:9091` и metrics `127.0.0.1:9090`; добавлено меню Telegram-бота для отдельных ключей пользователей (`[access.users]`): список, добавление, удаление, ссылка и runtime/API-информация; backup/restore сохраняет bot `.env`, языки бота, custom templates, stats history и структуру Let's Encrypt для переезда на новый VPS; добавлен безопасный детект 3x-ui/Xray на 443 с предупреждением и заметкой по shared-443.
+- **2.4.6** — ожидание apt/dpkg lock на свежих Ubuntu/Debian, чтобы установка nginx/certbot/Python не падала во время unattended-upgrades.
- **2.4.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`).
diff --git a/bootstrap.sh b/bootstrap.sh
index ef01d61..604f120 100755
--- a/bootstrap.sh
+++ b/bootstrap.sh
@@ -6,7 +6,7 @@ set -euo pipefail
REPO="anten-ka/gotelegram_pro"
BRANCH="${GOTELEGRAM_BRANCH:-test}"
-PAT="${GOTELEGRAM_PAT:-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}"
+PAT="${GOTELEGRAM_PAT:-}"
INSTALL_DIR="/opt/gotelegram"
# Use raw.githubusercontent.com (CDN) — faster and avoids Contents API caching
# issues that occasionally return 404 for recently added files on non-default branches.
@@ -34,6 +34,12 @@ if [ "$(id -u)" -ne 0 ]; then
exit 1
fi
+if [ -z "$PAT" ]; then
+ echo -e " ${RED}✗${NC} Не задан GitHub token."
+ echo -e " ${YELLOW}Запустите так:${NC} ${CYAN}GOTELEGRAM_PAT=YOUR_PAT sudo -E bash bootstrap.sh${NC}"
+ exit 1
+fi
+
# Check dependencies
for cmd in curl jq; do
if ! command -v "$cmd" &>/dev/null; then
diff --git a/gotelegram-bot/README.md b/gotelegram-bot/README.md
index fa9cbfc..4c9d190 100644
--- a/gotelegram-bot/README.md
+++ b/gotelegram-bot/README.md
@@ -1,4 +1,4 @@
-# GoTelegram v2.2 Bot
+# GoTelegram v2.5.0 Bot
Production-quality Telegram bot for managing MTProxy (telemt engine) on Linux servers.
@@ -19,6 +19,7 @@ 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
- **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+
@@ -144,4 +145,4 @@ code, stdout, stderr = await sh("command", "arg1", "arg2")
## License
-GoTelegram v2.2 - Open source community project
+GoTelegram v2.5.0 - Open source community project
diff --git a/gotelegram-bot/bot.py b/gotelegram-bot/bot.py
index ca7138e..c92b613 100644
--- a/gotelegram-bot/bot.py
+++ b/gotelegram-bot/bot.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
"""
-GoTelegram v2.4 Bot - MTProxy Management for Linux
+GoTelegram v2.5.0 Bot - MTProxy Management for Linux
Manages telemt engine via Telegram interface with full CLI feature parity
Uses python-telegram-bot v21+
Supports EN/RU UI with per-user language preferences.
@@ -23,6 +23,7 @@ from datetime import datetime
from io import StringIO
from pathlib import Path
from typing import Tuple, Optional, List, Dict, Any
+from urllib.parse import quote
from dotenv import load_dotenv
from telegram import (
@@ -100,7 +101,7 @@ logger = logging.getLogger(__name__)
# CONFIGURATION
# ============================================================================
-GOTELEGRAM_VERSION = "2.4.6"
+GOTELEGRAM_VERSION = "2.5.0"
GOTELEGRAM_CONFIG = "/opt/gotelegram/config.json"
TELEMT_CONFIG = "/etc/telemt/config.toml"
TELEMT_SERVICE = "telemt"
@@ -256,6 +257,7 @@ _DOMAIN_RE = re.compile(
r"^(?=.{1,253}$)(?:(?!-)[A-Za-z0-9-]{1,63}(? Dict:
@@ -342,6 +344,22 @@ def save_json(path: str, data: Dict) -> bool:
return False
+def template_display_name(template_id: str) -> str:
+ """Resolve a template id to a human-friendly name from catalog/config."""
+ if not template_id:
+ return ""
+ if template_id.startswith("custom_"):
+ config = load_json(GOTELEGRAM_CONFIG) or {}
+ source = config.get("template_source", "")
+ return f"{template_id} ({source})" if source else template_id
+ catalog = load_json(TEMPLATES_CATALOG) or {}
+ for cat in catalog.get("categories", []):
+ for tpl in cat.get("templates", []):
+ if tpl.get("id") == template_id:
+ return f"{tpl.get('name', template_id)} ({template_id})"
+ return template_id
+
+
async def safe_edit_message(
query,
text: str,
@@ -498,14 +516,17 @@ def get_main_menu(user_id: Optional[int] = None) -> InlineKeyboardMarkup:
],
[
InlineKeyboardButton(_t(user_id, "menu_stats"), callback_data="menu_stats"),
+ InlineKeyboardButton(_t(user_id, "menu_users"), callback_data="menu_users"),
+ ],
+ [
InlineKeyboardButton(_t(user_id, "menu_remove"), callback_data="menu_remove"),
- ],
- [
InlineKeyboardButton(_t(user_id, "menu_admins"), callback_data="menu_admins"),
- InlineKeyboardButton(_t(user_id, "menu_credits"), callback_data="menu_credits"),
],
[
+ InlineKeyboardButton(_t(user_id, "menu_credits"), callback_data="menu_credits"),
InlineKeyboardButton(_t(user_id, "menu_language"), callback_data="menu_lang"),
+ ],
+ [
InlineKeyboardButton(_t(user_id, "menu_close"), callback_data="close_menu"),
],
]
@@ -653,7 +674,7 @@ async def get_status_text(user_id: Optional[int] = None) -> str:
# install.sh/save_gotelegram_config uses "template_id" (not "template")
tpl = config.get("template_id") or config.get("template")
if tpl:
- lines.append(f"{_t(user_id, 'status_template')}: {html.escape(str(tpl))}")
+ lines.append(f"{_t(user_id, 'status_template')}: {html.escape(template_display_name(str(tpl)))}")
if config.get("domain"):
lines.append(f"{_t(user_id, 'status_domain')}: {html.escape(str(config['domain']))}")
if config.get("port"):
@@ -689,6 +710,15 @@ async def get_status_text(user_id: Optional[int] = None) -> str:
async def get_traffic_stats() -> str:
"""Get formatted traffic statistics."""
+ await sh(
+ "bash",
+ "-lc",
+ "source /opt/gotelegram/lib/common.sh; "
+ "source /opt/gotelegram/lib/stats.sh; "
+ "stats_init >/dev/null 2>&1 || true; stats_collect >/dev/null 2>&1 || true",
+ timeout=15,
+ )
+
# Read current snapshot
current_file = "/run/gotelegram/stats_current.json"
history_file = "/opt/gotelegram/stats_history.csv"
@@ -706,6 +736,8 @@ async def get_traffic_stats() -> str:
reader = csv.reader(f)
for row in reader:
if len(row) >= 3:
+ if not row[0].isdigit():
+ continue
history.append({
"ts": int(row[0]),
"proxy": int(row[1]),
@@ -824,12 +856,12 @@ async def cb_menu_status(update: Update, context: ContextTypes.DEFAULT_TYPE) ->
# ============================================================================
-def get_install_mode_menu() -> InlineKeyboardMarkup:
+def get_install_mode_menu(user_id: Optional[int] = None) -> InlineKeyboardMarkup:
"""Install mode selection menu."""
buttons = [
[InlineKeyboardButton("⚡ Lite", callback_data="install_mode_lite")],
[InlineKeyboardButton("🛡 Pro", callback_data="install_mode_pro")],
- [InlineKeyboardButton(_t(_uid(update), "btn_back"), callback_data="menu_main")],
+ [InlineKeyboardButton(_t(user_id, "btn_back"), callback_data="menu_main")],
]
return InlineKeyboardMarkup(buttons)
@@ -858,7 +890,7 @@ async def cb_menu_install(update: Update, context: ContextTypes.DEFAULT_TYPE) ->
keyboard = InlineKeyboardMarkup(buttons)
else:
text = "Select installation mode:"
- keyboard = get_install_mode_menu()
+ keyboard = get_install_mode_menu(_uid(update))
await safe_edit_message(query,
text, reply_markup=keyboard, parse_mode="HTML"
@@ -1362,6 +1394,96 @@ async def cb_pro_confirm(update: Update, context: ContextTypes.DEFAULT_TYPE) ->
# PROXY LINK & SHARE
# ============================================================================
+def load_telemt_users() -> Dict[str, str]:
+ """Return users from [access.users] in telemt config."""
+ telemt_cfg = load_toml(TELEMT_CONFIG) or {}
+ users = telemt_cfg.get("access", {}).get("users", {})
+ if not isinstance(users, dict):
+ return {}
+ return {
+ str(name): str(secret)
+ for name, secret in users.items()
+ if isinstance(name, str) and isinstance(secret, str)
+ }
+
+
+def save_telemt_users(users: Dict[str, str]) -> bool:
+ """Persist [access.users] while keeping the rest of the TOML structure."""
+ telemt_cfg = load_toml(TELEMT_CONFIG) or {}
+ access = telemt_cfg.setdefault("access", {})
+ access["users"] = dict(sorted(users.items()))
+ try:
+ os.makedirs(os.path.dirname(TELEMT_CONFIG), exist_ok=True)
+ with open(TELEMT_CONFIG, "w") as f:
+ toml.dump(telemt_cfg, f)
+ os.chmod(TELEMT_CONFIG, 0o600)
+ return True
+ except Exception as e:
+ logger.error(f"Failed to save telemt users: {e}")
+ return False
+
+
+async def refresh_telemt_after_user_change() -> bool:
+ """Restart telemt after config user changes."""
+ code, _, _ = await sh("systemctl", "restart", TELEMT_SERVICE, timeout=20)
+ return code == 0
+
+
+async def telemt_api_get(path: str) -> Optional[Dict[str, Any]]:
+ """Read telemt local API if it is enabled in config."""
+ code, stdout, _ = await sh(
+ "curl",
+ "-sS",
+ "--max-time",
+ "3",
+ f"http://127.0.0.1:9091{path}",
+ timeout=5,
+ )
+ if code != 0 or not stdout.strip():
+ return None
+ try:
+ data = json.loads(stdout)
+ return data if isinstance(data, dict) else None
+ except json.JSONDecodeError:
+ return None
+
+
+def _extract_traffic_value(data: Any, keys: List[str]) -> int:
+ if isinstance(data, dict):
+ total = 0
+ for key, value in data.items():
+ if key in keys and isinstance(value, (int, float)):
+ total += int(value)
+ elif isinstance(value, (dict, list)):
+ total += _extract_traffic_value(value, keys)
+ return total
+ if isinstance(data, list):
+ return sum(_extract_traffic_value(item, keys) for item in data)
+ return 0
+
+
+async def get_proxy_link_for_secret(secret: str) -> Optional[str]:
+ """Generate a fake-TLS proxy link for an arbitrary telemt user secret."""
+ config = load_json(GOTELEGRAM_CONFIG) or {}
+ if not secret:
+ return None
+
+ mode = config.get("mode", "lite")
+ domain = config.get("domain", "")
+ port = config.get("port", 443)
+
+ if mode == "pro" and domain:
+ domain_hex = str(domain).encode().hex()
+ return f"tg://proxy?server={domain}&port={port}&secret=ee{secret}{domain_hex}"
+
+ code, stdout, _ = await sh("curl", "-s", "-4", "--max-time", "5", "https://api.ipify.org")
+ server = stdout.strip() if code == 0 and stdout.strip() else "0.0.0.0"
+ mask_host = config.get("mask_host", "")
+ if mask_host:
+ domain_hex = str(mask_host).encode().hex()
+ return f"tg://proxy?server={server}&port={port}&secret=ee{secret}{domain_hex}"
+ return f"tg://proxy?server={server}&port={port}&secret={secret}"
+
async def get_proxy_link() -> Optional[str]:
"""Generate proxy link from config. Pro-mode uses domain + fake-TLS secret."""
@@ -1381,27 +1503,7 @@ async def get_proxy_link() -> Optional[str]:
if not secret:
return None
- mode = config.get("mode", "lite")
- domain = config.get("domain", "")
- port = config.get("port", 443)
-
- # Pro-режим: ссылка с доменом и fake-TLS секретом (ee + secret + hex domain)
- if mode == "pro" and domain:
- domain_hex = domain.encode().hex()
- faketls_secret = f"ee{secret}{domain_hex}"
- return f"tg://proxy?server={domain}&port={port}&secret={faketls_secret}"
-
- # Lite-режим: IP + fake-TLS с mask_host
- code, stdout, _ = await sh("curl", "-s", "-4", "--max-time", "5", "https://api.ipify.org")
- server = stdout.strip() if code == 0 and stdout.strip() else "0.0.0.0"
-
- mask_host = config.get("mask_host", "")
- if mask_host:
- domain_hex = mask_host.encode().hex()
- faketls_secret = f"ee{secret}{domain_hex}"
- return f"tg://proxy?server={server}&port={port}&secret={faketls_secret}"
-
- return f"tg://proxy?server={server}&port={port}&secret={secret}"
+ return await get_proxy_link_for_secret(secret)
async def cb_menu_link(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
@@ -1477,6 +1579,201 @@ async def cb_menu_share(update: Update, context: ContextTypes.DEFAULT_TYPE) -> N
)
+# ============================================================================
+# TELEMT USERS
+# ============================================================================
+
+
+def _users_keyboard(users: Dict[str, str], user_id: Optional[int]) -> InlineKeyboardMarkup:
+ rows = []
+ for name in sorted(users):
+ rows.append([InlineKeyboardButton(f"👤 {name}", callback_data=f"user_view_{name}")])
+ rows.append([InlineKeyboardButton("➕ Добавить ключ", callback_data="user_add")])
+ rows.append([
+ InlineKeyboardButton(_t(user_id, "btn_refresh"), callback_data="menu_users"),
+ InlineKeyboardButton(_t(user_id, "btn_back"), callback_data="menu_main"),
+ ])
+ return InlineKeyboardMarkup(rows)
+
+
+async def cb_menu_users(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
+ query = update.callback_query
+ await query.answer()
+ user_id = _uid(update)
+ users = load_telemt_users()
+
+ if users:
+ user_lines = "\n".join(f"• {html.escape(name)}" for name in sorted(users))
+ else:
+ user_lines = "Ключей пока нет"
+
+ api_summary = await telemt_api_get("/v1/stats/summary")
+ api_note = ""
+ if api_summary and isinstance(api_summary.get("data"), dict):
+ data = api_summary["data"]
+ configured = data.get("configured_users")
+ active = data.get("active_connections") or data.get("connections_active")
+ bits = []
+ if configured is not None:
+ bits.append(f"users: {configured}")
+ if active is not None:
+ bits.append(f"active: {active}")
+ if bits:
+ api_note = "\n\nAPI: " + ", ".join(bits)
+
+ text = (
+ "🔑 Ключи пользователей\n\n"
+ f"{user_lines}"
+ f"{api_note}\n\n"
+ "Нажмите на пользователя, чтобы увидеть ссылку, статистику и действия."
+ )
+ await safe_edit_message(query, text, reply_markup=_users_keyboard(users, user_id), parse_mode="HTML")
+
+
+async def _user_detail_text(name: str, secret: str) -> str:
+ link = await get_proxy_link_for_secret(secret)
+ api = await telemt_api_get(f"/v1/users/{quote(name, safe='')}")
+ details = ""
+ if api:
+ data = api.get("data", api)
+ up = _extract_traffic_value(data, ["upload_bytes", "uplink_bytes", "tx_bytes", "sent_bytes", "up"])
+ down = _extract_traffic_value(data, ["download_bytes", "downlink_bytes", "rx_bytes", "received_bytes", "down"])
+ active_ips = _extract_traffic_value(data, ["active_ips", "unique_ips"])
+ parts = []
+ if up:
+ parts.append(f"↑ {up} B")
+ if down:
+ parts.append(f"↓ {down} B")
+ if active_ips:
+ parts.append(f"active IPs: {active_ips}")
+ if parts:
+ details = "\n" + "\n".join(parts)
+ else:
+ compact = json.dumps(data, ensure_ascii=False)[:600]
+ details = f"\n
{html.escape(compact)}"
+ else:
+ details = "\nRuntime API недоступен. Новые установки GoTelegram включают его автоматически."
+
+ link_line = html.escape(link) if link else "link unavailable"
+ return (
+ f"👤 {html.escape(name)}\n\n"
+ f"Secret: {html.escape(secret)}\n\n"
+ f"Ссылка:\n{link_line}\n"
+ f"{details}"
+ )
+
+
+async def cb_user_view(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
+ query = update.callback_query
+ await query.answer()
+ user_id = _uid(update)
+ name = query.data.removeprefix("user_view_")
+ users = load_telemt_users()
+ secret = users.get(name)
+ if not secret:
+ await safe_edit_message(
+ query,
+ "❌ Пользователь не найден.",
+ reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton(_t(user_id, "btn_back"), callback_data="menu_users")]]),
+ )
+ return
+
+ buttons = [
+ [InlineKeyboardButton("🗑 Удалить", callback_data=f"user_del_{name}")],
+ [InlineKeyboardButton(_t(user_id, "btn_back"), callback_data="menu_users")],
+ ]
+ await safe_edit_message(
+ query,
+ await _user_detail_text(name, secret),
+ reply_markup=InlineKeyboardMarkup(buttons),
+ parse_mode="HTML",
+ disable_web_page_preview=True,
+ )
+
+
+async def cb_user_add(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
+ query = update.callback_query
+ await query.answer()
+ user_id = _uid(update)
+ context.user_data["awaiting_user_name"] = True
+ text = (
+ "➕ Новый ключ\n\n"
+ "Отправьте имя пользователя: латиница, цифры, _ . -, до 48 символов.\n"
+ "Пример: ivan или family-1."
+ )
+ await safe_edit_message(
+ query,
+ text,
+ reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton(_t(user_id, "btn_cancel"), callback_data="menu_users")]]),
+ parse_mode="HTML",
+ )
+
+
+async def cb_user_delete(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
+ query = update.callback_query
+ await query.answer()
+ user_id = _uid(update)
+ name = query.data.removeprefix("user_del_")
+ if name == "main":
+ await query.answer("main нельзя удалить", show_alert=True)
+ return
+ text = f"Удалить ключ {html.escape(name)}?"
+ buttons = [
+ [InlineKeyboardButton("✅ Удалить", callback_data=f"user_del_yes_{name}")],
+ [InlineKeyboardButton(_t(user_id, "btn_cancel"), callback_data=f"user_view_{name}")],
+ ]
+ await safe_edit_message(query, text, reply_markup=InlineKeyboardMarkup(buttons), parse_mode="HTML")
+
+
+async def cb_user_delete_confirm(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
+ query = update.callback_query
+ await query.answer()
+ user_id = _uid(update)
+ name = query.data.removeprefix("user_del_yes_")
+ users = load_telemt_users()
+ if name == "main" or name not in users:
+ await query.answer("Нельзя удалить этот ключ", show_alert=True)
+ return
+ users.pop(name, None)
+ if not save_telemt_users(users):
+ await safe_edit_message(query, "❌ Не удалось сохранить config.toml")
+ return
+ await refresh_telemt_after_user_change()
+ await safe_edit_message(
+ query,
+ f"✅ Ключ {html.escape(name)} удалён.",
+ reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton(_t(user_id, "btn_back"), callback_data="menu_users")]]),
+ parse_mode="HTML",
+ )
+
+
+async def create_user_from_text(update: Update, context: ContextTypes.DEFAULT_TYPE, name: str) -> None:
+ user_id = update.effective_user.id
+ if not _USER_NAME_RE.match(name):
+ await update.message.reply_text("❌ Некорректное имя. Используйте латиницу, цифры, _ . - и до 48 символов.")
+ return
+ users = load_telemt_users()
+ if name in users:
+ await update.message.reply_text("❌ Такой пользователь уже есть.")
+ return
+ secret = hashlib.sha256(f"{name}:{time.time()}:{os.urandom(16).hex()}".encode()).hexdigest()[:32]
+ users[name] = secret
+ if not save_telemt_users(users):
+ await update.message.reply_text("❌ Не удалось сохранить /etc/telemt/config.toml")
+ return
+ await refresh_telemt_after_user_change()
+ link = await get_proxy_link_for_secret(secret)
+ await update.message.reply_text(
+ f"✅ Ключ создан\n\n"
+ f"Пользователь: {html.escape(name)}\n"
+ f"Secret: {secret}\n\n"
+ f"{html.escape(link or '')}",
+ reply_markup=_users_keyboard(load_telemt_users(), user_id),
+ parse_mode="HTML",
+ disable_web_page_preview=True,
+ )
+
+
# ============================================================================
# RESTART & LOGS
# ============================================================================
@@ -1795,7 +2092,7 @@ async def cb_install_migrate(update: Update, context: ContextTypes.DEFAULT_TYPE)
"✅ v1 container stopped and removed\n\n"
"Now select installation mode for v2:"
)
- keyboard = get_install_mode_menu()
+ keyboard = get_install_mode_menu(_uid(update))
await safe_edit_message(query,text, reply_markup=keyboard, parse_mode="HTML")
@@ -2218,6 +2515,7 @@ async def handle_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) ->
"menu_promo": cb_menu_promo,
"menu_credits": cb_menu_credits,
"menu_admins": cb_menu_admins,
+ "menu_users": cb_menu_users,
"menu_remove": cb_menu_remove,
"install_mode_lite": cb_install_mode_lite,
"install_mode_pro": cb_install_mode_pro,
@@ -2251,6 +2549,14 @@ async def handle_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) ->
# Pattern-based handlers
if data.startswith("lite_dom_"):
await cb_lite_domain(update, context)
+ elif data == "user_add":
+ await cb_user_add(update, context)
+ elif data.startswith("user_view_"):
+ await cb_user_view(update, context)
+ elif data.startswith("user_del_yes_"):
+ await cb_user_delete_confirm(update, context)
+ elif data.startswith("user_del_"):
+ await cb_user_delete(update, context)
elif data.startswith("pro_cat_"):
await cb_pro_category(update, context)
elif data.startswith("pro_tpl_"):
@@ -2277,6 +2583,10 @@ async def handle_text_message(update: Update, context: ContextTypes.DEFAULT_TYPE
if not is_user_allowed(update.effective_user.id):
return
user_id = update.effective_user.id
+ if context.user_data.pop("awaiting_user_name", False):
+ await create_user_from_text(update, context, update.message.text.strip())
+ return
+
# Only act when we're explicitly waiting for a custom-git URL
if not _CUSTOM_GIT_WAITERS.pop(user_id, False):
return
@@ -2295,6 +2605,24 @@ async def handle_text_message(update: Update, context: ContextTypes.DEFAULT_TYPE
config["template_id"] = tpl_id
config["template_source"] = url
save_json(GOTELEGRAM_CONFIG, config)
+ if config.get("mode") == "pro" and os.path.isdir(info):
+ try:
+ os.makedirs(WEBSITE_ROOT, exist_ok=True)
+ for entry in os.listdir(WEBSITE_ROOT):
+ path = os.path.join(WEBSITE_ROOT, entry)
+ if os.path.isdir(path) and not os.path.islink(path):
+ shutil.rmtree(path)
+ else:
+ os.remove(path)
+ for entry in os.listdir(info):
+ src = os.path.join(info, entry)
+ dst = os.path.join(WEBSITE_ROOT, entry)
+ if os.path.isdir(src):
+ shutil.copytree(src, dst)
+ else:
+ shutil.copy2(src, dst)
+ except OSError as e:
+ logger.error("custom template deploy failed: %s", e)
await update.message.reply_text(
_tf(user_id, "cg_ok_fmt", html.escape(tpl_id)),
reply_markup=get_main_menu(user_id),
diff --git a/gotelegram-bot/config.example.env b/gotelegram-bot/config.example.env
index a2bf535..55fb309 100644
--- a/gotelegram-bot/config.example.env
+++ b/gotelegram-bot/config.example.env
@@ -1,4 +1,4 @@
-# GoTelegram v2.2 Bot Configuration
+# GoTelegram v2.5.0 Bot Configuration
# Copy this to .env and fill in your values
# Telegram Bot Token from @BotFather
diff --git a/gotelegram-bot/i18n.py b/gotelegram-bot/i18n.py
index 83151fe..ede5fa4 100644
--- a/gotelegram-bot/i18n.py
+++ b/gotelegram-bot/i18n.py
@@ -1,5 +1,5 @@
"""
-GoTelegram v2.4 Bot — i18n module
+GoTelegram v2.5.0 Bot — i18n module
Provides per-user language preferences and a simple t()/tf() API.
Usage:
diff --git a/gotelegram-bot/lang/en.json b/gotelegram-bot/lang/en.json
index 5ae9002..647ae6a 100644
--- a/gotelegram-bot/lang/en.json
+++ b/gotelegram-bot/lang/en.json
@@ -33,6 +33,7 @@
"menu_website": "🌐 Website/SSL",
"menu_promo": "🎁 Promo",
"menu_stats": "📊 Traffic Stats",
+ "menu_users": "🔑 Keys",
"menu_remove": "🗑️ Remove",
"menu_admins": "👤 Admins",
"menu_credits": "ℹ️ Credits",
diff --git a/gotelegram-bot/lang/ru.json b/gotelegram-bot/lang/ru.json
index a8a195f..ed32566 100644
--- a/gotelegram-bot/lang/ru.json
+++ b/gotelegram-bot/lang/ru.json
@@ -33,6 +33,7 @@
"menu_website": "🌐 Сайт/SSL",
"menu_promo": "🎁 Промо",
"menu_stats": "📊 Трафик",
+ "menu_users": "🔑 Ключи",
"menu_remove": "🗑️ Удалить",
"menu_admins": "👤 Админы",
"menu_credits": "ℹ️ О проекте",
diff --git a/install.sh b/install.sh
index 7204897..fad9459 100755
--- a/install.sh
+++ b/install.sh
@@ -1,6 +1,6 @@
#!/bin/bash
# ══════════════════════════════════════════════════════════════════════════════
-# GoTelegram v2.4.0 — MTProxy powered by telemt (Rust + Tokio)
+# GoTelegram v2.5.0 — MTProxy powered by telemt (Rust + Tokio)
# Anti-DPI • Fake TLS • TCP Splice • JA3/JA4 Resistance • i18n (EN/RU)
#
# Install:
@@ -294,6 +294,9 @@ install_lite_mode() {
local port
port=$(select_port)
[ $? -ne 0 ] && return
+ if [ "$port" = "443" ]; then
+ warn_3xui_443_conflict || true
+ fi
# Generate secret
local secret
@@ -342,6 +345,8 @@ install_lite_mode() {
install_pro_mode() {
log_step "$(t install_pro_step)"
+ warn_3xui_443_conflict || true
+
# Enter domain
echo ""
echo -ne " ${WHITE}$(t install_enter_domain)${NC} "
@@ -536,6 +541,21 @@ menu_logs() {
}
# ── Change mode / template ──────────────────────────────────────────────────
+update_current_template_id() {
+ local template_dir="$1"
+ local tpl_id
+ tpl_id=$(basename "$template_dir")
+ [ -z "$tpl_id" ] && return 0
+
+ if [ -f "$template_dir/.custom_git_source" ]; then
+ local source_url
+ source_url=$(head -1 "$template_dir/.custom_git_source" 2>/dev/null || echo "")
+ bot_update_config_field "template_source" "$source_url" || true
+ fi
+ bot_update_config_field "template_id" "$tpl_id" || \
+ log_warning "Не удалось обновить template_id в config.json"
+}
+
menu_change_mode() {
local current_mode
current_mode=$(config_get mode 2>/dev/null)
@@ -558,6 +578,7 @@ menu_change_mode() {
template_dir=$(interactive_template_selection)
[ $? -ne 0 ] && return
switch_template "$template_dir"
+ update_current_template_id "$template_dir"
;;
2)
log_warning "$(t change_requires_reinstall)"
@@ -601,6 +622,7 @@ menu_website() {
template_dir=$(interactive_template_selection)
[ $? -ne 0 ] && return
switch_template "$template_dir"
+ update_current_template_id "$template_dir"
;;
esac
}
diff --git a/install_gotelegram_bot.sh b/install_gotelegram_bot.sh
index bb0f1a6..eb73c5a 100755
--- a/install_gotelegram_bot.sh
+++ b/install_gotelegram_bot.sh
@@ -1,5 +1,5 @@
#!/bin/bash
-# GoTelegram v2.2.1 — Установка Telegram-бота
+# GoTelegram v2.5.0 — Установка Telegram-бота
# Создаёт venv, ставит зависимости, настраивает systemd
set -e
@@ -19,7 +19,7 @@ if [ "$EUID" -ne 0 ]; then
fi
echo -e "${CYAN}╔═══════════════════════════════════════════╗${NC}"
-echo -e "${CYAN}║${NC} ${GREEN}GoTelegram v2.2.1 — Установка бота${NC} ${CYAN}║${NC}"
+echo -e "${CYAN}║${NC} ${GREEN}GoTelegram v2.5.0 — Установка бота${NC} ${CYAN}║${NC}"
echo -e "${CYAN}╚═══════════════════════════════════════════╝${NC}"
echo ""
@@ -93,7 +93,7 @@ fi
# ── Systemd ──────────────────────────────────────────────────────────────────
cat > "/etc/systemd/system/${SERVICE_NAME}.service" << EOF
[Unit]
-Description=GoTelegram v2.2.1 Telegram Bot
+Description=GoTelegram v2.5.0 Telegram Bot
After=network.target
[Service]
diff --git a/lib/backup.sh b/lib/backup.sh
index f5d3cd6..2b44667 100755
--- a/lib/backup.sh
+++ b/lib/backup.sh
@@ -1,5 +1,5 @@
#!/bin/bash
-# GoTelegram v2.4 — backup and restore (i18n-aware)
+# GoTelegram v2.5.0 — backup and restore (i18n-aware)
# ── Создание бекапа ──────────────────────────────────────────────────────────
create_backup() {
@@ -35,13 +35,16 @@ create_backup() {
cp "$NGINX_SITE_CONF" "$tmp_dir/nginx.conf"
fi
- # SSL сертификаты
+ # SSL сертификаты и renewal metadata для переносов между VPS
local domain
domain=$(config_get domain 2>/dev/null)
if [ -n "$domain" ] && [ -d "/etc/letsencrypt/live/$domain" ]; then
- mkdir -p "$tmp_dir/certs"
- cp "/etc/letsencrypt/live/$domain/fullchain.pem" "$tmp_dir/certs/" 2>/dev/null
- cp "/etc/letsencrypt/live/$domain/privkey.pem" "$tmp_dir/certs/" 2>/dev/null
+ mkdir -p "$tmp_dir/letsencrypt/live" "$tmp_dir/letsencrypt/archive" "$tmp_dir/letsencrypt/renewal"
+ cp -a "/etc/letsencrypt/live/$domain" "$tmp_dir/letsencrypt/live/" 2>/dev/null
+ [ -d "/etc/letsencrypt/archive/$domain" ] && \
+ cp -a "/etc/letsencrypt/archive/$domain" "$tmp_dir/letsencrypt/archive/" 2>/dev/null
+ [ -f "/etc/letsencrypt/renewal/$domain.conf" ] && \
+ cp -a "/etc/letsencrypt/renewal/$domain.conf" "$tmp_dir/letsencrypt/renewal/" 2>/dev/null
log_dim "SSL сертификаты включены"
fi
@@ -52,6 +55,28 @@ create_backup() {
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
+
+ # 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
+
# Метаданные
local ip mode engine lang port domain
ip=$(get_server_ip)
@@ -65,7 +90,7 @@ create_backup() {
cat > "$tmp_dir/metadata.json" << EOMETA
{
- "backup_version": "1.1",
+ "backup_version": "1.2",
"gotelegram_version": "$GOTELEGRAM_VERSION",
"created_at": "$(date -Iseconds)",
"hostname": "$(hostname)",
@@ -231,8 +256,14 @@ restore_backup() {
log_success "$(_t_or backup_restored_nginx 'nginx конфиг восстановлен')"
fi
- # Восстанавливаем SSL
- if [ -d "$backup_dir/certs" ]; then
+ # Восстанавливаем SSL / Let's Encrypt structure
+ if [ -d "$backup_dir/letsencrypt" ]; then
+ mkdir -p /etc/letsencrypt/live /etc/letsencrypt/archive /etc/letsencrypt/renewal
+ [ -d "$backup_dir/letsencrypt/live" ] && cp -a "$backup_dir/letsencrypt/live/." /etc/letsencrypt/live/ 2>/dev/null
+ [ -d "$backup_dir/letsencrypt/archive" ] && cp -a "$backup_dir/letsencrypt/archive/." /etc/letsencrypt/archive/ 2>/dev/null
+ [ -d "$backup_dir/letsencrypt/renewal" ] && cp -a "$backup_dir/letsencrypt/renewal/." /etc/letsencrypt/renewal/ 2>/dev/null
+ log_success "$(_t_or backup_restored_ssl 'SSL сертификаты восстановлены')"
+ elif [ -d "$backup_dir/certs" ]; then
local domain
domain=$(config_get domain 2>/dev/null)
if [ -n "$domain" ]; then
@@ -251,11 +282,38 @@ restore_backup() {
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 [ -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
+
# Запускаем сервисы
+ 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
# Очистка
rm -rf "$tmp_dir"
diff --git a/lib/common.sh b/lib/common.sh
index b5291c8..0a02eef 100755
--- a/lib/common.sh
+++ b/lib/common.sh
@@ -1,9 +1,9 @@
#!/bin/bash
-# GoTelegram v2.4 — common utilities
+# GoTelegram v2.5.0 — common utilities
# Colors, logging, spinner, system helpers, v1 compat, i18n-aware
# ── Version ───────────────────────────────────────────────────────────────────
-GOTELEGRAM_VERSION="2.4.6"
+GOTELEGRAM_VERSION="2.5.0"
GOTELEGRAM_NAME="GoTelegram"
# ── Пути ──────────────────────────────────────────────────────────────────────
@@ -333,6 +333,7 @@ apt_pkg_for_cmd() {
ss) echo "iproute2" ;;
netstat) echo "net-tools" ;;
flock) echo "util-linux" ;;
+ iptables) echo "iptables" ;;
*) echo "$1" ;; # команда == имя пакета
esac
}
@@ -344,6 +345,7 @@ dnf_pkg_for_cmd() {
ss) echo "iproute" ;;
netstat) echo "net-tools" ;;
flock) echo "util-linux" ;;
+ iptables) echo "iptables" ;;
*) echo "$1" ;;
esac
}
@@ -355,7 +357,7 @@ ensure_deps() {
# change-lite-domain из бота).
local critical=(curl jq openssl git xxd tar dig flock)
# Желательные — есть fallback, устанавливать всё равно, но не падать если не смогли
- local optional=(qrencode bc)
+ local optional=(qrencode bc iptables)
local missing_critical=() missing_optional=() cmd
for cmd in "${critical[@]}"; do
@@ -463,6 +465,46 @@ 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 не будет молча останавливать или переписывать 3x-ui."
+ log_dim "Для настоящего shared-443 нужен один фронтовой TLS/SNI-диспетчер и разные SNI-домены для Xray и GoTelegram."
+ mkdir -p "$GOTELEGRAM_DIR" 2>/dev/null
+ cat > "$GOTELEGRAM_DIR/shared-443-3xui.md" <<'EOF' 2>/dev/null || true
+# GoTelegram + 3x-ui on one TCP/443
+
+GoTelegram 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
+- GoTelegram telemt: 127.0.0.1:7443
+- 3x-ui/Xray inbound: 127.0.0.1:9443
+- GoTelegram nginx mask site: 127.0.0.1:8443
+
+The dispatcher must route Xray SNI domains to Xray and route the GoTelegram
+SNI domain to telemt. If Xray and GoTelegram use the same SNI domain, automatic
+sharing is not reliable: the first TLS ClientHello is intentionally identical.
+
+GoTelegram 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
diff --git a/lib/i18n.sh b/lib/i18n.sh
index ca8107a..6fc2767 100755
--- a/lib/i18n.sh
+++ b/lib/i18n.sh
@@ -1,5 +1,5 @@
#!/bin/bash
-# GoTelegram v2.4 — i18n engine
+# GoTelegram v2.5.0 — i18n engine
# Internationalization support: EN (English) / RU (Русский)
#
# Usage:
diff --git a/lib/lang/en.sh b/lib/lang/en.sh
index fd3046e..fc0f3d6 100755
--- a/lib/lang/en.sh
+++ b/lib/lang/en.sh
@@ -1,5 +1,5 @@
#!/bin/bash
-# GoTelegram v2.4 — English translations
+# GoTelegram v2.5.0 — English translations
# shellcheck disable=SC2034,SC2148
# ── Common words ────────────────────────────────────────────────────────
diff --git a/lib/lang/ru.sh b/lib/lang/ru.sh
index 6101c23..797b1fb 100755
--- a/lib/lang/ru.sh
+++ b/lib/lang/ru.sh
@@ -1,5 +1,5 @@
#!/bin/bash
-# GoTelegram v2.4 — Russian translations
+# GoTelegram v2.5.0 — Russian translations
# shellcheck disable=SC2034,SC2148
# ── Common words ────────────────────────────────────────────────────────
diff --git a/lib/stats.sh b/lib/stats.sh
index a52e382..f7bf753 100644
--- a/lib/stats.sh
+++ b/lib/stats.sh
@@ -1,5 +1,5 @@
#!/bin/bash
-# stats.sh — Traffic statistics module for GoTelegram
+# stats.sh — Traffic statistics module for GoTelegram v2.5.0
# Tracks proxy (telemt port 443) and site (nginx port 8443) traffic
# Uses iptables counters + real-time snapshots + historical CSV
@@ -18,6 +18,11 @@ CONFIG_FILE="/opt/gotelegram/config.json"
# 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
@@ -57,6 +62,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)
@@ -350,6 +362,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")
diff --git a/lib/telemt.sh b/lib/telemt.sh
index 3af9484..91a8d80 100755
--- a/lib/telemt.sh
+++ b/lib/telemt.sh
@@ -1,5 +1,5 @@
#!/bin/bash
-# GoTelegram v2.2 — Управление telemt binary
+# GoTelegram v2.5.0 — Управление telemt binary
# Скачивание, обновление, запуск, остановка через systemd
TELEMT_GITHUB="telemt/telemt"
diff --git a/lib/telemt_config.sh b/lib/telemt_config.sh
index 01d3482..6ae502c 100755
--- a/lib/telemt_config.sh
+++ b/lib/telemt_config.sh
@@ -1,5 +1,5 @@
#!/bin/bash
-# GoTelegram v2.2 — Генерация TOML конфигурации для telemt
+# GoTelegram v2.5.0 — Генерация TOML конфигурации для telemt
# ── Популярные домены (не заблокированные в РФ) ──────────────────────────────
QUICK_DOMAINS=(
@@ -48,15 +48,38 @@ generate_telemt_toml() {
# Сгенерировано: $(date -Iseconds)
# Режим: ${mask_mode}
+[general]
+use_middle_proxy = true
+log_level = "normal"
+
+[general.modes]
+classic = false
+secure = false
+tls = true
+
+[general.links]
+show = "*"
+public_port = ${port}
+
[server]
port = ${port}
listen_addr_ipv4 = "0.0.0.0"
+metrics_listen = "127.0.0.1:9090"
+metrics_whitelist = ["127.0.0.1/32", "::1/128"]
+
+[server.api]
+enabled = true
+listen = "127.0.0.1:9091"
+whitelist = ["127.0.0.1/32", "::1/128"]
+minimal_runtime_enabled = false
+minimal_runtime_cache_ttl_ms = 1000
[censorship]
tls_domain = "${mask_domain}"
mask = true
mask_port = ${mask_port}
tls_emulation = $([ "$mask_mode" = "pro" ] && echo "false" || echo "true")
+unknown_sni_action = "mask"
[access.users]
main = "${secret}"
@@ -106,21 +129,57 @@ 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 && $1 == "main" {
+ sub(/^[^=]*=[[:space:]]*"/, "")
+ sub(/".*$/, "")
+ print
+ exit
+ }
+ ' "$config" | tr -d ' '
;;
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
}
diff --git a/lib/templates_catalog.sh b/lib/templates_catalog.sh
index 01503a1..7c3eeca 100755
--- a/lib/templates_catalog.sh
+++ b/lib/templates_catalog.sh
@@ -1,5 +1,5 @@
#!/bin/bash
-# GoTelegram v2.4 — website templates catalog
+# GoTelegram v2.5.0 — website templates catalog
# Pick from ~1800 templates, preview links, git sparse-checkout downloads,
# + custom git URL templates (user-supplied public repos)
diff --git a/lib/website.sh b/lib/website.sh
index 9f01504..788386c 100755
--- a/lib/website.sh
+++ b/lib/website.sh
@@ -1,5 +1,5 @@
#!/bin/bash
-# GoTelegram v2.2 — Управление сайтом (nginx + certbot + шаблоны)
+# GoTelegram v2.5.0 — Управление сайтом (nginx + certbot + шаблоны)
# ── Установка nginx ──────────────────────────────────────────────────────────
install_nginx() {
@@ -39,7 +39,7 @@ generate_nginx_config() {
mkdir -p /etc/nginx/sites-available /etc/nginx/sites-enabled
cat > "$NGINX_SITE_CONF" << 'EONGINX'
-# GoTelegram v2.3 — nginx config
+# GoTelegram v2.5.0 — nginx config
# Pro: nginx на 127.0.0.1:8443 (внутренний), telemt на 0.0.0.0:443 (внешний)
# Обычный браузер → :443 → telemt → 127.0.0.1:8443 → nginx (сайт)