v2.5.0: maintenance and bot user management

This commit is contained in:
Codex
2026-04-24 18:50:43 +03:00
parent b10ea54ce9
commit 7afeb59261
21 changed files with 618 additions and 70 deletions

View File

@@ -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` (этот файл).

View File

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

View File

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

View File

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

View File

@@ -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}(?<!-)\.)+"
r"(?!-)[A-Za-z0-9-]{2,63}(?<!-)$"
)
_USER_NAME_RE = re.compile(r"^[A-Za-z0-9_.-]{1,48}$")
async def run_bot_action(action: str, timeout: int = 300, **params) -> 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"<b>{_t(user_id, 'status_template')}:</b> {html.escape(str(tpl))}")
lines.append(f"<b>{_t(user_id, 'status_template')}:</b> {html.escape(template_display_name(str(tpl)))}")
if config.get("domain"):
lines.append(f"<b>{_t(user_id, 'status_domain')}:</b> {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"• <code>{html.escape(name)}</code>" for name in sorted(users))
else:
user_lines = "<i>Ключей пока нет</i>"
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: <code>{configured}</code>")
if active is not None:
bits.append(f"active: <code>{active}</code>")
if bits:
api_note = "\n\nAPI: " + ", ".join(bits)
text = (
"<b>🔑 Ключи пользователей</b>\n\n"
f"{user_lines}"
f"{api_note}\n\n"
"<i>Нажмите на пользователя, чтобы увидеть ссылку, статистику и действия.</i>"
)
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<pre>{html.escape(compact)}</pre>"
else:
details = "\n<i>Runtime API недоступен. Новые установки GoTelegram включают его автоматически.</i>"
link_line = html.escape(link) if link else "link unavailable"
return (
f"<b>👤 {html.escape(name)}</b>\n\n"
f"Secret: <code>{html.escape(secret)}</code>\n\n"
f"<b>Ссылка:</b>\n<code>{link_line}</code>\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 = (
"<b> Новый ключ</b>\n\n"
"Отправьте имя пользователя: латиница, цифры, <code>_ . -</code>, до 48 символов.\n"
"Пример: <code>ivan</code> или <code>family-1</code>."
)
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"Удалить ключ <code>{html.escape(name)}</code>?"
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"✅ Ключ <code>{html.escape(name)}</code> удалён.",
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"✅ <b>Ключ создан</b>\n\n"
f"Пользователь: <code>{html.escape(name)}</code>\n"
f"Secret: <code>{secret}</code>\n\n"
f"<code>{html.escape(link or '')}</code>",
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)
"✅ <b>v1 container stopped and removed</b>\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),

View File

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

View File

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

View File

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

View File

@@ -33,6 +33,7 @@
"menu_website": "🌐 Сайт/SSL",
"menu_promo": "🎁 Промо",
"menu_stats": "📊 Трафик",
"menu_users": "🔑 Ключи",
"menu_remove": "🗑️ Удалить",
"menu_admins": "👤 Админы",
"menu_credits": " О проекте",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
#!/bin/bash
# GoTelegram v2.4 — English translations
# GoTelegram v2.5.0 — English translations
# shellcheck disable=SC2034,SC2148
# ── Common words ────────────────────────────────────────────────────────

View File

@@ -1,5 +1,5 @@
#!/bin/bash
# GoTelegram v2.4 — Russian translations
# GoTelegram v2.5.0 — Russian translations
# shellcheck disable=SC2034,SC2148
# ── Common words ────────────────────────────────────────────────────────

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

View File

@@ -1,5 +1,5 @@
#!/bin/bash
# GoTelegram v2.2 — Управление telemt binary
# GoTelegram v2.5.0 — Управление telemt binary
# Скачивание, обновление, запуск, остановка через systemd
TELEMT_GITHUB="telemt/telemt"

View File

@@ -1,5 +1,5 @@
#!/bin/bash
# GoTelegram v2.2 — Генерация TOML конфигурации для telemt
# GoTelegram v2.5.0 — Генерация TOML конфигурации для telemt
# ── Популярные домены (не заблокированные в РФ) ──────────────────────────────
QUICK_DOMAINS=(
@@ -48,15 +48,38 @@ generate_telemt_toml() {
# Сгенерировано: $(date -Iseconds)
# Режим: ${mask_mode}
[general]
use_middle_proxy = true
log_level = "normal"
[general.modes]
classic = false
secure = false
tls = true
[general.links]
show = "*"
public_port = ${port}
[server]
port = ${port}
listen_addr_ipv4 = "0.0.0.0"
metrics_listen = "127.0.0.1:9090"
metrics_whitelist = ["127.0.0.1/32", "::1/128"]
[server.api]
enabled = true
listen = "127.0.0.1:9091"
whitelist = ["127.0.0.1/32", "::1/128"]
minimal_runtime_enabled = false
minimal_runtime_cache_ttl_ms = 1000
[censorship]
tls_domain = "${mask_domain}"
mask = true
mask_port = ${mask_port}
tls_emulation = $([ "$mask_mode" = "pro" ] && echo "false" || echo "true")
unknown_sni_action = "mask"
[access.users]
main = "${secret}"
@@ -106,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
}

View File

@@ -1,5 +1,5 @@
#!/bin/bash
# GoTelegram v2.4 — website templates catalog
# GoTelegram v2.5.0 — website templates catalog
# Pick from ~1800 templates, preview links, git sparse-checkout downloads,
# + custom git URL templates (user-supplied public repos)

View File

@@ -1,5 +1,5 @@
#!/bin/bash
# GoTelegram v2.2 — Управление сайтом (nginx + certbot + шаблоны)
# GoTelegram v2.5.0 — Управление сайтом (nginx + certbot + шаблоны)
# ── Установка nginx ──────────────────────────────────────────────────────────
install_nginx() {
@@ -39,7 +39,7 @@ generate_nginx_config() {
mkdir -p /etc/nginx/sites-available /etc/nginx/sites-enabled
cat > "$NGINX_SITE_CONF" << 'EONGINX'
# GoTelegram v2.3 — nginx config
# GoTelegram v2.5.0 — nginx config
# Pro: nginx на 127.0.0.1:8443 (внутренний), telemt на 0.0.0.0:443 (внешний)
# Обычный браузер → :443 → telemt → 127.0.0.1:8443 → nginx (сайт)