mirror of
https://github.com/anten-ka/gotelegram_pro.git
synced 2026-06-10 18:32:47 +00:00
v2.5.0: maintenance and bot user management
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
# GoTelegram Pro — техническая документация для ИИ-агентов
|
# GoTelegram Pro — техническая документация для ИИ-агентов
|
||||||
|
|
||||||
**Версия:** 2.4.3
|
**Версия:** 2.5.0
|
||||||
**Репозиторий:** `anten-ka/gotelegram_pro`
|
**Репозиторий:** `anten-ka/gotelegram_pro`
|
||||||
**Активная ветка:** `alfa-test` (ветка `test` заморожена и содержит stable для конечных пользователей)
|
**Активная ветка:** `alfa-test` (ветка `test` заморожена и содержит stable для конечных пользователей)
|
||||||
**Целевая ОС:** Ubuntu 20.04+, Debian 11+
|
**Целевая ОС:** 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`.
|
Коды ошибок: `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).
|
1. Одновременных callback'ах внутри бота (asyncio.Lock уже ловит это, но flock — defense-in-depth).
|
||||||
2. Параллельных CLI-вызовах (бот + ручной SSH, или два бот-процесса — теоретически).
|
2. Параллельных CLI-вызовах (бот + ручной SSH, или два бот-процесса — теоретически).
|
||||||
|
|
||||||
@@ -446,7 +446,7 @@ switch_language ru|en
|
|||||||
3. Напиши `C:\Temp\push_<описание>.py`:
|
3. Напиши `C:\Temp\push_<описание>.py`:
|
||||||
```python
|
```python
|
||||||
import os, base64, json, urllib.request, ssl
|
import os, base64, json, urllib.request, ssl
|
||||||
TOKEN = "github_pat_..."
|
TOKEN = os.environ["GOTELEGRAM_PAT"]
|
||||||
REPO = "anten-ka/gotelegram_pro"
|
REPO = "anten-ka/gotelegram_pro"
|
||||||
BRANCH = "alfa-test"
|
BRANCH = "alfa-test"
|
||||||
API = f"https://api.github.com/repos/{REPO}"
|
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
|
## 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.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.2 (2026-04-10)** — реализация non-interactive `bot_action_*` в install.sh (change-template + change-lite-domain с JSON-ответом). bot.py подключает `run_bot_action()` и делает реальную работу вместо stub'ов. Критфиксы: (a) `safe_edit_message` принимает `disable_web_page_preview` (иначе TypeError в success-пути cb_pro_confirm); (b) чтение/запись `config['template_id']` вместо `config['template']` (save_gotelegram_config всегда писал `template_id`, бот смотрел не туда); (c) `bot_update_config_field` использует shell `date -Iseconds` вместо `jq now|todate` (jq 1.5 совместимость для Debian 10); (d) `asyncio.Lock _BOT_ACTION_LOCK` сериализует callback'и в процессе бота; (e) валидация `_TPL_ID_RE`/`_DOMAIN_RE` до subprocess. Полный аудит и автоустановка зависимостей: `ensure_deps` разделяет critical/optional, дедуплицирует пакеты, re-verify после install; `apt_pkg_for_cmd` и `dnf_pkg_for_cmd` мапят команды на пакеты (dig→dnsutils/bind-utils, xxd→xxd/vim-common, flock→util-linux). `check_deps_present` — быстрый чек без `apt-get update`.
|
||||||
- **2.4.1 (2026-04-10)** — баг #23: `start_telemt` делает `restart` если сервис активен (иначе stale in-memory config после переустановки Lite поверх Pro). Полная документация проекта — `DOCS_HUMAN.md` и `DOCS_AI.md` (этот файл).
|
- **2.4.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 — руководство пользователя
|
# GoTelegram Pro — руководство пользователя
|
||||||
|
|
||||||
**Версия:** 2.4.3
|
**Версия:** 2.5.0
|
||||||
**Репозиторий:** `anten-ka/gotelegram_pro`
|
**Репозиторий:** `anten-ka/gotelegram_pro`
|
||||||
**Для кого:** владельцы VPS, которым нужен надёжный MTProxy для Telegram с маскировкой под обычный HTTPS-сайт.
|
**Для кого:** владельцы 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 <(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` уже будет работать откуда угодно.
|
`bootstrap.sh` скачает все файлы из приватного репозитория, создаст симлинк `/usr/local/bin/gotelegram` и запустит главное меню. Через минуту команда `gotelegram` уже будет работать откуда угодно.
|
||||||
|
|
||||||
Дальше в меню:
|
Дальше в меню:
|
||||||
@@ -214,6 +220,8 @@ A: Сам MTProxy — да, это публичная технология из
|
|||||||
|
|
||||||
## Changelog (коротко)
|
## 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.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.2** — смена шаблона и домена маскировки **прямо из Telegram-бота** без SSH. Раньше эти пункты меню показывали сообщение «сделай через CLI», теперь бот вызывает `install.sh --action=change-template --json` / `--action=change-lite-domain --json` и разбирает ответ. Плюс: безопасный `safe_edit_message` принимает `disable_web_page_preview`; поле статуса шаблона наконец-то отображается (раньше читалось не из того ключа JSON); полный аудит и автоустановка системных зависимостей при первом запуске (`curl jq openssl git xxd tar dig flock` + опциональные `qrencode bc`); `asyncio.Lock` в боте сериализует параллельные callback'и; валидация tpl\_id (`[A-Za-z0-9_-]{1,64}`) и домена до subprocess.
|
||||||
- **2.4.1** — фикс: `start_telemt` теперь делает `restart` если сервис уже запущен. Раньше переустановка Lite поверх Pro оставляла в памяти старый конфиг, и клиенты получали «Unknown TLS SNI drop». Плюс полная документация проекта (этот файл и `DOCS_AI.md`).
|
- **2.4.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"
|
REPO="anten-ka/gotelegram_pro"
|
||||||
BRANCH="${GOTELEGRAM_BRANCH:-test}"
|
BRANCH="${GOTELEGRAM_BRANCH:-test}"
|
||||||
PAT="${GOTELEGRAM_PAT:-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}"
|
PAT="${GOTELEGRAM_PAT:-}"
|
||||||
INSTALL_DIR="/opt/gotelegram"
|
INSTALL_DIR="/opt/gotelegram"
|
||||||
# Use raw.githubusercontent.com (CDN) — faster and avoids Contents API caching
|
# Use raw.githubusercontent.com (CDN) — faster and avoids Contents API caching
|
||||||
# issues that occasionally return 404 for recently added files on non-default branches.
|
# 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
|
exit 1
|
||||||
fi
|
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
|
# Check dependencies
|
||||||
for cmd in curl jq; do
|
for cmd in curl jq; do
|
||||||
if ! command -v "$cmd" &>/dev/null; then
|
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.
|
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
|
- Promotional links
|
||||||
|
|
||||||
- **Template Browsing** - Browse categories → templates → preview → install
|
- **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
|
- **V1 Migration** - Detects old mtg Docker container and offers migration
|
||||||
- **Access Control** - ALLOWED_IDS from .env
|
- **Access Control** - ALLOWED_IDS from .env
|
||||||
- **Async/Await** - Full async support via python-telegram-bot v21+
|
- **Async/Await** - Full async support via python-telegram-bot v21+
|
||||||
@@ -144,4 +145,4 @@ code, stdout, stderr = await sh("command", "arg1", "arg2")
|
|||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
GoTelegram v2.2 - Open source community project
|
GoTelegram v2.5.0 - Open source community project
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
#!/usr/bin/env python3
|
#!/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
|
Manages telemt engine via Telegram interface with full CLI feature parity
|
||||||
Uses python-telegram-bot v21+
|
Uses python-telegram-bot v21+
|
||||||
Supports EN/RU UI with per-user language preferences.
|
Supports EN/RU UI with per-user language preferences.
|
||||||
@@ -23,6 +23,7 @@ from datetime import datetime
|
|||||||
from io import StringIO
|
from io import StringIO
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Tuple, Optional, List, Dict, Any
|
from typing import Tuple, Optional, List, Dict, Any
|
||||||
|
from urllib.parse import quote
|
||||||
|
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
from telegram import (
|
from telegram import (
|
||||||
@@ -100,7 +101,7 @@ logger = logging.getLogger(__name__)
|
|||||||
# CONFIGURATION
|
# CONFIGURATION
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
GOTELEGRAM_VERSION = "2.4.6"
|
GOTELEGRAM_VERSION = "2.5.0"
|
||||||
GOTELEGRAM_CONFIG = "/opt/gotelegram/config.json"
|
GOTELEGRAM_CONFIG = "/opt/gotelegram/config.json"
|
||||||
TELEMT_CONFIG = "/etc/telemt/config.toml"
|
TELEMT_CONFIG = "/etc/telemt/config.toml"
|
||||||
TELEMT_SERVICE = "telemt"
|
TELEMT_SERVICE = "telemt"
|
||||||
@@ -256,6 +257,7 @@ _DOMAIN_RE = re.compile(
|
|||||||
r"^(?=.{1,253}$)(?:(?!-)[A-Za-z0-9-]{1,63}(?<!-)\.)+"
|
r"^(?=.{1,253}$)(?:(?!-)[A-Za-z0-9-]{1,63}(?<!-)\.)+"
|
||||||
r"(?!-)[A-Za-z0-9-]{2,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:
|
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
|
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(
|
async def safe_edit_message(
|
||||||
query,
|
query,
|
||||||
text: str,
|
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_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_remove"), callback_data="menu_remove"),
|
||||||
],
|
|
||||||
[
|
|
||||||
InlineKeyboardButton(_t(user_id, "menu_admins"), callback_data="menu_admins"),
|
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_language"), callback_data="menu_lang"),
|
||||||
|
],
|
||||||
|
[
|
||||||
InlineKeyboardButton(_t(user_id, "menu_close"), callback_data="close_menu"),
|
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")
|
# install.sh/save_gotelegram_config uses "template_id" (not "template")
|
||||||
tpl = config.get("template_id") or config.get("template")
|
tpl = config.get("template_id") or config.get("template")
|
||||||
if tpl:
|
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"):
|
if config.get("domain"):
|
||||||
lines.append(f"<b>{_t(user_id, 'status_domain')}:</b> {html.escape(str(config['domain']))}")
|
lines.append(f"<b>{_t(user_id, 'status_domain')}:</b> {html.escape(str(config['domain']))}")
|
||||||
if config.get("port"):
|
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:
|
async def get_traffic_stats() -> str:
|
||||||
"""Get formatted traffic statistics."""
|
"""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
|
# Read current snapshot
|
||||||
current_file = "/run/gotelegram/stats_current.json"
|
current_file = "/run/gotelegram/stats_current.json"
|
||||||
history_file = "/opt/gotelegram/stats_history.csv"
|
history_file = "/opt/gotelegram/stats_history.csv"
|
||||||
@@ -706,6 +736,8 @@ async def get_traffic_stats() -> str:
|
|||||||
reader = csv.reader(f)
|
reader = csv.reader(f)
|
||||||
for row in reader:
|
for row in reader:
|
||||||
if len(row) >= 3:
|
if len(row) >= 3:
|
||||||
|
if not row[0].isdigit():
|
||||||
|
continue
|
||||||
history.append({
|
history.append({
|
||||||
"ts": int(row[0]),
|
"ts": int(row[0]),
|
||||||
"proxy": int(row[1]),
|
"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."""
|
"""Install mode selection menu."""
|
||||||
buttons = [
|
buttons = [
|
||||||
[InlineKeyboardButton("⚡ Lite", callback_data="install_mode_lite")],
|
[InlineKeyboardButton("⚡ Lite", callback_data="install_mode_lite")],
|
||||||
[InlineKeyboardButton("🛡 Pro", callback_data="install_mode_pro")],
|
[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)
|
return InlineKeyboardMarkup(buttons)
|
||||||
|
|
||||||
@@ -858,7 +890,7 @@ async def cb_menu_install(update: Update, context: ContextTypes.DEFAULT_TYPE) ->
|
|||||||
keyboard = InlineKeyboardMarkup(buttons)
|
keyboard = InlineKeyboardMarkup(buttons)
|
||||||
else:
|
else:
|
||||||
text = "Select installation mode:"
|
text = "Select installation mode:"
|
||||||
keyboard = get_install_mode_menu()
|
keyboard = get_install_mode_menu(_uid(update))
|
||||||
|
|
||||||
await safe_edit_message(query,
|
await safe_edit_message(query,
|
||||||
text, reply_markup=keyboard, parse_mode="HTML"
|
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
|
# 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]:
|
async def get_proxy_link() -> Optional[str]:
|
||||||
"""Generate proxy link from config. Pro-mode uses domain + fake-TLS secret."""
|
"""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:
|
if not secret:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
mode = config.get("mode", "lite")
|
return await get_proxy_link_for_secret(secret)
|
||||||
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}"
|
|
||||||
|
|
||||||
|
|
||||||
async def cb_menu_link(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
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
|
# 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"
|
"✅ <b>v1 container stopped and removed</b>\n\n"
|
||||||
"Now select installation mode for v2:"
|
"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")
|
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_promo": cb_menu_promo,
|
||||||
"menu_credits": cb_menu_credits,
|
"menu_credits": cb_menu_credits,
|
||||||
"menu_admins": cb_menu_admins,
|
"menu_admins": cb_menu_admins,
|
||||||
|
"menu_users": cb_menu_users,
|
||||||
"menu_remove": cb_menu_remove,
|
"menu_remove": cb_menu_remove,
|
||||||
"install_mode_lite": cb_install_mode_lite,
|
"install_mode_lite": cb_install_mode_lite,
|
||||||
"install_mode_pro": cb_install_mode_pro,
|
"install_mode_pro": cb_install_mode_pro,
|
||||||
@@ -2251,6 +2549,14 @@ async def handle_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) ->
|
|||||||
# Pattern-based handlers
|
# Pattern-based handlers
|
||||||
if data.startswith("lite_dom_"):
|
if data.startswith("lite_dom_"):
|
||||||
await cb_lite_domain(update, context)
|
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_"):
|
elif data.startswith("pro_cat_"):
|
||||||
await cb_pro_category(update, context)
|
await cb_pro_category(update, context)
|
||||||
elif data.startswith("pro_tpl_"):
|
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):
|
if not is_user_allowed(update.effective_user.id):
|
||||||
return
|
return
|
||||||
user_id = update.effective_user.id
|
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
|
# Only act when we're explicitly waiting for a custom-git URL
|
||||||
if not _CUSTOM_GIT_WAITERS.pop(user_id, False):
|
if not _CUSTOM_GIT_WAITERS.pop(user_id, False):
|
||||||
return
|
return
|
||||||
@@ -2295,6 +2605,24 @@ async def handle_text_message(update: Update, context: ContextTypes.DEFAULT_TYPE
|
|||||||
config["template_id"] = tpl_id
|
config["template_id"] = tpl_id
|
||||||
config["template_source"] = url
|
config["template_source"] = url
|
||||||
save_json(GOTELEGRAM_CONFIG, config)
|
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(
|
await update.message.reply_text(
|
||||||
_tf(user_id, "cg_ok_fmt", html.escape(tpl_id)),
|
_tf(user_id, "cg_ok_fmt", html.escape(tpl_id)),
|
||||||
reply_markup=get_main_menu(user_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
|
# Copy this to .env and fill in your values
|
||||||
|
|
||||||
# Telegram Bot Token from @BotFather
|
# 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.
|
Provides per-user language preferences and a simple t()/tf() API.
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
|
|||||||
@@ -33,6 +33,7 @@
|
|||||||
"menu_website": "🌐 Website/SSL",
|
"menu_website": "🌐 Website/SSL",
|
||||||
"menu_promo": "🎁 Promo",
|
"menu_promo": "🎁 Promo",
|
||||||
"menu_stats": "📊 Traffic Stats",
|
"menu_stats": "📊 Traffic Stats",
|
||||||
|
"menu_users": "🔑 Keys",
|
||||||
"menu_remove": "🗑️ Remove",
|
"menu_remove": "🗑️ Remove",
|
||||||
"menu_admins": "👤 Admins",
|
"menu_admins": "👤 Admins",
|
||||||
"menu_credits": "ℹ️ Credits",
|
"menu_credits": "ℹ️ Credits",
|
||||||
|
|||||||
@@ -33,6 +33,7 @@
|
|||||||
"menu_website": "🌐 Сайт/SSL",
|
"menu_website": "🌐 Сайт/SSL",
|
||||||
"menu_promo": "🎁 Промо",
|
"menu_promo": "🎁 Промо",
|
||||||
"menu_stats": "📊 Трафик",
|
"menu_stats": "📊 Трафик",
|
||||||
|
"menu_users": "🔑 Ключи",
|
||||||
"menu_remove": "🗑️ Удалить",
|
"menu_remove": "🗑️ Удалить",
|
||||||
"menu_admins": "👤 Админы",
|
"menu_admins": "👤 Админы",
|
||||||
"menu_credits": "ℹ️ О проекте",
|
"menu_credits": "ℹ️ О проекте",
|
||||||
|
|||||||
24
install.sh
24
install.sh
@@ -1,6 +1,6 @@
|
|||||||
#!/bin/bash
|
#!/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)
|
# Anti-DPI • Fake TLS • TCP Splice • JA3/JA4 Resistance • i18n (EN/RU)
|
||||||
#
|
#
|
||||||
# Install:
|
# Install:
|
||||||
@@ -294,6 +294,9 @@ install_lite_mode() {
|
|||||||
local port
|
local port
|
||||||
port=$(select_port)
|
port=$(select_port)
|
||||||
[ $? -ne 0 ] && return
|
[ $? -ne 0 ] && return
|
||||||
|
if [ "$port" = "443" ]; then
|
||||||
|
warn_3xui_443_conflict || true
|
||||||
|
fi
|
||||||
|
|
||||||
# Generate secret
|
# Generate secret
|
||||||
local secret
|
local secret
|
||||||
@@ -342,6 +345,8 @@ install_lite_mode() {
|
|||||||
install_pro_mode() {
|
install_pro_mode() {
|
||||||
log_step "$(t install_pro_step)"
|
log_step "$(t install_pro_step)"
|
||||||
|
|
||||||
|
warn_3xui_443_conflict || true
|
||||||
|
|
||||||
# Enter domain
|
# Enter domain
|
||||||
echo ""
|
echo ""
|
||||||
echo -ne " ${WHITE}$(t install_enter_domain)${NC} "
|
echo -ne " ${WHITE}$(t install_enter_domain)${NC} "
|
||||||
@@ -536,6 +541,21 @@ menu_logs() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# ── Change mode / template ──────────────────────────────────────────────────
|
# ── 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() {
|
menu_change_mode() {
|
||||||
local current_mode
|
local current_mode
|
||||||
current_mode=$(config_get mode 2>/dev/null)
|
current_mode=$(config_get mode 2>/dev/null)
|
||||||
@@ -558,6 +578,7 @@ menu_change_mode() {
|
|||||||
template_dir=$(interactive_template_selection)
|
template_dir=$(interactive_template_selection)
|
||||||
[ $? -ne 0 ] && return
|
[ $? -ne 0 ] && return
|
||||||
switch_template "$template_dir"
|
switch_template "$template_dir"
|
||||||
|
update_current_template_id "$template_dir"
|
||||||
;;
|
;;
|
||||||
2)
|
2)
|
||||||
log_warning "$(t change_requires_reinstall)"
|
log_warning "$(t change_requires_reinstall)"
|
||||||
@@ -601,6 +622,7 @@ menu_website() {
|
|||||||
template_dir=$(interactive_template_selection)
|
template_dir=$(interactive_template_selection)
|
||||||
[ $? -ne 0 ] && return
|
[ $? -ne 0 ] && return
|
||||||
switch_template "$template_dir"
|
switch_template "$template_dir"
|
||||||
|
update_current_template_id "$template_dir"
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# GoTelegram v2.2.1 — Установка Telegram-бота
|
# GoTelegram v2.5.0 — Установка Telegram-бота
|
||||||
# Создаёт venv, ставит зависимости, настраивает systemd
|
# Создаёт venv, ставит зависимости, настраивает systemd
|
||||||
|
|
||||||
set -e
|
set -e
|
||||||
@@ -19,7 +19,7 @@ if [ "$EUID" -ne 0 ]; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
echo -e "${CYAN}╔═══════════════════════════════════════════╗${NC}"
|
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 -e "${CYAN}╚═══════════════════════════════════════════╝${NC}"
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
@@ -93,7 +93,7 @@ fi
|
|||||||
# ── Systemd ──────────────────────────────────────────────────────────────────
|
# ── Systemd ──────────────────────────────────────────────────────────────────
|
||||||
cat > "/etc/systemd/system/${SERVICE_NAME}.service" << EOF
|
cat > "/etc/systemd/system/${SERVICE_NAME}.service" << EOF
|
||||||
[Unit]
|
[Unit]
|
||||||
Description=GoTelegram v2.2.1 Telegram Bot
|
Description=GoTelegram v2.5.0 Telegram Bot
|
||||||
After=network.target
|
After=network.target
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# GoTelegram v2.4 — backup and restore (i18n-aware)
|
# GoTelegram v2.5.0 — backup and restore (i18n-aware)
|
||||||
|
|
||||||
# ── Создание бекапа ──────────────────────────────────────────────────────────
|
# ── Создание бекапа ──────────────────────────────────────────────────────────
|
||||||
create_backup() {
|
create_backup() {
|
||||||
@@ -35,13 +35,16 @@ create_backup() {
|
|||||||
cp "$NGINX_SITE_CONF" "$tmp_dir/nginx.conf"
|
cp "$NGINX_SITE_CONF" "$tmp_dir/nginx.conf"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# SSL сертификаты
|
# SSL сертификаты и renewal metadata для переносов между VPS
|
||||||
local domain
|
local domain
|
||||||
domain=$(config_get domain 2>/dev/null)
|
domain=$(config_get domain 2>/dev/null)
|
||||||
if [ -n "$domain" ] && [ -d "/etc/letsencrypt/live/$domain" ]; then
|
if [ -n "$domain" ] && [ -d "/etc/letsencrypt/live/$domain" ]; then
|
||||||
mkdir -p "$tmp_dir/certs"
|
mkdir -p "$tmp_dir/letsencrypt/live" "$tmp_dir/letsencrypt/archive" "$tmp_dir/letsencrypt/renewal"
|
||||||
cp "/etc/letsencrypt/live/$domain/fullchain.pem" "$tmp_dir/certs/" 2>/dev/null
|
cp -a "/etc/letsencrypt/live/$domain" "$tmp_dir/letsencrypt/live/" 2>/dev/null
|
||||||
cp "/etc/letsencrypt/live/$domain/privkey.pem" "$tmp_dir/certs/" 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 сертификаты включены"
|
log_dim "SSL сертификаты включены"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -52,6 +55,28 @@ create_backup() {
|
|||||||
log_dim "$(_t_or backup_site_included 'Шаблон сайта включён')"
|
log_dim "$(_t_or backup_site_included 'Шаблон сайта включён')"
|
||||||
fi
|
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
|
local ip mode engine lang port domain
|
||||||
ip=$(get_server_ip)
|
ip=$(get_server_ip)
|
||||||
@@ -65,7 +90,7 @@ create_backup() {
|
|||||||
|
|
||||||
cat > "$tmp_dir/metadata.json" << EOMETA
|
cat > "$tmp_dir/metadata.json" << EOMETA
|
||||||
{
|
{
|
||||||
"backup_version": "1.1",
|
"backup_version": "1.2",
|
||||||
"gotelegram_version": "$GOTELEGRAM_VERSION",
|
"gotelegram_version": "$GOTELEGRAM_VERSION",
|
||||||
"created_at": "$(date -Iseconds)",
|
"created_at": "$(date -Iseconds)",
|
||||||
"hostname": "$(hostname)",
|
"hostname": "$(hostname)",
|
||||||
@@ -231,8 +256,14 @@ restore_backup() {
|
|||||||
log_success "$(_t_or backup_restored_nginx 'nginx конфиг восстановлен')"
|
log_success "$(_t_or backup_restored_nginx 'nginx конфиг восстановлен')"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Восстанавливаем SSL
|
# Восстанавливаем SSL / Let's Encrypt structure
|
||||||
if [ -d "$backup_dir/certs" ]; then
|
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
|
local domain
|
||||||
domain=$(config_get domain 2>/dev/null)
|
domain=$(config_get domain 2>/dev/null)
|
||||||
if [ -n "$domain" ]; then
|
if [ -n "$domain" ]; then
|
||||||
@@ -251,11 +282,38 @@ restore_backup() {
|
|||||||
log_success "$(_t_or backup_restored_site 'Шаблон сайта восстановлен')"
|
log_success "$(_t_or backup_restored_site 'Шаблон сайта восстановлен')"
|
||||||
fi
|
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
|
if is_telemt_installed; then
|
||||||
start_telemt
|
start_telemt
|
||||||
fi
|
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"
|
rm -rf "$tmp_dir"
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# GoTelegram v2.4 — common utilities
|
# GoTelegram v2.5.0 — common utilities
|
||||||
# Colors, logging, spinner, system helpers, v1 compat, i18n-aware
|
# Colors, logging, spinner, system helpers, v1 compat, i18n-aware
|
||||||
|
|
||||||
# ── Version ───────────────────────────────────────────────────────────────────
|
# ── Version ───────────────────────────────────────────────────────────────────
|
||||||
GOTELEGRAM_VERSION="2.4.6"
|
GOTELEGRAM_VERSION="2.5.0"
|
||||||
GOTELEGRAM_NAME="GoTelegram"
|
GOTELEGRAM_NAME="GoTelegram"
|
||||||
|
|
||||||
# ── Пути ──────────────────────────────────────────────────────────────────────
|
# ── Пути ──────────────────────────────────────────────────────────────────────
|
||||||
@@ -333,6 +333,7 @@ apt_pkg_for_cmd() {
|
|||||||
ss) echo "iproute2" ;;
|
ss) echo "iproute2" ;;
|
||||||
netstat) echo "net-tools" ;;
|
netstat) echo "net-tools" ;;
|
||||||
flock) echo "util-linux" ;;
|
flock) echo "util-linux" ;;
|
||||||
|
iptables) echo "iptables" ;;
|
||||||
*) echo "$1" ;; # команда == имя пакета
|
*) echo "$1" ;; # команда == имя пакета
|
||||||
esac
|
esac
|
||||||
}
|
}
|
||||||
@@ -344,6 +345,7 @@ dnf_pkg_for_cmd() {
|
|||||||
ss) echo "iproute" ;;
|
ss) echo "iproute" ;;
|
||||||
netstat) echo "net-tools" ;;
|
netstat) echo "net-tools" ;;
|
||||||
flock) echo "util-linux" ;;
|
flock) echo "util-linux" ;;
|
||||||
|
iptables) echo "iptables" ;;
|
||||||
*) echo "$1" ;;
|
*) echo "$1" ;;
|
||||||
esac
|
esac
|
||||||
}
|
}
|
||||||
@@ -355,7 +357,7 @@ ensure_deps() {
|
|||||||
# change-lite-domain из бота).
|
# change-lite-domain из бота).
|
||||||
local critical=(curl jq openssl git xxd tar dig flock)
|
local critical=(curl jq openssl git xxd tar dig flock)
|
||||||
# Желательные — есть fallback, устанавливать всё равно, но не падать если не смогли
|
# Желательные — есть fallback, устанавливать всё равно, но не падать если не смогли
|
||||||
local optional=(qrencode bc)
|
local optional=(qrencode bc iptables)
|
||||||
|
|
||||||
local missing_critical=() missing_optional=() cmd
|
local missing_critical=() missing_optional=() cmd
|
||||||
for cmd in "${critical[@]}"; do
|
for cmd in "${critical[@]}"; do
|
||||||
@@ -463,6 +465,46 @@ check_port() {
|
|||||||
return 1 # свободен
|
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() {
|
check_disk_space() {
|
||||||
local min_mb="${1:-500}"
|
local min_mb="${1:-500}"
|
||||||
local avail_mb
|
local avail_mb
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# GoTelegram v2.4 — i18n engine
|
# GoTelegram v2.5.0 — i18n engine
|
||||||
# Internationalization support: EN (English) / RU (Русский)
|
# Internationalization support: EN (English) / RU (Русский)
|
||||||
#
|
#
|
||||||
# Usage:
|
# Usage:
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# GoTelegram v2.4 — English translations
|
# GoTelegram v2.5.0 — English translations
|
||||||
# shellcheck disable=SC2034,SC2148
|
# shellcheck disable=SC2034,SC2148
|
||||||
|
|
||||||
# ── Common words ────────────────────────────────────────────────────────
|
# ── Common words ────────────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# GoTelegram v2.4 — Russian translations
|
# GoTelegram v2.5.0 — Russian translations
|
||||||
# shellcheck disable=SC2034,SC2148
|
# shellcheck disable=SC2034,SC2148
|
||||||
|
|
||||||
# ── Common words ────────────────────────────────────────────────────────
|
# ── Common words ────────────────────────────────────────────────────────
|
||||||
|
|||||||
22
lib/stats.sh
22
lib/stats.sh
@@ -1,5 +1,5 @@
|
|||||||
#!/bin/bash
|
#!/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
|
# Tracks proxy (telemt port 443) and site (nginx port 8443) traffic
|
||||||
# Uses iptables counters + real-time snapshots + historical CSV
|
# Uses iptables counters + real-time snapshots + historical CSV
|
||||||
|
|
||||||
@@ -18,6 +18,11 @@ CONFIG_FILE="/opt/gotelegram/config.json"
|
|||||||
|
|
||||||
# Initialize stats infrastructure
|
# Initialize stats infrastructure
|
||||||
stats_init() {
|
stats_init() {
|
||||||
|
if ! command -v iptables &>/dev/null; then
|
||||||
|
log_warning "iptables не найден: установите пакет iptables или запустите установку зависимостей"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
# Create runtime directory
|
# Create runtime directory
|
||||||
mkdir -p "$STATS_DIR" "$SNAPSHOTS_DIR" 2>/dev/null
|
mkdir -p "$STATS_DIR" "$SNAPSHOTS_DIR" 2>/dev/null
|
||||||
chmod 755 "$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 ts=$(date +%s)
|
||||||
local temp_file=$(mktemp)
|
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"
|
# Parse iptables output: format is "pkts bytes target"
|
||||||
# We need to extract bytes (2nd column) for each rule
|
# We need to extract bytes (2nd column) for each rule
|
||||||
local iptables_output=$(iptables -L GOTELEGRAM_STATS -v -n -x 2>/dev/null)
|
local iptables_output=$(iptables -L GOTELEGRAM_STATS -v -n -x 2>/dev/null)
|
||||||
@@ -350,6 +362,14 @@ install_stats_collector() {
|
|||||||
return 1
|
return 1
|
||||||
fi
|
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)
|
# Get script directory (resolve symlinks)
|
||||||
local script_dir=$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")
|
local script_dir=$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")
|
||||||
local lib_dir=$(dirname "$script_dir")
|
local lib_dir=$(dirname "$script_dir")
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# GoTelegram v2.2 — Управление telemt binary
|
# GoTelegram v2.5.0 — Управление telemt binary
|
||||||
# Скачивание, обновление, запуск, остановка через systemd
|
# Скачивание, обновление, запуск, остановка через systemd
|
||||||
|
|
||||||
TELEMT_GITHUB="telemt/telemt"
|
TELEMT_GITHUB="telemt/telemt"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# GoTelegram v2.2 — Генерация TOML конфигурации для telemt
|
# GoTelegram v2.5.0 — Генерация TOML конфигурации для telemt
|
||||||
|
|
||||||
# ── Популярные домены (не заблокированные в РФ) ──────────────────────────────
|
# ── Популярные домены (не заблокированные в РФ) ──────────────────────────────
|
||||||
QUICK_DOMAINS=(
|
QUICK_DOMAINS=(
|
||||||
@@ -48,15 +48,38 @@ generate_telemt_toml() {
|
|||||||
# Сгенерировано: $(date -Iseconds)
|
# Сгенерировано: $(date -Iseconds)
|
||||||
# Режим: ${mask_mode}
|
# Режим: ${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]
|
[server]
|
||||||
port = ${port}
|
port = ${port}
|
||||||
listen_addr_ipv4 = "0.0.0.0"
|
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]
|
[censorship]
|
||||||
tls_domain = "${mask_domain}"
|
tls_domain = "${mask_domain}"
|
||||||
mask = true
|
mask = true
|
||||||
mask_port = ${mask_port}
|
mask_port = ${mask_port}
|
||||||
tls_emulation = $([ "$mask_mode" = "pro" ] && echo "false" || echo "true")
|
tls_emulation = $([ "$mask_mode" = "pro" ] && echo "false" || echo "true")
|
||||||
|
unknown_sni_action = "mask"
|
||||||
|
|
||||||
[access.users]
|
[access.users]
|
||||||
main = "${secret}"
|
main = "${secret}"
|
||||||
@@ -106,21 +129,57 @@ get_config_value() {
|
|||||||
case "$key" in
|
case "$key" in
|
||||||
secret)
|
secret)
|
||||||
# [access.users] main = "..."
|
# [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)
|
port)
|
||||||
# [server] port = 443
|
# [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)
|
mask_host|tls_domain)
|
||||||
# [censorship] 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)
|
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
|
esac
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/bin/bash
|
#!/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,
|
# Pick from ~1800 templates, preview links, git sparse-checkout downloads,
|
||||||
# + custom git URL templates (user-supplied public repos)
|
# + custom git URL templates (user-supplied public repos)
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# GoTelegram v2.2 — Управление сайтом (nginx + certbot + шаблоны)
|
# GoTelegram v2.5.0 — Управление сайтом (nginx + certbot + шаблоны)
|
||||||
|
|
||||||
# ── Установка nginx ──────────────────────────────────────────────────────────
|
# ── Установка nginx ──────────────────────────────────────────────────────────
|
||||||
install_nginx() {
|
install_nginx() {
|
||||||
@@ -39,7 +39,7 @@ generate_nginx_config() {
|
|||||||
mkdir -p /etc/nginx/sites-available /etc/nginx/sites-enabled
|
mkdir -p /etc/nginx/sites-available /etc/nginx/sites-enabled
|
||||||
|
|
||||||
cat > "$NGINX_SITE_CONF" << 'EONGINX'
|
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 (внешний)
|
# Pro: nginx на 127.0.0.1:8443 (внутренний), telemt на 0.0.0.0:443 (внешний)
|
||||||
# Обычный браузер → :443 → telemt → 127.0.0.1:8443 → nginx (сайт)
|
# Обычный браузер → :443 → telemt → 127.0.0.1:8443 → nginx (сайт)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user