mirror of
https://github.com/anten-ka/gotelegram_pro.git
synced 2026-05-27 11:47:51 +00:00
v2.5.0: maintenance and bot user management
This commit is contained in:
@@ -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` (этот файл).
|
||||
|
||||
@@ -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`).
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
"menu_website": "🌐 Сайт/SSL",
|
||||
"menu_promo": "🎁 Промо",
|
||||
"menu_stats": "📊 Трафик",
|
||||
"menu_users": "🔑 Ключи",
|
||||
"menu_remove": "🗑️ Удалить",
|
||||
"menu_admins": "👤 Админы",
|
||||
"menu_credits": "ℹ️ О проекте",
|
||||
|
||||
24
install.sh
24
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
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/bin/bash
|
||||
# GoTelegram v2.4 — i18n engine
|
||||
# GoTelegram v2.5.0 — i18n engine
|
||||
# Internationalization support: EN (English) / RU (Русский)
|
||||
#
|
||||
# Usage:
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/bin/bash
|
||||
# GoTelegram v2.4 — English translations
|
||||
# GoTelegram v2.5.0 — English translations
|
||||
# shellcheck disable=SC2034,SC2148
|
||||
|
||||
# ── Common words ────────────────────────────────────────────────────────
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/bin/bash
|
||||
# GoTelegram v2.4 — Russian translations
|
||||
# GoTelegram v2.5.0 — Russian translations
|
||||
# shellcheck disable=SC2034,SC2148
|
||||
|
||||
# ── Common words ────────────────────────────────────────────────────────
|
||||
|
||||
22
lib/stats.sh
22
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")
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/bin/bash
|
||||
# GoTelegram v2.2 — Управление telemt binary
|
||||
# GoTelegram v2.5.0 — Управление telemt binary
|
||||
# Скачивание, обновление, запуск, остановка через systemd
|
||||
|
||||
TELEMT_GITHUB="telemt/telemt"
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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 (сайт)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user