From 13cca51f5f8662881409d73fc845bd5e06d968cf Mon Sep 17 00:00:00 2001 From: anten-ka Date: Mon, 6 Apr 2026 21:30:30 +0300 Subject: [PATCH] v2.2.1 hotfix: fix telemt download grep pattern (grep -iE), add QR cleanup, bump version to 2.2.1 --- gotelegram-bot/bot.py | 2799 +++++++++++++++++++++-------------------- lib/common.sh | 912 +++++++------- lib/telemt.sh | 610 ++++----- 3 files changed, 2163 insertions(+), 2158 deletions(-) diff --git a/gotelegram-bot/bot.py b/gotelegram-bot/bot.py index aaf4ac5..ae2a9ad 100644 --- a/gotelegram-bot/bot.py +++ b/gotelegram-bot/bot.py @@ -1,1397 +1,1402 @@ -#!/usr/bin/env python3 -""" -GoTelegram v2.2 Bot - MTProxy Management for Linux -Manages telemt engine via Telegram interface with full CLI feature parity -Uses python-telegram-bot v21+ -""" - -import asyncio -import html -import json -import logging -import os -import re -import subprocess -import toml -from datetime import datetime -from pathlib import Path -from typing import Tuple, Optional, List, Dict, Any - -from dotenv import load_dotenv -from telegram import ( - Update, - InlineKeyboardButton, - InlineKeyboardMarkup, - InputFile, -) -from telegram.ext import ( - Application, - CommandHandler, - CallbackQueryHandler, - ContextTypes, - filters, -) -from telegram.error import TelegramError, BadRequest - -# Load environment variables -load_dotenv() - -# Logging configuration -logging.basicConfig( - format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", - level=logging.INFO, -) -logger = logging.getLogger(__name__) - -# ============================================================================ -# CONFIGURATION -# ============================================================================ - -GOTELEGRAM_VERSION = "2.2.0" -GOTELEGRAM_CONFIG = "/opt/gotelegram/config.json" -TELEMT_CONFIG = "/etc/telemt/config.toml" -TELEMT_SERVICE = "telemt" -WEBSITE_ROOT = "/var/www/gotelegram-site" -BACKUP_DIR = "/opt/gotelegram/backups" -TEMPLATES_CATALOG = "/opt/gotelegram/templates_catalog.json" - -PROMO_LINK = "https://vk.cc/ct29NQ" -TIP_LINK = "https://pay.cloudtips.ru/p/7410814f" - -BOT_TOKEN = os.getenv("BOT_TOKEN") -ALLOWED_IDS_STR = os.getenv("ALLOWED_IDS", "") -ALLOWED_IDS: set = set() -for _id_str in ALLOWED_IDS_STR.split(","): - _id_str = _id_str.strip() - if _id_str: - try: - ALLOWED_IDS.add(int(_id_str)) - except ValueError: - logging.warning(f"Invalid ALLOWED_IDS entry: {_id_str}") - -QUICK_DOMAINS = [ - "google.com", - "microsoft.com", - "cloudflare.com", - "apple.com", - "amazon.com", - "github.com", - "stackoverflow.com", - "medium.com", - "wikipedia.org", - "coursera.org", - "udemy.com", - "habr.com", - "stepik.org", - "duolingo.com", - "khanacademy.org", - "bbc.com", - "reuters.com", - "nytimes.com", - "ted.com", - "zoom.us", -] - -# ============================================================================ -# UTILITY FUNCTIONS -# ============================================================================ - - -async def sh(*args, timeout: int = 60) -> Tuple[int, str, str]: - """Execute shell command asynchronously. - - Args: - *args: Command and arguments - timeout: Timeout in seconds - - Returns: - Tuple of (return_code, stdout, stderr) - """ - try: - process = await asyncio.create_subprocess_exec( - *args, - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE, - ) - stdout, stderr = await asyncio.wait_for( - process.communicate(), timeout=timeout - ) - return ( - process.returncode, - stdout.decode("utf-8", errors="replace"), - stderr.decode("utf-8", errors="replace"), - ) - except asyncio.TimeoutError: - try: - process.kill() - await process.wait() - except Exception: - pass - return (-1, "", f"Command timeout after {timeout}s") - except Exception as e: - return (-1, "", str(e)) - - -def load_json(path: str) -> Optional[Dict]: - """Load JSON file.""" - try: - with open(path, "r") as f: - return json.load(f) - except Exception as e: - logger.warning(f"Failed to load {path}: {e}") - return None - - -def load_toml(path: str) -> Optional[Dict]: - """Load TOML file.""" - try: - with open(path, "r") as f: - return toml.load(f) - except Exception as e: - logger.warning(f"Failed to load {path}: {e}") - return None - - -def save_json(path: str, data: Dict) -> bool: - """Save JSON file.""" - try: - os.makedirs(os.path.dirname(path), exist_ok=True) - with open(path, "w") as f: - json.dump(data, f, indent=2) - return True - except Exception as e: - logger.error(f"Failed to save {path}: {e}") - return False - - -async def safe_edit_message(query, text: str, reply_markup=None, parse_mode=None) -> bool: - """Safely edit message, handling cases where message was deleted or not modified.""" - try: - await query.edit_message_text( - text, reply_markup=reply_markup, parse_mode=parse_mode - ) - return True - except BadRequest as e: - err_msg = str(e).lower() - if "message is not modified" in err_msg: - return True # No change needed, not an error - if "message to edit not found" in err_msg or "message can't be edited" in err_msg: - logger.warning(f"Cannot edit message: {e}") - return False - raise # Re-raise unexpected BadRequest - - -async def check_service_status(service: str) -> bool: - """Check if systemd service is running.""" - code, _, _ = await sh("systemctl", "is-active", service) - return code == 0 - - -async def get_telemt_version() -> str: - """Get telemt version.""" - code, stdout, _ = await sh("telemt", "-v") - if code == 0: - return stdout.strip().split()[-1] if stdout else "unknown" - return "unknown" - - -def is_docker_running() -> bool: - """Check if Docker daemon is running.""" - try: - subprocess.run( - ["docker", "ps"], - stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL, - timeout=5, - ) - return True - except Exception: - return False - - -async def check_old_container() -> Optional[str]: - """Check for old mtg Docker container (v1 migration).""" - if not is_docker_running(): - return None - code, stdout, _ = await sh("docker", "ps", "-a", "--format", "{{.Names}}") - if code == 0 and "mtg" in stdout: - return "mtg" - return None - - -# ============================================================================ -# ACCESS CONTROL -# ============================================================================ - - -def is_user_allowed(user_id: int) -> bool: - """Check if user ID is in ALLOWED_IDS.""" - if not ALLOWED_IDS: - return True - return user_id in ALLOWED_IDS - - -async def require_auth(update: Update, context: ContextTypes.DEFAULT_TYPE) -> bool: - """Check authorization and send error if not allowed.""" - if not is_user_allowed(update.effective_user.id): - await update.message.reply_text( - f"Access denied. Your ID: {update.effective_user.id}" - ) - logger.warning( - f"Unauthorized access attempt from user {update.effective_user.id}" - ) - return False - return True - - -# ============================================================================ -# MAIN MENU -# ============================================================================ - - -def get_main_menu() -> InlineKeyboardMarkup: - """Generate main menu keyboard.""" - buttons = [ - [ - InlineKeyboardButton("βš™οΈ Install", callback_data="menu_install"), - InlineKeyboardButton("πŸ“Š Status", callback_data="menu_status"), - ], - [ - InlineKeyboardButton("πŸ”— Link", callback_data="menu_link"), - InlineKeyboardButton("πŸ“€ Share", callback_data="menu_share"), - ], - [ - InlineKeyboardButton("πŸ”„ Restart", callback_data="menu_restart"), - InlineKeyboardButton("πŸ“‹ Logs", callback_data="menu_logs"), - ], - [ - InlineKeyboardButton("⚑ Change Mode/Template", callback_data="menu_change"), - InlineKeyboardButton("πŸ’Ύ Backup", callback_data="menu_backup"), - ], - [ - InlineKeyboardButton("↩️ Restore", callback_data="menu_restore"), - InlineKeyboardButton("πŸ“‘ Update telemt", callback_data="menu_update"), - ], - [ - InlineKeyboardButton("🌐 Website/SSL", callback_data="menu_website"), - InlineKeyboardButton("🎁 Promo", callback_data="menu_promo"), - ], - [ - InlineKeyboardButton("πŸ—‘οΈ Remove", callback_data="menu_remove"), - InlineKeyboardButton("ℹ️ Credits", callback_data="menu_credits"), - ], - [InlineKeyboardButton("❌ Close", callback_data="close_menu")], - ] - return InlineKeyboardMarkup(buttons) - - -# ============================================================================ -# COMMANDS -# ============================================================================ - - -async def cmd_start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: - """Start command - show main menu.""" - if not await require_auth(update, context): - return - - welcome = ( - f"GoTelegram v{GOTELEGRAM_VERSION}\n\n" - "πŸ€– MTProxy Management Bot\n" - "Powered by telemt engine\n\n" - "Select an action from the menu below:" - ) - await update.message.reply_text( - welcome, reply_markup=get_main_menu(), parse_mode="HTML" - ) - - -async def cmd_help(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: - """Help command - show available commands.""" - if not await require_auth(update, context): - return - - help_text = ( - "GoTelegram Bot Commands\n\n" - "/start - Show main menu\n" - "/help - Show this help message\n" - "/status - Quick status check\n" - "/logs - Show recent logs\n\n" - "Use the inline menu for all other operations." - ) - await update.message.reply_text(help_text, parse_mode="HTML") - - -async def cmd_status(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: - """Quick status check.""" - if not await require_auth(update, context): - return - - await update.message.reply_text("⏳ Checking status...", parse_mode="HTML") - status_text = await get_status_text() - await update.message.reply_text(status_text, parse_mode="HTML") - - -async def cmd_logs(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: - """Show recent logs.""" - if not await require_auth(update, context): - return - - code, stdout, stderr = await sh( - "journalctl", "-u", TELEMT_SERVICE, "-n", "20", "--no-pager" - ) - if code == 0: - log_text = stdout[-1500:] if len(stdout) > 1500 else stdout - await update.message.reply_text( - f"
{html.escape(log_text)}
", - parse_mode="HTML", - ) - else: - await update.message.reply_text("Failed to retrieve logs") - - -# ============================================================================ -# STATUS -# ============================================================================ - - -async def get_status_text() -> str: - """Generate status report.""" - lines = ["πŸ“Š Current Status\n"] - - # Service status - is_running = await check_service_status(TELEMT_SERVICE) - lines.append(f"Service: {'βœ… Running' if is_running else '❌ Stopped'}") - - # Telemt version - version = await get_telemt_version() - lines.append(f"Telemt: v{version}") - - # Config status - config = load_json(GOTELEGRAM_CONFIG) - if config: - lines.append(f"Mode: {html.escape(str(config.get('mode', 'unknown')))}") - if "template" in config: - lines.append(f"Template: {html.escape(str(config['template']))}") - if "domain" in config: - lines.append(f"Domain: {html.escape(str(config['domain']))}") - if "port" in config: - lines.append(f"Port: {html.escape(str(config['port']))}") - - # Telemt config - telemt_cfg = load_toml(TELEMT_CONFIG) - if telemt_cfg: - cfg = telemt_cfg.get("config", {}) - if "listen_port" in cfg: - lines.append(f"Listen Port: {cfg['listen_port']}") - - # Backups - backup_count = 0 - try: - if os.path.exists(BACKUP_DIR): - backup_count = len([f for f in os.listdir(BACKUP_DIR) if f.endswith((".tar.gz", ".tar.gz.enc")) and not f.endswith(".sha256")]) - except Exception: - pass - lines.append(f"Backups: {backup_count}") - - # Old container check - old_container = await check_old_container() - if old_container: - lines.append(f"\n⚠️ Found old container: {html.escape(old_container)}") - lines.append("Run 'Install' to migrate") - - return "\n".join(lines) - - -async def cb_menu_status(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: - """Status callback.""" - query = update.callback_query - await query.answer() - - await safe_edit_message(query,"⏳ Checking status...") - - status_text = await get_status_text() - keyboard = InlineKeyboardMarkup( - [[InlineKeyboardButton("Β« Back", callback_data="menu_main")]] - ) - await safe_edit_message(query, - status_text, reply_markup=keyboard, parse_mode="HTML" - ) - - -# ============================================================================ -# INSTALL -# ============================================================================ - - -def get_install_mode_menu() -> InlineKeyboardMarkup: - """Install mode selection menu.""" - buttons = [ - [InlineKeyboardButton("⚑ Quick Mode", callback_data="install_mode_quick")], - [InlineKeyboardButton("πŸ”’ Stealth Mode", callback_data="install_mode_stealth")], - [InlineKeyboardButton("Β« Back", callback_data="menu_main")], - ] - return InlineKeyboardMarkup(buttons) - - -async def cb_menu_install(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: - """Install menu callback.""" - query = update.callback_query - await query.answer() - - # Check for old container - old_container = await check_old_container() - if old_container: - text = ( - f"⚠️ Migration from v1 detected\n\n" - f"Found Docker container: {html.escape(old_container)}\n\n" - f"Would you like to:\n" - f"1. Migrate from v1 (recommended)\n" - f"2. Fresh install (will remove old container)\n\n" - f"Select below or choose install mode" - ) - buttons = [ - [InlineKeyboardButton("πŸ”„ Migrate from v1", callback_data="install_migrate")], - [InlineKeyboardButton("✨ Fresh Install", callback_data="install_mode_quick")], - [InlineKeyboardButton("Β« Back", callback_data="menu_main")], - ] - keyboard = InlineKeyboardMarkup(buttons) - else: - text = "Select installation mode:" - keyboard = get_install_mode_menu() - - await safe_edit_message(query, - text, reply_markup=keyboard, parse_mode="HTML" - ) - - -async def cb_install_mode_quick(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: - """Quick mode domain selection.""" - query = update.callback_query - await query.answer() - - # Show domains with pagination (4 per row, 2 rows) - buttons = [] - for i in range(0, len(QUICK_DOMAINS), 2): - row = [] - for j in range(2): - if i + j < len(QUICK_DOMAINS): - domain = QUICK_DOMAINS[i + j] - row.append( - InlineKeyboardButton( - domain, callback_data=f"quick_dom_{i+j}" - ) - ) - buttons.append(row) - - buttons.append([InlineKeyboardButton("Β« Back", callback_data="menu_install")]) - - text = "Select a domain for quick mode:" - keyboard = InlineKeyboardMarkup(buttons) - await safe_edit_message(query,text, reply_markup=keyboard) - - -async def cb_quick_domain(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: - """Quick domain selection callback.""" - query = update.callback_query - data = query.data - try: - domain_idx = int(data.split("_")[-1]) - domain = QUICK_DOMAINS[domain_idx] - except (ValueError, IndexError): - await query.answer("Invalid domain selection") - return - - await query.answer() - await safe_edit_message(query,f"⏳ Installing with domain: {domain}...") - - # Simulate installation (in real scenario, call install script) - config = { - "mode": "quick", - "domain": domain, - "port": 443, - "installed_at": datetime.now().isoformat(), - } - - if save_json(GOTELEGRAM_CONFIG, config): - text = ( - f"βœ… Quick mode installed!\n\n" - f"Domain: {domain}\n" - f"Mode: Quick\n\n" - f"Service starting... Check status in 10 seconds." - ) - keyboard = InlineKeyboardMarkup( - [[InlineKeyboardButton("Β« Back", callback_data="menu_main")]] - ) - await safe_edit_message(query, - text, reply_markup=keyboard, parse_mode="HTML" - ) - else: - await safe_edit_message(query, - "❌ Failed to save configuration", - reply_markup=InlineKeyboardMarkup( - [[InlineKeyboardButton("Β« Back", callback_data="menu_install")]] - ), - ) - - -async def cb_install_mode_stealth(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: - """Stealth mode - show template categories.""" - query = update.callback_query - await query.answer() - - catalog = load_json(TEMPLATES_CATALOG) - if not catalog or "categories" not in catalog: - await safe_edit_message(query, - "❌ Template catalog not found", - reply_markup=InlineKeyboardMarkup( - [[InlineKeyboardButton("Β« Back", callback_data="menu_install")]] - ), - ) - return - - buttons = [] - for cat in catalog.get("categories", []): - buttons.append( - [ - InlineKeyboardButton( - f"πŸ“ {cat['name']}", callback_data=f"stealth_cat_{cat['id']}" - ) - ] - ) - buttons.append([InlineKeyboardButton("Β« Back", callback_data="menu_install")]) - - text = "Stealth Mode - Select Template Category:" - keyboard = InlineKeyboardMarkup(buttons) - await safe_edit_message(query,text, reply_markup=keyboard) - - -async def cb_stealth_category(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: - """Show templates in category.""" - query = update.callback_query - data = query.data - cat_id = data.removeprefix("stealth_cat_") - - await query.answer() - - catalog = load_json(TEMPLATES_CATALOG) - if not catalog: - await safe_edit_message(query,"❌ Template catalog not found") - return - - # Find category and templates - category = None - templates = [] - for cat in catalog.get("categories", []): - if cat["id"] == cat_id: - category = cat - templates = cat.get("templates", []) - break - - if not category: - await safe_edit_message(query,"❌ Category not found") - return - - buttons = [] - for tpl in templates: - buttons.append( - [ - InlineKeyboardButton( - f"🎨 {tpl['name']}", callback_data=f"stealth_tpl_{tpl['id']}" - ) - ] - ) - buttons.append([InlineKeyboardButton("Β« Back", callback_data="install_mode_stealth")]) - - text = f"Select template from {html.escape(category['name'])}:" - keyboard = InlineKeyboardMarkup(buttons) - await safe_edit_message(query,text, reply_markup=keyboard, parse_mode="HTML") - - -async def cb_stealth_template(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: - """Show template preview and confirm.""" - query = update.callback_query - data = query.data - tpl_id = data.removeprefix("stealth_tpl_") - - await query.answer() - - catalog = load_json(TEMPLATES_CATALOG) - if not catalog: - await safe_edit_message(query,"❌ Template catalog not found") - return - - # Find template - template = None - for cat in catalog.get("categories", []): - for tpl in cat.get("templates", []): - if tpl["id"] == tpl_id: - template = tpl - break - if template: - break - - if not template: - await safe_edit_message(query,"❌ Template not found") - return - - tpl_name = html.escape(template.get('name', 'Unknown')) - tpl_desc = html.escape(template.get('description', 'N/A')) - text = ( - f"🎨 Template Preview\n\n" - f"Name: {tpl_name}\n" - f"Description: {tpl_desc}\n\n" - ) - if "preview_url" in template: - preview_url = html.escape(template['preview_url'], quote=True) - text += f'View Live Preview\n\n' - - text += "Confirm installation?" - - buttons = [ - [ - InlineKeyboardButton( - "βœ… Install", callback_data=f"stealth_confirm_{tpl_id}" - ) - ], - [InlineKeyboardButton("Β« Back", callback_data="install_mode_stealth")], - ] - keyboard = InlineKeyboardMarkup(buttons) - await safe_edit_message(query,text, reply_markup=keyboard, parse_mode="HTML") - - -async def cb_stealth_confirm(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: - """Confirm and install stealth template.""" - query = update.callback_query - data = query.data - tpl_id = data.removeprefix("stealth_confirm_") - - await query.answer() - await safe_edit_message(query,"⏳ Installing template...") - - config = { - "mode": "stealth", - "template": tpl_id, - "port": 443, - "installed_at": datetime.now().isoformat(), - } - - if save_json(GOTELEGRAM_CONFIG, config): - text = ( - f"βœ… Stealth mode installed!\n\n" - f"Template: {html.escape(tpl_id)}\n" - f"Mode: Stealth\n\n" - f"Service starting... Check status in 10 seconds." - ) - keyboard = InlineKeyboardMarkup( - [[InlineKeyboardButton("Β« Back", callback_data="menu_main")]] - ) - await safe_edit_message(query, - text, reply_markup=keyboard, parse_mode="HTML" - ) - else: - await safe_edit_message(query, - "❌ Failed to save configuration", - reply_markup=InlineKeyboardMarkup( - [[InlineKeyboardButton("Β« Back", callback_data="menu_install")]] - ), - ) - - -# ============================================================================ -# PROXY LINK & SHARE -# ============================================================================ - - -async def get_proxy_link() -> Optional[str]: - """Generate proxy link from config.""" - config = load_json(GOTELEGRAM_CONFIG) - if not config: - return None - - # Get secret from telemt TOML config - secret = config.get("secret", "") - if not secret: - telemt_cfg = load_toml(TELEMT_CONFIG) - if telemt_cfg: - users = telemt_cfg.get("users", []) - if isinstance(users, list) and users: - secret = users[0].get("secret", "") - if not secret: - return None - - # Get server IP - 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" - - port = config.get("port", 443) - - return f"tg://proxy?server={server}&port={port}&secret={secret}" - - -async def cb_menu_link(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: - """Generate and show proxy link.""" - query = update.callback_query - await query.answer() - - link = await get_proxy_link() - if not link: - text = "❌ Proxy not installed yet. Run install first." - else: - text = ( - f"πŸ”— Proxy Link\n\n" - f"{html.escape(link)}\n\n" - f"Open in Telegram to connect." - ) - - keyboard = InlineKeyboardMarkup( - [[InlineKeyboardButton("Β« Back", callback_data="menu_main")]] - ) - await safe_edit_message(query,text, reply_markup=keyboard, parse_mode="HTML") - - -async def cb_menu_share(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: - """Share link as QR code.""" - query = update.callback_query - await query.answer() - - link = await get_proxy_link() - if not link: - text = "❌ Proxy not installed yet." - keyboard = InlineKeyboardMarkup( - [[InlineKeyboardButton("Β« Back", callback_data="menu_main")]] - ) - await safe_edit_message(query,text, reply_markup=keyboard) - return - - # Try to generate QR code - qr_file = None - code, _, _ = await sh("which", "qrencode") - if code == 0: - qr_path = "/tmp/proxy_qr.png" - code, _, _ = await sh("qrencode", "-o", qr_path, link) - if code == 0 and os.path.exists(qr_path): - qr_file = qr_path - - if qr_file: - try: - with open(qr_file, "rb") as f: - await query.message.reply_photo( - photo=f, - caption=f"πŸ“€ Proxy QR Code\n\n{html.escape(link)}", - parse_mode="HTML", - ) - except Exception as e: - logger.error(f"Failed to send QR code: {e}") - await safe_edit_message(query, - f"πŸ”— Proxy Link\n\n{html.escape(link)}", - parse_mode="HTML", - ) - else: - await safe_edit_message(query, - f"πŸ”— Proxy Link\n\n{html.escape(link)}", - reply_markup=InlineKeyboardMarkup( - [[InlineKeyboardButton("Β« Back", callback_data="menu_main")]] - ), - parse_mode="HTML", - ) - - -# ============================================================================ -# RESTART & LOGS -# ============================================================================ - - -async def cb_menu_restart(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: - """Restart service.""" - query = update.callback_query - await query.answer() - - text = "⏳ Restarting telemt service..." - await safe_edit_message(query,text) - - code, _, stderr = await sh("systemctl", "restart", TELEMT_SERVICE) - if code == 0: - text = "βœ… Service restarted successfully" - else: - text = f"❌ Failed to restart:\n{html.escape(stderr[:500])}" - - keyboard = InlineKeyboardMarkup( - [[InlineKeyboardButton("Β« Back", callback_data="menu_main")]] - ) - await safe_edit_message(query, - text, reply_markup=keyboard, parse_mode="HTML" - ) - - -async def cb_menu_logs(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: - """Show recent logs.""" - query = update.callback_query - await query.answer() - - code, stdout, _ = await sh( - "journalctl", "-u", TELEMT_SERVICE, "-n", "30", "--no-pager" - ) - - if code == 0: - log_text = stdout[-1000:] if len(stdout) > 1000 else stdout - text = f"πŸ“‹ Recent Logs\n\n
{html.escape(log_text)}
" - else: - text = "❌ Failed to retrieve logs" - - keyboard = InlineKeyboardMarkup( - [[InlineKeyboardButton("Β« Back", callback_data="menu_main")]] - ) - await safe_edit_message(query,text, reply_markup=keyboard, parse_mode="HTML") - - -# ============================================================================ -# BACKUP & RESTORE -# ============================================================================ - - -async def cb_menu_backup(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: - """Backup menu.""" - query = update.callback_query - await query.answer() - - # List existing backups - backups = [] - try: - if os.path.exists(BACKUP_DIR): - backups = sorted( - [f for f in os.listdir(BACKUP_DIR) - if f.endswith((".tar.gz", ".tar.gz.enc")) and not f.endswith(".sha256")], - reverse=True, - ) - except Exception: - pass - - buttons = [[InlineKeyboardButton("πŸ’Ύ Create Backup", callback_data="backup_create")]] - - if backups: - buttons.append( - [InlineKeyboardButton("πŸ“‹ List Backups", callback_data="backup_list")] - ) - - buttons.append([InlineKeyboardButton("Β« Back", callback_data="menu_main")]) - - text = f"πŸ’Ύ Backup Management\n\nExisting backups: {len(backups)}" - keyboard = InlineKeyboardMarkup(buttons) - await safe_edit_message(query,text, reply_markup=keyboard, parse_mode="HTML") - - -async def cb_backup_create(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: - """Create backup.""" - query = update.callback_query - await query.answer() - - await safe_edit_message(query,"⏳ Creating backup...") - - timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") - backup_file = os.path.join(BACKUP_DIR, f"backup_{timestamp}.tar.gz") - - os.makedirs(BACKUP_DIR, exist_ok=True) - code, _, stderr = await sh( - "tar", "-czf", backup_file, GOTELEGRAM_CONFIG, TELEMT_CONFIG - ) - - if code == 0: - text = f"βœ… Backup created:\n{html.escape(backup_file)}" - else: - text = f"❌ Backup failed:\n{html.escape(stderr[:500])}" - - keyboard = InlineKeyboardMarkup( - [[InlineKeyboardButton("Β« Back", callback_data="menu_backup")]] - ) - await safe_edit_message(query,text, reply_markup=keyboard, parse_mode="HTML") - - -async def cb_backup_list(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: - """List backups.""" - query = update.callback_query - await query.answer() - - backups = [] - try: - if os.path.exists(BACKUP_DIR): - backups = sorted( - [f for f in os.listdir(BACKUP_DIR) if f.endswith((".tar.gz", ".tar.gz.enc")) and not f.endswith(".sha256")], - reverse=True, - ) - except Exception: - pass - - if not backups: - text = "No backups found" - else: - text = "πŸ“‹ Available Backups\n\n" - for backup in backups[:10]: - path = os.path.join(BACKUP_DIR, backup) - size = os.path.getsize(path) / (1024 * 1024) - text += f"{html.escape(backup)} ({size:.2f} MB)\n" - - keyboard = InlineKeyboardMarkup( - [[InlineKeyboardButton("Β« Back", callback_data="menu_backup")]] - ) - await safe_edit_message(query,text, reply_markup=keyboard, parse_mode="HTML") - - -async def cb_menu_restore(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: - """Restore menu.""" - query = update.callback_query - await query.answer() - - backups = [] - try: - if os.path.exists(BACKUP_DIR): - backups = sorted( - [f for f in os.listdir(BACKUP_DIR) if f.endswith((".tar.gz", ".tar.gz.enc")) and not f.endswith(".sha256")], - reverse=True, - ) - except Exception: - pass - - if not backups: - text = "❌ No backups available" - keyboard = InlineKeyboardMarkup( - [[InlineKeyboardButton("Β« Back", callback_data="menu_main")]] - ) - else: - text = "Select backup to restore:" - buttons = [] - for i, backup in enumerate(backups[:10]): - buttons.append( - [ - InlineKeyboardButton( - backup, callback_data=f"restore_idx_{i}" - ) - ] - ) - buttons.append([InlineKeyboardButton("Β« Back", callback_data="menu_main")]) - keyboard = InlineKeyboardMarkup(buttons) - # Store backup list in user_data for retrieval - context.user_data["backup_list"] = backups[:10] - - await safe_edit_message(query,text, reply_markup=keyboard) - - -async def cb_restore_backup(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: - """Execute backup restoration.""" - query = update.callback_query - data = query.data - - try: - idx = int(data.removeprefix("restore_idx_")) - except ValueError: - await query.answer("Invalid backup selection") - return - - backup_list = context.user_data.get("backup_list", []) - if idx < 0 or idx >= len(backup_list): - await query.answer("Backup not found") - return - - backup_name = backup_list[idx] - backup_path = os.path.join(BACKUP_DIR, backup_name) - - await query.answer() - await safe_edit_message(query,f"⏳ Restoring from {html.escape(backup_name)}...") - - if not os.path.exists(backup_path): - text = "❌ Backup file not found" - else: - # Simple restore: extract tar to overwrite configs - code, _, stderr = await sh( - "tar", "-xzf", backup_path, "-C", "/", timeout=60 - ) - if code == 0: - # Restart services - await sh("systemctl", "restart", TELEMT_SERVICE) - text = f"βœ… Restored from {html.escape(backup_name)}" - else: - text = f"❌ Restore failed:\n{html.escape(stderr[:500])}" - - keyboard = InlineKeyboardMarkup( - [[InlineKeyboardButton("Β« Back", callback_data="menu_main")]] - ) - await safe_edit_message(query,text, reply_markup=keyboard, parse_mode="HTML") - - -# ============================================================================ -# UPDATE & MODE/TEMPLATE CHANGE -# ============================================================================ - - -async def cb_menu_update(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: - """Update telemt by re-running the install script's update logic.""" - query = update.callback_query - await query.answer() - - await safe_edit_message(query,"⏳ Checking for telemt updates...") - - # Get current version - cur_code, cur_out, _ = await sh("telemt", "--version") - current = cur_out.strip() if cur_code == 0 else "unknown" - - # Check latest release from GitHub - code, stdout, stderr = await sh( - "curl", "-s", "--max-time", "10", - "https://api.github.com/repos/telemt/telemt/releases/latest", - ) - - if code != 0 or not stdout.strip(): - text = "❌ Failed to check for updates" - else: - try: - release = json.loads(stdout) - latest = release.get("tag_name", "unknown") - if latest == current: - text = f"βœ… telemt is already up to date ({html.escape(current)})" - else: - text = ( - f"ℹ️ Update available: {html.escape(current)} β†’ {html.escape(latest)}\n\n" - f"Run the CLI installer to update:\n" - f"sudo bash install.sh β†’ menu item 10" - ) - except json.JSONDecodeError: - text = "❌ Failed to parse release info" - - keyboard = InlineKeyboardMarkup( - [[InlineKeyboardButton("Β« Back", callback_data="menu_main")]] - ) - await safe_edit_message(query,text, reply_markup=keyboard, parse_mode="HTML") - - -async def cb_menu_change(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: - """Change mode or template.""" - query = update.callback_query - await query.answer() - - buttons = [ - [InlineKeyboardButton("⚑ Switch to Quick Mode", callback_data="change_quick")], - [InlineKeyboardButton("πŸ”’ Switch to Stealth Mode", callback_data="change_stealth")], - [InlineKeyboardButton("Β« Back", callback_data="menu_main")], - ] - keyboard = InlineKeyboardMarkup(buttons) - await safe_edit_message(query, - "Change mode or template:", reply_markup=keyboard - ) - - -async def cb_change_quick(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: - """Switch to quick mode β€” show domain selection.""" - query = update.callback_query - await query.answer() - # Reuse the quick mode domain selection flow - await cb_install_mode_quick(update, context) - - -async def cb_change_stealth(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: - """Switch to stealth mode β€” show template categories.""" - query = update.callback_query - await query.answer() - # Reuse the stealth mode template selection flow - await cb_install_mode_stealth(update, context) - - -async def cb_install_migrate(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: - """Migrate from v1 (mtg Docker) to v2 (telemt).""" - query = update.callback_query - await query.answer() - - await safe_edit_message(query,"⏳ Migrating from v1...") - - # Stop old mtg container - code, _, stderr = await sh("docker", "stop", "mtproto-proxy", timeout=30) - if code != 0: - code, _, stderr = await sh("docker", "stop", "mtg", timeout=30) - - # Remove old container - await sh("docker", "rm", "mtproto-proxy", timeout=15) - await sh("docker", "rm", "mtg", timeout=15) - - text = ( - "βœ… v1 container stopped and removed\n\n" - "Now select installation mode for v2:" - ) - keyboard = get_install_mode_menu() - await safe_edit_message(query,text, reply_markup=keyboard, parse_mode="HTML") - - -# ============================================================================ -# WEBSITE & SSL -# ============================================================================ - - -async def cb_menu_website(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: - """Website and SSL management.""" - query = update.callback_query - await query.answer() - - buttons = [ - [InlineKeyboardButton("πŸ”„ Renew SSL Certificate", callback_data="ssl_renew")], - [InlineKeyboardButton("πŸ“Š SSL Status", callback_data="ssl_status")], - [InlineKeyboardButton("Β« Back", callback_data="menu_main")], - ] - keyboard = InlineKeyboardMarkup(buttons) - await safe_edit_message(query, - "Website & SSL Management:", reply_markup=keyboard - ) - - -async def cb_ssl_renew(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: - """Renew SSL certificate.""" - query = update.callback_query - await query.answer() - - await safe_edit_message(query,"⏳ Renewing SSL certificate...") - - code, stdout, stderr = await sh("certbot", "renew", timeout=120) - - if code == 0: - text = "βœ… SSL certificate renewed successfully" - else: - text = f"❌ Renewal failed:\n{html.escape(stderr[:500])}" - - keyboard = InlineKeyboardMarkup( - [[InlineKeyboardButton("Β« Back", callback_data="menu_website")]] - ) - await safe_edit_message(query,text, reply_markup=keyboard, parse_mode="HTML") - - -async def cb_ssl_status(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: - """Show SSL status.""" - query = update.callback_query - await query.answer() - - code, stdout, _ = await sh("certbot", "certificates") - - if code == 0: - text = f"πŸ“Š SSL Certificates\n\n
{html.escape(stdout[:1000])}
" - else: - text = "❌ Failed to get SSL status" - - keyboard = InlineKeyboardMarkup( - [[InlineKeyboardButton("Β« Back", callback_data="menu_website")]] - ) - await safe_edit_message(query,text, reply_markup=keyboard, parse_mode="HTML") - - -# ============================================================================ -# PROMO & CREDITS -# ============================================================================ - - -async def cb_menu_promo(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: - """Promo information.""" - query = update.callback_query - await query.answer() - - text = ( - f"🎁 GoTelegram Promo\n\n" - f"Share the love! Invite friends to use GoTelegram.\n\n" - f"Promo Link\n\n" - f"Support development:\n" - f"Send a Tip" - ) - - keyboard = InlineKeyboardMarkup( - [[InlineKeyboardButton("Β« Back", callback_data="menu_main")]] - ) - await safe_edit_message(query,text, reply_markup=keyboard, parse_mode="HTML") - - -async def cb_menu_credits(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: - """Credits and acknowledgements.""" - query = update.callback_query - await query.answer() - - text = ( - f"ℹ️ Credits & Acknowledgements\n\n" - f"GoTelegram v{GOTELEGRAM_VERSION}\n\n" - f"Built with love for the Telegram community\n\n" - f"Special thanks to:\n\n" - f"πŸ™ telemt - MTProxy engine\n" - f" High-performance proxy core\n\n" - f"🎨 HTML5UP - Beautiful web templates\n" - f" Responsive design & themes\n\n" - f"πŸ“š Learning Zone - Educational resources\n" - f" Community learning support\n\n" - f"πŸš€ Start Bootstrap - Bootstrap templates\n" - f" Professional design framework\n\n" - f"πŸ’¬ Community - Your feedback & support\n\n" - f"GoTelegram is open-source and community-driven" - ) - - keyboard = InlineKeyboardMarkup( - [[InlineKeyboardButton("Β« Back", callback_data="menu_main")]] - ) - await safe_edit_message(query,text, reply_markup=keyboard, parse_mode="HTML") - - -# ============================================================================ -# REMOVE -# ============================================================================ - - -async def cb_menu_remove(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: - """Remove installation.""" - query = update.callback_query - await query.answer() - - text = ( - "⚠️ Remove GoTelegram\n\n" - "This will completely remove the installation.\n" - "Are you sure?" - ) - - buttons = [ - [InlineKeyboardButton("❌ Yes, Remove", callback_data="remove_confirm")], - [InlineKeyboardButton("Β« Back", callback_data="menu_main")], - ] - keyboard = InlineKeyboardMarkup(buttons) - await safe_edit_message(query,text, reply_markup=keyboard, parse_mode="HTML") - - -async def cb_remove_confirm(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: - """Confirm removal.""" - query = update.callback_query - await query.answer() - - await safe_edit_message(query,"⏳ Removing GoTelegram...") - - # Stop service - await sh("systemctl", "stop", TELEMT_SERVICE) - - # Remove directories - for path in ["/opt/gotelegram", WEBSITE_ROOT]: - await sh("rm", "-rf", path) - - text = "βœ… GoTelegram removed successfully" - keyboard = InlineKeyboardMarkup( - [[InlineKeyboardButton("Β« Back", callback_data="menu_main")]] - ) - await safe_edit_message(query,text, reply_markup=keyboard, parse_mode="HTML") - - -# ============================================================================ -# CALLBACK ROUTING -# ============================================================================ - - -async def handle_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: - """Route all callbacks.""" - query = update.callback_query - data = query.data - - # Access control - if not is_user_allowed(update.effective_user.id): - await query.answer("Access denied") - return - - # Main menu - if data == "menu_main": - await query.answer() - buttons = get_main_menu() - text = ( - f"GoTelegram v{GOTELEGRAM_VERSION}\n\n" - "πŸ€– MTProxy Management\n" - "Select an action:" - ) - await safe_edit_message(query,text, reply_markup=buttons, parse_mode="HTML") - return - - if data == "close_menu": - await query.answer() - await query.delete_message() - return - - # Dispatch to handlers - handlers = { - "menu_install": cb_menu_install, - "menu_status": cb_menu_status, - "menu_link": cb_menu_link, - "menu_share": cb_menu_share, - "menu_restart": cb_menu_restart, - "menu_logs": cb_menu_logs, - "menu_backup": cb_menu_backup, - "menu_restore": cb_menu_restore, - "menu_update": cb_menu_update, - "menu_change": cb_menu_change, - "menu_website": cb_menu_website, - "menu_promo": cb_menu_promo, - "menu_credits": cb_menu_credits, - "menu_remove": cb_menu_remove, - "install_mode_quick": cb_install_mode_quick, - "install_mode_stealth": cb_install_mode_stealth, - "backup_create": cb_backup_create, - "backup_list": cb_backup_list, - "ssl_renew": cb_ssl_renew, - "ssl_status": cb_ssl_status, - "remove_confirm": cb_remove_confirm, - "change_quick": cb_change_quick, - "change_stealth": cb_change_stealth, - "install_migrate": cb_install_migrate, - } - - # Pattern-based handlers - if data.startswith("quick_dom_"): - await cb_quick_domain(update, context) - elif data.startswith("stealth_cat_"): - await cb_stealth_category(update, context) - elif data.startswith("stealth_tpl_"): - await cb_stealth_template(update, context) - elif data.startswith("stealth_confirm_"): - await cb_stealth_confirm(update, context) - elif data.startswith("restore_idx_"): - await cb_restore_backup(update, context) - elif data in handlers: - await handlers[data](update, context) - else: - await query.answer("Unknown action") - - -# ============================================================================ -# ERROR HANDLERS -# ============================================================================ - - -async def error_handler(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: - """Log errors caused by Updates.""" - logger.error(f"Exception while handling an update:", exc_info=context.error) - - -# ============================================================================ -# MAIN APPLICATION -# ============================================================================ - - -def main() -> None: - """Start the bot.""" - if not BOT_TOKEN: - logger.error("BOT_TOKEN not set in .env file") - return - - # Create the Application - application = Application.builder().token(BOT_TOKEN).build() - - # Command handlers - application.add_handler(CommandHandler("start", cmd_start)) - application.add_handler(CommandHandler("help", cmd_help)) - application.add_handler(CommandHandler("status", cmd_status)) - application.add_handler(CommandHandler("logs", cmd_logs)) - - # Callback query handler (buttons) - application.add_handler(CallbackQueryHandler(handle_callback)) - - # Error handler - application.add_error_handler(error_handler) - - # Run the bot - logger.info(f"GoTelegram v{GOTELEGRAM_VERSION} bot starting...") - application.run_polling(allowed_updates=Update.ALL_TYPES) - - -if __name__ == "__main__": - main() +#!/usr/bin/env python3 +""" +GoTelegram v2.2 Bot - MTProxy Management for Linux +Manages telemt engine via Telegram interface with full CLI feature parity +Uses python-telegram-bot v21+ +""" + +import asyncio +import html +import json +import logging +import os +import re +import subprocess +import toml +from datetime import datetime +from pathlib import Path +from typing import Tuple, Optional, List, Dict, Any + +from dotenv import load_dotenv +from telegram import ( + Update, + InlineKeyboardButton, + InlineKeyboardMarkup, + InputFile, +) +from telegram.ext import ( + Application, + CommandHandler, + CallbackQueryHandler, + ContextTypes, + filters, +) +from telegram.error import TelegramError, BadRequest + +# Load environment variables +load_dotenv() + +# Logging configuration +logging.basicConfig( + format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", + level=logging.INFO, +) +logger = logging.getLogger(__name__) + +# ============================================================================ +# CONFIGURATION +# ============================================================================ + +GOTELEGRAM_VERSION = "2.2.0" +GOTELEGRAM_CONFIG = "/opt/gotelegram/config.json" +TELEMT_CONFIG = "/etc/telemt/config.toml" +TELEMT_SERVICE = "telemt" +WEBSITE_ROOT = "/var/www/gotelegram-site" +BACKUP_DIR = "/opt/gotelegram/backups" +TEMPLATES_CATALOG = "/opt/gotelegram/templates_catalog.json" + +PROMO_LINK = "https://vk.cc/ct29NQ" +TIP_LINK = "https://pay.cloudtips.ru/p/7410814f" + +BOT_TOKEN = os.getenv("BOT_TOKEN") +ALLOWED_IDS_STR = os.getenv("ALLOWED_IDS", "") +ALLOWED_IDS: set = set() +for _id_str in ALLOWED_IDS_STR.split(","): + _id_str = _id_str.strip() + if _id_str: + try: + ALLOWED_IDS.add(int(_id_str)) + except ValueError: + logging.warning(f"Invalid ALLOWED_IDS entry: {_id_str}") + +QUICK_DOMAINS = [ + "google.com", + "microsoft.com", + "cloudflare.com", + "apple.com", + "amazon.com", + "github.com", + "stackoverflow.com", + "medium.com", + "wikipedia.org", + "coursera.org", + "udemy.com", + "habr.com", + "stepik.org", + "duolingo.com", + "khanacademy.org", + "bbc.com", + "reuters.com", + "nytimes.com", + "ted.com", + "zoom.us", +] + +# ============================================================================ +# UTILITY FUNCTIONS +# ============================================================================ + + +async def sh(*args, timeout: int = 60) -> Tuple[int, str, str]: + """Execute shell command asynchronously. + + Args: + *args: Command and arguments + timeout: Timeout in seconds + + Returns: + Tuple of (return_code, stdout, stderr) + """ + try: + process = await asyncio.create_subprocess_exec( + *args, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + ) + stdout, stderr = await asyncio.wait_for( + process.communicate(), timeout=timeout + ) + return ( + process.returncode, + stdout.decode("utf-8", errors="replace"), + stderr.decode("utf-8", errors="replace"), + ) + except asyncio.TimeoutError: + try: + process.kill() + await process.wait() + except Exception: + pass + return (-1, "", f"Command timeout after {timeout}s") + except Exception as e: + return (-1, "", str(e)) + + +def load_json(path: str) -> Optional[Dict]: + """Load JSON file.""" + try: + with open(path, "r") as f: + return json.load(f) + except Exception as e: + logger.warning(f"Failed to load {path}: {e}") + return None + + +def load_toml(path: str) -> Optional[Dict]: + """Load TOML file.""" + try: + with open(path, "r") as f: + return toml.load(f) + except Exception as e: + logger.warning(f"Failed to load {path}: {e}") + return None + + +def save_json(path: str, data: Dict) -> bool: + """Save JSON file.""" + try: + os.makedirs(os.path.dirname(path), exist_ok=True) + with open(path, "w") as f: + json.dump(data, f, indent=2) + return True + except Exception as e: + logger.error(f"Failed to save {path}: {e}") + return False + + +async def safe_edit_message(query, text: str, reply_markup=None, parse_mode=None) -> bool: + """Safely edit message, handling cases where message was deleted or not modified.""" + try: + await query.edit_message_text( + text, reply_markup=reply_markup, parse_mode=parse_mode + ) + return True + except BadRequest as e: + err_msg = str(e).lower() + if "message is not modified" in err_msg: + return True # No change needed, not an error + if "message to edit not found" in err_msg or "message can't be edited" in err_msg: + logger.warning(f"Cannot edit message: {e}") + return False + raise # Re-raise unexpected BadRequest + + +async def check_service_status(service: str) -> bool: + """Check if systemd service is running.""" + code, _, _ = await sh("systemctl", "is-active", service) + return code == 0 + + +async def get_telemt_version() -> str: + """Get telemt version.""" + code, stdout, _ = await sh("telemt", "-v") + if code == 0: + return stdout.strip().split()[-1] if stdout else "unknown" + return "unknown" + + +def is_docker_running() -> bool: + """Check if Docker daemon is running.""" + try: + subprocess.run( + ["docker", "ps"], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + timeout=5, + ) + return True + except Exception: + return False + + +async def check_old_container() -> Optional[str]: + """Check for old mtg Docker container (v1 migration).""" + if not is_docker_running(): + return None + code, stdout, _ = await sh("docker", "ps", "-a", "--format", "{{.Names}}") + if code == 0 and "mtg" in stdout: + return "mtg" + return None + + +# ============================================================================ +# ACCESS CONTROL +# ============================================================================ + + +def is_user_allowed(user_id: int) -> bool: + """Check if user ID is in ALLOWED_IDS.""" + if not ALLOWED_IDS: + return True + return user_id in ALLOWED_IDS + + +async def require_auth(update: Update, context: ContextTypes.DEFAULT_TYPE) -> bool: + """Check authorization and send error if not allowed.""" + if not is_user_allowed(update.effective_user.id): + await update.message.reply_text( + f"Access denied. Your ID: {update.effective_user.id}" + ) + logger.warning( + f"Unauthorized access attempt from user {update.effective_user.id}" + ) + return False + return True + + +# ============================================================================ +# MAIN MENU +# ============================================================================ + + +def get_main_menu() -> InlineKeyboardMarkup: + """Generate main menu keyboard.""" + buttons = [ + [ + InlineKeyboardButton("βš™οΈ Install", callback_data="menu_install"), + InlineKeyboardButton("πŸ“Š Status", callback_data="menu_status"), + ], + [ + InlineKeyboardButton("πŸ”— Link", callback_data="menu_link"), + InlineKeyboardButton("πŸ“€ Share", callback_data="menu_share"), + ], + [ + InlineKeyboardButton("πŸ”„ Restart", callback_data="menu_restart"), + InlineKeyboardButton("πŸ“‹ Logs", callback_data="menu_logs"), + ], + [ + InlineKeyboardButton("⚑ Change Mode/Template", callback_data="menu_change"), + InlineKeyboardButton("πŸ’Ύ Backup", callback_data="menu_backup"), + ], + [ + InlineKeyboardButton("↩️ Restore", callback_data="menu_restore"), + InlineKeyboardButton("πŸ“‘ Update telemt", callback_data="menu_update"), + ], + [ + InlineKeyboardButton("🌐 Website/SSL", callback_data="menu_website"), + InlineKeyboardButton("🎁 Promo", callback_data="menu_promo"), + ], + [ + InlineKeyboardButton("πŸ—‘οΈ Remove", callback_data="menu_remove"), + InlineKeyboardButton("ℹ️ Credits", callback_data="menu_credits"), + ], + [InlineKeyboardButton("❌ Close", callback_data="close_menu")], + ] + return InlineKeyboardMarkup(buttons) + + +# ============================================================================ +# COMMANDS +# ============================================================================ + + +async def cmd_start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Start command - show main menu.""" + if not await require_auth(update, context): + return + + welcome = ( + f"GoTelegram v{GOTELEGRAM_VERSION}\n\n" + "πŸ€– MTProxy Management Bot\n" + "Powered by telemt engine\n\n" + "Select an action from the menu below:" + ) + await update.message.reply_text( + welcome, reply_markup=get_main_menu(), parse_mode="HTML" + ) + + +async def cmd_help(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Help command - show available commands.""" + if not await require_auth(update, context): + return + + help_text = ( + "GoTelegram Bot Commands\n\n" + "/start - Show main menu\n" + "/help - Show this help message\n" + "/status - Quick status check\n" + "/logs - Show recent logs\n\n" + "Use the inline menu for all other operations." + ) + await update.message.reply_text(help_text, parse_mode="HTML") + + +async def cmd_status(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Quick status check.""" + if not await require_auth(update, context): + return + + await update.message.reply_text("⏳ Checking status...", parse_mode="HTML") + status_text = await get_status_text() + await update.message.reply_text(status_text, parse_mode="HTML") + + +async def cmd_logs(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Show recent logs.""" + if not await require_auth(update, context): + return + + code, stdout, stderr = await sh( + "journalctl", "-u", TELEMT_SERVICE, "-n", "20", "--no-pager" + ) + if code == 0: + log_text = stdout[-1500:] if len(stdout) > 1500 else stdout + await update.message.reply_text( + f"
{html.escape(log_text)}
", + parse_mode="HTML", + ) + else: + await update.message.reply_text("Failed to retrieve logs") + + +# ============================================================================ +# STATUS +# ============================================================================ + + +async def get_status_text() -> str: + """Generate status report.""" + lines = ["πŸ“Š Current Status\n"] + + # Service status + is_running = await check_service_status(TELEMT_SERVICE) + lines.append(f"Service: {'βœ… Running' if is_running else '❌ Stopped'}") + + # Telemt version + version = await get_telemt_version() + lines.append(f"Telemt: v{version}") + + # Config status + config = load_json(GOTELEGRAM_CONFIG) + if config: + lines.append(f"Mode: {html.escape(str(config.get('mode', 'unknown')))}") + if "template" in config: + lines.append(f"Template: {html.escape(str(config['template']))}") + if "domain" in config: + lines.append(f"Domain: {html.escape(str(config['domain']))}") + if "port" in config: + lines.append(f"Port: {html.escape(str(config['port']))}") + + # Telemt config + telemt_cfg = load_toml(TELEMT_CONFIG) + if telemt_cfg: + cfg = telemt_cfg.get("config", {}) + if "listen_port" in cfg: + lines.append(f"Listen Port: {cfg['listen_port']}") + + # Backups + backup_count = 0 + try: + if os.path.exists(BACKUP_DIR): + backup_count = len([f for f in os.listdir(BACKUP_DIR) if f.endswith((".tar.gz", ".tar.gz.enc")) and not f.endswith(".sha256")]) + except Exception: + pass + lines.append(f"Backups: {backup_count}") + + # Old container check + old_container = await check_old_container() + if old_container: + lines.append(f"\n⚠️ Found old container: {html.escape(old_container)}") + lines.append("Run 'Install' to migrate") + + return "\n".join(lines) + + +async def cb_menu_status(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Status callback.""" + query = update.callback_query + await query.answer() + + await safe_edit_message(query,"⏳ Checking status...") + + status_text = await get_status_text() + keyboard = InlineKeyboardMarkup( + [[InlineKeyboardButton("Β« Back", callback_data="menu_main")]] + ) + await safe_edit_message(query, + status_text, reply_markup=keyboard, parse_mode="HTML" + ) + + +# ============================================================================ +# INSTALL +# ============================================================================ + + +def get_install_mode_menu() -> InlineKeyboardMarkup: + """Install mode selection menu.""" + buttons = [ + [InlineKeyboardButton("⚑ Quick Mode", callback_data="install_mode_quick")], + [InlineKeyboardButton("πŸ”’ Stealth Mode", callback_data="install_mode_stealth")], + [InlineKeyboardButton("Β« Back", callback_data="menu_main")], + ] + return InlineKeyboardMarkup(buttons) + + +async def cb_menu_install(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Install menu callback.""" + query = update.callback_query + await query.answer() + + # Check for old container + old_container = await check_old_container() + if old_container: + text = ( + f"⚠️ Migration from v1 detected\n\n" + f"Found Docker container: {html.escape(old_container)}\n\n" + f"Would you like to:\n" + f"1. Migrate from v1 (recommended)\n" + f"2. Fresh install (will remove old container)\n\n" + f"Select below or choose install mode" + ) + buttons = [ + [InlineKeyboardButton("πŸ”„ Migrate from v1", callback_data="install_migrate")], + [InlineKeyboardButton("✨ Fresh Install", callback_data="install_mode_quick")], + [InlineKeyboardButton("Β« Back", callback_data="menu_main")], + ] + keyboard = InlineKeyboardMarkup(buttons) + else: + text = "Select installation mode:" + keyboard = get_install_mode_menu() + + await safe_edit_message(query, + text, reply_markup=keyboard, parse_mode="HTML" + ) + + +async def cb_install_mode_quick(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Quick mode domain selection.""" + query = update.callback_query + await query.answer() + + # Show domains with pagination (4 per row, 2 rows) + buttons = [] + for i in range(0, len(QUICK_DOMAINS), 2): + row = [] + for j in range(2): + if i + j < len(QUICK_DOMAINS): + domain = QUICK_DOMAINS[i + j] + row.append( + InlineKeyboardButton( + domain, callback_data=f"quick_dom_{i+j}" + ) + ) + buttons.append(row) + + buttons.append([InlineKeyboardButton("Β« Back", callback_data="menu_install")]) + + text = "Select a domain for quick mode:" + keyboard = InlineKeyboardMarkup(buttons) + await safe_edit_message(query,text, reply_markup=keyboard) + + +async def cb_quick_domain(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Quick domain selection callback.""" + query = update.callback_query + data = query.data + try: + domain_idx = int(data.split("_")[-1]) + domain = QUICK_DOMAINS[domain_idx] + except (ValueError, IndexError): + await query.answer("Invalid domain selection") + return + + await query.answer() + await safe_edit_message(query,f"⏳ Installing with domain: {domain}...") + + # Simulate installation (in real scenario, call install script) + config = { + "mode": "quick", + "domain": domain, + "port": 443, + "installed_at": datetime.now().isoformat(), + } + + if save_json(GOTELEGRAM_CONFIG, config): + text = ( + f"βœ… Quick mode installed!\n\n" + f"Domain: {domain}\n" + f"Mode: Quick\n\n" + f"Service starting... Check status in 10 seconds." + ) + keyboard = InlineKeyboardMarkup( + [[InlineKeyboardButton("Β« Back", callback_data="menu_main")]] + ) + await safe_edit_message(query, + text, reply_markup=keyboard, parse_mode="HTML" + ) + else: + await safe_edit_message(query, + "❌ Failed to save configuration", + reply_markup=InlineKeyboardMarkup( + [[InlineKeyboardButton("Β« Back", callback_data="menu_install")]] + ), + ) + + +async def cb_install_mode_stealth(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Stealth mode - show template categories.""" + query = update.callback_query + await query.answer() + + catalog = load_json(TEMPLATES_CATALOG) + if not catalog or "categories" not in catalog: + await safe_edit_message(query, + "❌ Template catalog not found", + reply_markup=InlineKeyboardMarkup( + [[InlineKeyboardButton("Β« Back", callback_data="menu_install")]] + ), + ) + return + + buttons = [] + for cat in catalog.get("categories", []): + buttons.append( + [ + InlineKeyboardButton( + f"πŸ“ {cat['name']}", callback_data=f"stealth_cat_{cat['id']}" + ) + ] + ) + buttons.append([InlineKeyboardButton("Β« Back", callback_data="menu_install")]) + + text = "Stealth Mode - Select Template Category:" + keyboard = InlineKeyboardMarkup(buttons) + await safe_edit_message(query,text, reply_markup=keyboard) + + +async def cb_stealth_category(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Show templates in category.""" + query = update.callback_query + data = query.data + cat_id = data.removeprefix("stealth_cat_") + + await query.answer() + + catalog = load_json(TEMPLATES_CATALOG) + if not catalog: + await safe_edit_message(query,"❌ Template catalog not found") + return + + # Find category and templates + category = None + templates = [] + for cat in catalog.get("categories", []): + if cat["id"] == cat_id: + category = cat + templates = cat.get("templates", []) + break + + if not category: + await safe_edit_message(query,"❌ Category not found") + return + + buttons = [] + for tpl in templates: + buttons.append( + [ + InlineKeyboardButton( + f"🎨 {tpl['name']}", callback_data=f"stealth_tpl_{tpl['id']}" + ) + ] + ) + buttons.append([InlineKeyboardButton("Β« Back", callback_data="install_mode_stealth")]) + + text = f"Select template from {html.escape(category['name'])}:" + keyboard = InlineKeyboardMarkup(buttons) + await safe_edit_message(query,text, reply_markup=keyboard, parse_mode="HTML") + + +async def cb_stealth_template(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Show template preview and confirm.""" + query = update.callback_query + data = query.data + tpl_id = data.removeprefix("stealth_tpl_") + + await query.answer() + + catalog = load_json(TEMPLATES_CATALOG) + if not catalog: + await safe_edit_message(query,"❌ Template catalog not found") + return + + # Find template + template = None + for cat in catalog.get("categories", []): + for tpl in cat.get("templates", []): + if tpl["id"] == tpl_id: + template = tpl + break + if template: + break + + if not template: + await safe_edit_message(query,"❌ Template not found") + return + + tpl_name = html.escape(template.get('name', 'Unknown')) + tpl_desc = html.escape(template.get('description', 'N/A')) + text = ( + f"🎨 Template Preview\n\n" + f"Name: {tpl_name}\n" + f"Description: {tpl_desc}\n\n" + ) + if "preview_url" in template: + preview_url = html.escape(template['preview_url'], quote=True) + text += f'View Live Preview\n\n' + + text += "Confirm installation?" + + buttons = [ + [ + InlineKeyboardButton( + "βœ… Install", callback_data=f"stealth_confirm_{tpl_id}" + ) + ], + [InlineKeyboardButton("Β« Back", callback_data="install_mode_stealth")], + ] + keyboard = InlineKeyboardMarkup(buttons) + await safe_edit_message(query,text, reply_markup=keyboard, parse_mode="HTML") + + +async def cb_stealth_confirm(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Confirm and install stealth template.""" + query = update.callback_query + data = query.data + tpl_id = data.removeprefix("stealth_confirm_") + + await query.answer() + await safe_edit_message(query,"⏳ Installing template...") + + config = { + "mode": "stealth", + "template": tpl_id, + "port": 443, + "installed_at": datetime.now().isoformat(), + } + + if save_json(GOTELEGRAM_CONFIG, config): + text = ( + f"βœ… Stealth mode installed!\n\n" + f"Template: {html.escape(tpl_id)}\n" + f"Mode: Stealth\n\n" + f"Service starting... Check status in 10 seconds." + ) + keyboard = InlineKeyboardMarkup( + [[InlineKeyboardButton("Β« Back", callback_data="menu_main")]] + ) + await safe_edit_message(query, + text, reply_markup=keyboard, parse_mode="HTML" + ) + else: + await safe_edit_message(query, + "❌ Failed to save configuration", + reply_markup=InlineKeyboardMarkup( + [[InlineKeyboardButton("Β« Back", callback_data="menu_install")]] + ), + ) + + +# ============================================================================ +# PROXY LINK & SHARE +# ============================================================================ + + +async def get_proxy_link() -> Optional[str]: + """Generate proxy link from config.""" + config = load_json(GOTELEGRAM_CONFIG) + if not config: + return None + + # Get secret from telemt TOML config + secret = config.get("secret", "") + if not secret: + telemt_cfg = load_toml(TELEMT_CONFIG) + if telemt_cfg: + users = telemt_cfg.get("users", []) + if isinstance(users, list) and users: + secret = users[0].get("secret", "") + if not secret: + return None + + # Get server IP + 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" + + port = config.get("port", 443) + + return f"tg://proxy?server={server}&port={port}&secret={secret}" + + +async def cb_menu_link(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Generate and show proxy link.""" + query = update.callback_query + await query.answer() + + link = await get_proxy_link() + if not link: + text = "❌ Proxy not installed yet. Run install first." + else: + text = ( + f"πŸ”— Proxy Link\n\n" + f"{html.escape(link)}\n\n" + f"Open in Telegram to connect." + ) + + keyboard = InlineKeyboardMarkup( + [[InlineKeyboardButton("Β« Back", callback_data="menu_main")]] + ) + await safe_edit_message(query,text, reply_markup=keyboard, parse_mode="HTML") + + +async def cb_menu_share(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Share link as QR code.""" + query = update.callback_query + await query.answer() + + link = await get_proxy_link() + if not link: + text = "❌ Proxy not installed yet." + keyboard = InlineKeyboardMarkup( + [[InlineKeyboardButton("Β« Back", callback_data="menu_main")]] + ) + await safe_edit_message(query,text, reply_markup=keyboard) + return + + # Try to generate QR code + qr_file = None + code, _, _ = await sh("which", "qrencode") + if code == 0: + qr_path = "/tmp/proxy_qr.png" + code, _, _ = await sh("qrencode", "-o", qr_path, link) + if code == 0 and os.path.exists(qr_path): + qr_file = qr_path + + if qr_file: + try: + with open(qr_file, "rb") as f: + await query.message.reply_photo( + photo=f, + caption=f"πŸ“€ Proxy QR Code\n\n{html.escape(link)}", + parse_mode="HTML", + ) + except Exception as e: + logger.error(f"Failed to send QR code: {e}") + await safe_edit_message(query, + f"πŸ”— Proxy Link\n\n{html.escape(link)}", + parse_mode="HTML", + ) + finally: + try: + os.remove(qr_file) + except OSError: + pass + else: + await safe_edit_message(query, + f"πŸ”— Proxy Link\n\n{html.escape(link)}", + reply_markup=InlineKeyboardMarkup( + [[InlineKeyboardButton("Β« Back", callback_data="menu_main")]] + ), + parse_mode="HTML", + ) + + +# ============================================================================ +# RESTART & LOGS +# ============================================================================ + + +async def cb_menu_restart(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Restart service.""" + query = update.callback_query + await query.answer() + + text = "⏳ Restarting telemt service..." + await safe_edit_message(query,text) + + code, _, stderr = await sh("systemctl", "restart", TELEMT_SERVICE) + if code == 0: + text = "βœ… Service restarted successfully" + else: + text = f"❌ Failed to restart:\n{html.escape(stderr[:500])}" + + keyboard = InlineKeyboardMarkup( + [[InlineKeyboardButton("Β« Back", callback_data="menu_main")]] + ) + await safe_edit_message(query, + text, reply_markup=keyboard, parse_mode="HTML" + ) + + +async def cb_menu_logs(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Show recent logs.""" + query = update.callback_query + await query.answer() + + code, stdout, _ = await sh( + "journalctl", "-u", TELEMT_SERVICE, "-n", "30", "--no-pager" + ) + + if code == 0: + log_text = stdout[-1000:] if len(stdout) > 1000 else stdout + text = f"πŸ“‹ Recent Logs\n\n
{html.escape(log_text)}
" + else: + text = "❌ Failed to retrieve logs" + + keyboard = InlineKeyboardMarkup( + [[InlineKeyboardButton("Β« Back", callback_data="menu_main")]] + ) + await safe_edit_message(query,text, reply_markup=keyboard, parse_mode="HTML") + + +# ============================================================================ +# BACKUP & RESTORE +# ============================================================================ + + +async def cb_menu_backup(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Backup menu.""" + query = update.callback_query + await query.answer() + + # List existing backups + backups = [] + try: + if os.path.exists(BACKUP_DIR): + backups = sorted( + [f for f in os.listdir(BACKUP_DIR) + if f.endswith((".tar.gz", ".tar.gz.enc")) and not f.endswith(".sha256")], + reverse=True, + ) + except Exception: + pass + + buttons = [[InlineKeyboardButton("πŸ’Ύ Create Backup", callback_data="backup_create")]] + + if backups: + buttons.append( + [InlineKeyboardButton("πŸ“‹ List Backups", callback_data="backup_list")] + ) + + buttons.append([InlineKeyboardButton("Β« Back", callback_data="menu_main")]) + + text = f"πŸ’Ύ Backup Management\n\nExisting backups: {len(backups)}" + keyboard = InlineKeyboardMarkup(buttons) + await safe_edit_message(query,text, reply_markup=keyboard, parse_mode="HTML") + + +async def cb_backup_create(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Create backup.""" + query = update.callback_query + await query.answer() + + await safe_edit_message(query,"⏳ Creating backup...") + + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + backup_file = os.path.join(BACKUP_DIR, f"backup_{timestamp}.tar.gz") + + os.makedirs(BACKUP_DIR, exist_ok=True) + code, _, stderr = await sh( + "tar", "-czf", backup_file, GOTELEGRAM_CONFIG, TELEMT_CONFIG + ) + + if code == 0: + text = f"βœ… Backup created:\n{html.escape(backup_file)}" + else: + text = f"❌ Backup failed:\n{html.escape(stderr[:500])}" + + keyboard = InlineKeyboardMarkup( + [[InlineKeyboardButton("Β« Back", callback_data="menu_backup")]] + ) + await safe_edit_message(query,text, reply_markup=keyboard, parse_mode="HTML") + + +async def cb_backup_list(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """List backups.""" + query = update.callback_query + await query.answer() + + backups = [] + try: + if os.path.exists(BACKUP_DIR): + backups = sorted( + [f for f in os.listdir(BACKUP_DIR) if f.endswith((".tar.gz", ".tar.gz.enc")) and not f.endswith(".sha256")], + reverse=True, + ) + except Exception: + pass + + if not backups: + text = "No backups found" + else: + text = "πŸ“‹ Available Backups\n\n" + for backup in backups[:10]: + path = os.path.join(BACKUP_DIR, backup) + size = os.path.getsize(path) / (1024 * 1024) + text += f"{html.escape(backup)} ({size:.2f} MB)\n" + + keyboard = InlineKeyboardMarkup( + [[InlineKeyboardButton("Β« Back", callback_data="menu_backup")]] + ) + await safe_edit_message(query,text, reply_markup=keyboard, parse_mode="HTML") + + +async def cb_menu_restore(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Restore menu.""" + query = update.callback_query + await query.answer() + + backups = [] + try: + if os.path.exists(BACKUP_DIR): + backups = sorted( + [f for f in os.listdir(BACKUP_DIR) if f.endswith((".tar.gz", ".tar.gz.enc")) and not f.endswith(".sha256")], + reverse=True, + ) + except Exception: + pass + + if not backups: + text = "❌ No backups available" + keyboard = InlineKeyboardMarkup( + [[InlineKeyboardButton("Β« Back", callback_data="menu_main")]] + ) + else: + text = "Select backup to restore:" + buttons = [] + for i, backup in enumerate(backups[:10]): + buttons.append( + [ + InlineKeyboardButton( + backup, callback_data=f"restore_idx_{i}" + ) + ] + ) + buttons.append([InlineKeyboardButton("Β« Back", callback_data="menu_main")]) + keyboard = InlineKeyboardMarkup(buttons) + # Store backup list in user_data for retrieval + context.user_data["backup_list"] = backups[:10] + + await safe_edit_message(query,text, reply_markup=keyboard) + + +async def cb_restore_backup(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Execute backup restoration.""" + query = update.callback_query + data = query.data + + try: + idx = int(data.removeprefix("restore_idx_")) + except ValueError: + await query.answer("Invalid backup selection") + return + + backup_list = context.user_data.get("backup_list", []) + if idx < 0 or idx >= len(backup_list): + await query.answer("Backup not found") + return + + backup_name = backup_list[idx] + backup_path = os.path.join(BACKUP_DIR, backup_name) + + await query.answer() + await safe_edit_message(query,f"⏳ Restoring from {html.escape(backup_name)}...") + + if not os.path.exists(backup_path): + text = "❌ Backup file not found" + else: + # Simple restore: extract tar to overwrite configs + code, _, stderr = await sh( + "tar", "-xzf", backup_path, "-C", "/", timeout=60 + ) + if code == 0: + # Restart services + await sh("systemctl", "restart", TELEMT_SERVICE) + text = f"βœ… Restored from {html.escape(backup_name)}" + else: + text = f"❌ Restore failed:\n{html.escape(stderr[:500])}" + + keyboard = InlineKeyboardMarkup( + [[InlineKeyboardButton("Β« Back", callback_data="menu_main")]] + ) + await safe_edit_message(query,text, reply_markup=keyboard, parse_mode="HTML") + + +# ============================================================================ +# UPDATE & MODE/TEMPLATE CHANGE +# ============================================================================ + + +async def cb_menu_update(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Update telemt by re-running the install script's update logic.""" + query = update.callback_query + await query.answer() + + await safe_edit_message(query,"⏳ Checking for telemt updates...") + + # Get current version + cur_code, cur_out, _ = await sh("telemt", "--version") + current = cur_out.strip() if cur_code == 0 else "unknown" + + # Check latest release from GitHub + code, stdout, stderr = await sh( + "curl", "-s", "--max-time", "10", + "https://api.github.com/repos/telemt/telemt/releases/latest", + ) + + if code != 0 or not stdout.strip(): + text = "❌ Failed to check for updates" + else: + try: + release = json.loads(stdout) + latest = release.get("tag_name", "unknown") + if latest == current: + text = f"βœ… telemt is already up to date ({html.escape(current)})" + else: + text = ( + f"ℹ️ Update available: {html.escape(current)} β†’ {html.escape(latest)}\n\n" + f"Run the CLI installer to update:\n" + f"sudo bash install.sh β†’ menu item 10" + ) + except json.JSONDecodeError: + text = "❌ Failed to parse release info" + + keyboard = InlineKeyboardMarkup( + [[InlineKeyboardButton("Β« Back", callback_data="menu_main")]] + ) + await safe_edit_message(query,text, reply_markup=keyboard, parse_mode="HTML") + + +async def cb_menu_change(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Change mode or template.""" + query = update.callback_query + await query.answer() + + buttons = [ + [InlineKeyboardButton("⚑ Switch to Quick Mode", callback_data="change_quick")], + [InlineKeyboardButton("πŸ”’ Switch to Stealth Mode", callback_data="change_stealth")], + [InlineKeyboardButton("Β« Back", callback_data="menu_main")], + ] + keyboard = InlineKeyboardMarkup(buttons) + await safe_edit_message(query, + "Change mode or template:", reply_markup=keyboard + ) + + +async def cb_change_quick(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Switch to quick mode β€” show domain selection.""" + query = update.callback_query + await query.answer() + # Reuse the quick mode domain selection flow + await cb_install_mode_quick(update, context) + + +async def cb_change_stealth(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Switch to stealth mode β€” show template categories.""" + query = update.callback_query + await query.answer() + # Reuse the stealth mode template selection flow + await cb_install_mode_stealth(update, context) + + +async def cb_install_migrate(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Migrate from v1 (mtg Docker) to v2 (telemt).""" + query = update.callback_query + await query.answer() + + await safe_edit_message(query,"⏳ Migrating from v1...") + + # Stop old mtg container + code, _, stderr = await sh("docker", "stop", "mtproto-proxy", timeout=30) + if code != 0: + code, _, stderr = await sh("docker", "stop", "mtg", timeout=30) + + # Remove old container + await sh("docker", "rm", "mtproto-proxy", timeout=15) + await sh("docker", "rm", "mtg", timeout=15) + + text = ( + "βœ… v1 container stopped and removed\n\n" + "Now select installation mode for v2:" + ) + keyboard = get_install_mode_menu() + await safe_edit_message(query,text, reply_markup=keyboard, parse_mode="HTML") + + +# ============================================================================ +# WEBSITE & SSL +# ============================================================================ + + +async def cb_menu_website(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Website and SSL management.""" + query = update.callback_query + await query.answer() + + buttons = [ + [InlineKeyboardButton("πŸ”„ Renew SSL Certificate", callback_data="ssl_renew")], + [InlineKeyboardButton("πŸ“Š SSL Status", callback_data="ssl_status")], + [InlineKeyboardButton("Β« Back", callback_data="menu_main")], + ] + keyboard = InlineKeyboardMarkup(buttons) + await safe_edit_message(query, + "Website & SSL Management:", reply_markup=keyboard + ) + + +async def cb_ssl_renew(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Renew SSL certificate.""" + query = update.callback_query + await query.answer() + + await safe_edit_message(query,"⏳ Renewing SSL certificate...") + + code, stdout, stderr = await sh("certbot", "renew", timeout=120) + + if code == 0: + text = "βœ… SSL certificate renewed successfully" + else: + text = f"❌ Renewal failed:\n{html.escape(stderr[:500])}" + + keyboard = InlineKeyboardMarkup( + [[InlineKeyboardButton("Β« Back", callback_data="menu_website")]] + ) + await safe_edit_message(query,text, reply_markup=keyboard, parse_mode="HTML") + + +async def cb_ssl_status(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Show SSL status.""" + query = update.callback_query + await query.answer() + + code, stdout, _ = await sh("certbot", "certificates") + + if code == 0: + text = f"πŸ“Š SSL Certificates\n\n
{html.escape(stdout[:1000])}
" + else: + text = "❌ Failed to get SSL status" + + keyboard = InlineKeyboardMarkup( + [[InlineKeyboardButton("Β« Back", callback_data="menu_website")]] + ) + await safe_edit_message(query,text, reply_markup=keyboard, parse_mode="HTML") + + +# ============================================================================ +# PROMO & CREDITS +# ============================================================================ + + +async def cb_menu_promo(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Promo information.""" + query = update.callback_query + await query.answer() + + text = ( + f"🎁 GoTelegram Promo\n\n" + f"Share the love! Invite friends to use GoTelegram.\n\n" + f"Promo Link\n\n" + f"Support development:\n" + f"Send a Tip" + ) + + keyboard = InlineKeyboardMarkup( + [[InlineKeyboardButton("Β« Back", callback_data="menu_main")]] + ) + await safe_edit_message(query,text, reply_markup=keyboard, parse_mode="HTML") + + +async def cb_menu_credits(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Credits and acknowledgements.""" + query = update.callback_query + await query.answer() + + text = ( + f"ℹ️ Credits & Acknowledgements\n\n" + f"GoTelegram v{GOTELEGRAM_VERSION}\n\n" + f"Built with love for the Telegram community\n\n" + f"Special thanks to:\n\n" + f"πŸ™ telemt - MTProxy engine\n" + f" High-performance proxy core\n\n" + f"🎨 HTML5UP - Beautiful web templates\n" + f" Responsive design & themes\n\n" + f"πŸ“š Learning Zone - Educational resources\n" + f" Community learning support\n\n" + f"πŸš€ Start Bootstrap - Bootstrap templates\n" + f" Professional design framework\n\n" + f"πŸ’¬ Community - Your feedback & support\n\n" + f"GoTelegram is open-source and community-driven" + ) + + keyboard = InlineKeyboardMarkup( + [[InlineKeyboardButton("Β« Back", callback_data="menu_main")]] + ) + await safe_edit_message(query,text, reply_markup=keyboard, parse_mode="HTML") + + +# ============================================================================ +# REMOVE +# ============================================================================ + + +async def cb_menu_remove(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Remove installation.""" + query = update.callback_query + await query.answer() + + text = ( + "⚠️ Remove GoTelegram\n\n" + "This will completely remove the installation.\n" + "Are you sure?" + ) + + buttons = [ + [InlineKeyboardButton("❌ Yes, Remove", callback_data="remove_confirm")], + [InlineKeyboardButton("Β« Back", callback_data="menu_main")], + ] + keyboard = InlineKeyboardMarkup(buttons) + await safe_edit_message(query,text, reply_markup=keyboard, parse_mode="HTML") + + +async def cb_remove_confirm(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Confirm removal.""" + query = update.callback_query + await query.answer() + + await safe_edit_message(query,"⏳ Removing GoTelegram...") + + # Stop service + await sh("systemctl", "stop", TELEMT_SERVICE) + + # Remove directories + for path in ["/opt/gotelegram", WEBSITE_ROOT]: + await sh("rm", "-rf", path) + + text = "βœ… GoTelegram removed successfully" + keyboard = InlineKeyboardMarkup( + [[InlineKeyboardButton("Β« Back", callback_data="menu_main")]] + ) + await safe_edit_message(query,text, reply_markup=keyboard, parse_mode="HTML") + + +# ============================================================================ +# CALLBACK ROUTING +# ============================================================================ + + +async def handle_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Route all callbacks.""" + query = update.callback_query + data = query.data + + # Access control + if not is_user_allowed(update.effective_user.id): + await query.answer("Access denied") + return + + # Main menu + if data == "menu_main": + await query.answer() + buttons = get_main_menu() + text = ( + f"GoTelegram v{GOTELEGRAM_VERSION}\n\n" + "πŸ€– MTProxy Management\n" + "Select an action:" + ) + await safe_edit_message(query,text, reply_markup=buttons, parse_mode="HTML") + return + + if data == "close_menu": + await query.answer() + await query.delete_message() + return + + # Dispatch to handlers + handlers = { + "menu_install": cb_menu_install, + "menu_status": cb_menu_status, + "menu_link": cb_menu_link, + "menu_share": cb_menu_share, + "menu_restart": cb_menu_restart, + "menu_logs": cb_menu_logs, + "menu_backup": cb_menu_backup, + "menu_restore": cb_menu_restore, + "menu_update": cb_menu_update, + "menu_change": cb_menu_change, + "menu_website": cb_menu_website, + "menu_promo": cb_menu_promo, + "menu_credits": cb_menu_credits, + "menu_remove": cb_menu_remove, + "install_mode_quick": cb_install_mode_quick, + "install_mode_stealth": cb_install_mode_stealth, + "backup_create": cb_backup_create, + "backup_list": cb_backup_list, + "ssl_renew": cb_ssl_renew, + "ssl_status": cb_ssl_status, + "remove_confirm": cb_remove_confirm, + "change_quick": cb_change_quick, + "change_stealth": cb_change_stealth, + "install_migrate": cb_install_migrate, + } + + # Pattern-based handlers + if data.startswith("quick_dom_"): + await cb_quick_domain(update, context) + elif data.startswith("stealth_cat_"): + await cb_stealth_category(update, context) + elif data.startswith("stealth_tpl_"): + await cb_stealth_template(update, context) + elif data.startswith("stealth_confirm_"): + await cb_stealth_confirm(update, context) + elif data.startswith("restore_idx_"): + await cb_restore_backup(update, context) + elif data in handlers: + await handlers[data](update, context) + else: + await query.answer("Unknown action") + + +# ============================================================================ +# ERROR HANDLERS +# ============================================================================ + + +async def error_handler(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Log errors caused by Updates.""" + logger.error(f"Exception while handling an update:", exc_info=context.error) + + +# ============================================================================ +# MAIN APPLICATION +# ============================================================================ + + +def main() -> None: + """Start the bot.""" + if not BOT_TOKEN: + logger.error("BOT_TOKEN not set in .env file") + return + + # Create the Application + application = Application.builder().token(BOT_TOKEN).build() + + # Command handlers + application.add_handler(CommandHandler("start", cmd_start)) + application.add_handler(CommandHandler("help", cmd_help)) + application.add_handler(CommandHandler("status", cmd_status)) + application.add_handler(CommandHandler("logs", cmd_logs)) + + # Callback query handler (buttons) + application.add_handler(CallbackQueryHandler(handle_callback)) + + # Error handler + application.add_error_handler(error_handler) + + # Run the bot + logger.info(f"GoTelegram v{GOTELEGRAM_VERSION} bot starting...") + application.run_polling(allowed_updates=Update.ALL_TYPES) + + +if __name__ == "__main__": + main() diff --git a/lib/common.sh b/lib/common.sh index 2804498..7aae651 100644 --- a/lib/common.sh +++ b/lib/common.sh @@ -1,456 +1,456 @@ -#!/bin/bash -# GoTelegram v2.2 β€” ΠžΠ±Ρ‰ΠΈΠ΅ ΡƒΡ‚ΠΈΠ»ΠΈΡ‚Ρ‹ -# Π¦Π²Π΅Ρ‚Π°, Π»ΠΎΠ³ΠΈΡ€ΠΎΠ²Π°Π½ΠΈΠ΅, спиннСр, систСмныС Ρ„ΡƒΠ½ΠΊΡ†ΠΈΠΈ, ΡΠΎΠ²ΠΌΠ΅ΡΡ‚ΠΈΠΌΠΎΡΡ‚ΡŒ с v1 - -# ── ВСрсия ──────────────────────────────────────────────────────────────────── -GOTELEGRAM_VERSION="2.2.0" -GOTELEGRAM_NAME="GoTelegram" - -# ── ΠŸΡƒΡ‚ΠΈ ────────────────────────────────────────────────────────────────────── -GOTELEGRAM_DIR="/opt/gotelegram" -GOTELEGRAM_CONFIG="$GOTELEGRAM_DIR/config.json" -TELEMT_CONFIG="/etc/telemt/config.toml" -TELEMT_BIN="/usr/local/bin/telemt" -TELEMT_SERVICE="telemt" -NGINX_SITE_CONF="/etc/nginx/sites-available/gotelegram" -NGINX_SITE_LINK="/etc/nginx/sites-enabled/gotelegram" -WEBSITE_ROOT="/var/www/gotelegram-site" -BACKUP_DIR="$GOTELEGRAM_DIR/backups" -LOG_FILE="/var/log/gotelegram.log" -BOT_DIR="/opt/gotelegram-bot" - -# ── V1 ΡΠΎΠ²ΠΌΠ΅ΡΡ‚ΠΈΠΌΠΎΡΡ‚ΡŒ ───────────────────────────────────────────────────────── -V1_CONTAINER_NAME="mtproto-proxy" -V1_CONFIG_FILE="/opt/gotelegram-bot/proxy.json" -V1_SERVICE_NAME="gotelegram-bot" - -# ── Π¦Π²Π΅Ρ‚Π° ──────────────────────────────────────────────────────────────────── -RED='\033[0;31m' -GREEN='\033[0;32m' -CYAN='\033[0;36m' -YELLOW='\033[1;33m' -MAGENTA='\033[0;35m' -BLUE='\033[0;34m' -WHITE='\033[1;37m' -BOLD='\033[1m' -DIM='\033[2m' -NC='\033[0m' - -# ── Π›ΠΎΠ³ΠΈΡ€ΠΎΠ²Π°Π½ΠΈΠ΅ ────────────────────────────────────────────────────────────── -log_info() { echo -e " ${CYAN}β„Ή${NC} $*"; } -log_success() { echo -e " ${GREEN}βœ“${NC} $*"; } -log_warning() { echo -e " ${YELLOW}⚠${NC} $*"; } -log_error() { echo -e " ${RED}βœ—${NC} $*"; } -log_step() { echo -e "\n${BOLD}${WHITE} $*${NC}"; } -log_dim() { echo -e " ${DIM}$*${NC}"; } - -log_to_file() { - local ts; ts=$(date '+%Y-%m-%d %H:%M:%S') - echo "[$ts] $*" >> "$LOG_FILE" 2>/dev/null -} - -# ── Π‘ΠΏΠΈΠ½Π½Π΅Ρ€ ────────────────────────────────────────────────────────────────── -_spin_pid="" -spinner_start() { - local msg="${1:-ΠŸΠΎΠ΄ΠΎΠΆΠ΄ΠΈΡ‚Π΅...}" - ( - local frames=('β ‹' 'β ™' 'β Ή' 'β Έ' 'β Ό' 'β ΄' 'β ¦' 'β §' 'β ‡' '⠏') - local i=0 - while true; do - printf "\r ${CYAN}${frames[$i]}${NC} ${msg}" >&2 - i=$(( (i+1) % ${#frames[@]} )) - sleep 0.1 - done - ) & - _spin_pid=$! -} - -spinner_stop() { - [ -n "$_spin_pid" ] && kill "$_spin_pid" 2>/dev/null && wait "$_spin_pid" 2>/dev/null - _spin_pid="" - printf "\r\033[K" >&2 -} - -# ── ΠŸΡ€ΠΎΠ³Ρ€Π΅ΡΡ-Π±Π°Ρ€ ───────────────────────────────────────────────────────────── -progress_bar() { - local current="$1" total="$2" label="${3:-}" - local pct=$(( current * 100 / total )) - local filled=$(( pct / 2 )) - local empty=$(( 50 - filled )) - local bar="" - for ((i=0; i&2 - [ "$current" -eq "$total" ] && echo "" >&2 -} - -# ── Π’Ρ‹ΠΏΠΎΠ»Π½Π΅Π½ΠΈΠ΅ с ΠΈΠ½Π΄ΠΈΠΊΠ°Ρ‚ΠΎΡ€ΠΎΠΌ ───────────────────────────────────────────────── -run_with_spinner() { - local label="$1"; shift - local err_file="/tmp/.gotelegram_spinner_err_$$" - spinner_start "$label" - "$@" >/dev/null 2>"$err_file" - local rc=$? - spinner_stop - if [ $rc -eq 0 ]; then - log_success "$label" - else - log_error "$label ${RED}(ошибка, ΠΊΠΎΠ΄: $rc)${NC}" - if [ -s "$err_file" ]; then - log_dim " $(head -3 "$err_file")" - fi - fi - rm -f "$err_file" - return $rc -} - -# ── Π‘Π°Π½Π½Π΅Ρ€ ─────────────────────────────────────────────────────────────────── -show_banner() { - echo "" - echo -e "${CYAN}╔══════════════════════════════════════════════════════════╗${NC}" - echo -e "${CYAN}β•‘${NC} ${BOLD}${WHITE}πŸš€ GoTelegram v${GOTELEGRAM_VERSION}${NC} ${CYAN}β•‘${NC}" - echo -e "${CYAN}β•‘${NC} ${DIM}MTProxy Π½Π° ядрС telemt (Rust + Tokio)${NC} ${CYAN}β•‘${NC}" - echo -e "${CYAN}β•‘${NC} ${DIM}Anti-DPI β€’ Fake TLS β€’ TCP Splice β€’ JA3/JA4${NC} ${CYAN}β•‘${NC}" - echo -e "${CYAN}β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•${NC}" - echo "" -} - -# ── Благодарности ──────────────────────────────────────────────────────────── -show_credits() { - echo "" - echo -e "${MAGENTA}╔══════════════════════════════════════════════════════════╗${NC}" - echo -e "${MAGENTA}β•‘${NC} ${BOLD}Благодарности / Credits${NC} ${MAGENTA}β•‘${NC}" - echo -e "${MAGENTA}β•Ÿβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β•’${NC}" - echo -e "${MAGENTA}β•‘${NC} ${WHITE}telemt${NC} β€” MTProxy engine (Rust) ${MAGENTA}β•‘${NC}" - echo -e "${MAGENTA}β•‘${NC} ${DIM}github.com/telemt/telemt${NC} ${MAGENTA}β•‘${NC}" - echo -e "${MAGENTA}β•‘${NC} ${MAGENTA}β•‘${NC}" - echo -e "${MAGENTA}β•‘${NC} ${WHITE}HTML5 UP${NC} β€” Π°Π΄Π°ΠΏΡ‚ΠΈΠ²Π½Ρ‹Π΅ HTML/CSS ΡˆΠ°Π±Π»ΠΎΠ½Ρ‹ ${MAGENTA}β•‘${NC}" - echo -e "${MAGENTA}β•‘${NC} ${DIM}html5up.net β€’ CC BY 3.0 β€’ @ajlkn${NC} ${MAGENTA}β•‘${NC}" - echo -e "${MAGENTA}β•‘${NC} ${MAGENTA}β•‘${NC}" - echo -e "${MAGENTA}β•‘${NC} ${WHITE}learning-zone${NC} β€” 150+ HTML5 шаблонов ${MAGENTA}β•‘${NC}" - echo -e "${MAGENTA}β•‘${NC} ${DIM}github.com/learning-zone/website-templates${NC} ${MAGENTA}β•‘${NC}" - echo -e "${MAGENTA}β•‘${NC} ${MAGENTA}β•‘${NC}" - echo -e "${MAGENTA}β•‘${NC} ${WHITE}Start Bootstrap${NC} β€” MIT лицСнзия ${MAGENTA}β•‘${NC}" - echo -e "${MAGENTA}β•‘${NC} ${DIM}startbootstrap.com${NC} ${MAGENTA}β•‘${NC}" - echo -e "${MAGENTA}β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•${NC}" - echo "" -} - -# ── БистСмныС ΡƒΡ‚ΠΈΠ»ΠΈΡ‚Ρ‹ ──────────────────────────────────────────────────────── -_valid_ip() { - # Validate that each octet is 0-255 - local ip="$1" - local IFS='.' - read -ra octets <<< "$ip" - [ ${#octets[@]} -ne 4 ] && return 1 - for octet in "${octets[@]}"; do - [[ "$octet" =~ ^[0-9]+$ ]] || return 1 - [ "$octet" -gt 255 ] && return 1 - done - return 0 -} - -get_server_ip() { - local ip raw - for url in "https://api.ipify.org" "https://icanhazip.com" "https://ifconfig.me"; do - raw=$(curl -s -4 --max-time 5 "$url" 2>/dev/null) - ip=$(echo "$raw" | grep -Eo '([0-9]{1,3}\.){3}[0-9]{1,3}' | head -1) - if [ -n "$ip" ] && _valid_ip "$ip"; then - echo "$ip" - return 0 - fi - done - echo "0.0.0.0" - return 1 -} - -check_root() { - if [ "$EUID" -ne 0 ]; then - log_error "ЗапуститС скрипт с sudo / ΠΎΡ‚ root" - exit 1 - fi -} - -check_os() { - if [ ! -f /etc/os-release ]; then - log_error "НС ΡƒΠ΄Π°Π»ΠΎΡΡŒ ΠΎΠΏΡ€Π΅Π΄Π΅Π»ΠΈΡ‚ΡŒ ОБ. ВрСбуСтся Linux." - return 1 - fi - # Validate os-release before sourcing (reject command injection: ;, backticks, $()) - if grep -qE '(;|`|\$\(|\$\{)' /etc/os-release 2>/dev/null; then - log_warning "/etc/os-release содСрТит ΠΏΠΎΠ΄ΠΎΠ·Ρ€ΠΈΡ‚Π΅Π»ΡŒΠ½Ρ‹Π΅ строки, пропускаСм" - return 0 - fi - . /etc/os-release - case "$ID" in - ubuntu|debian|centos|rocky|almalinux|fedora|rhel) - log_dim "ОБ: $PRETTY_NAME" - return 0 - ;; - *) - log_warning "ОБ $ID ΠΌΠΎΠΆΠ΅Ρ‚ Π±Ρ‹Ρ‚ΡŒ нСсовмСстима. ΠŸΠΎΠ΄Π΄Π΅Ρ€ΠΆΠΈΠ²Π°ΡŽΡ‚ΡΡ: Ubuntu, Debian, CentOS, Rocky." - return 0 - ;; - esac -} - -get_arch() { - local arch - arch=$(uname -m) - case "$arch" in - x86_64|amd64) echo "amd64" ;; - aarch64|arm64) echo "arm64" ;; - armv7*|armhf) echo "armv7" ;; - *) echo "$arch" ;; - esac -} - -get_pkg_manager() { - if command -v apt-get &>/dev/null; then echo "apt" - elif command -v dnf &>/dev/null; then echo "dnf" - elif command -v yum &>/dev/null; then echo "yum" - else echo "unknown" - fi -} - -install_pkg() { - local pkg="$1" - case "$(get_pkg_manager)" in - apt) apt-get install -y -qq "$pkg" ;; - dnf) dnf install -y -q "$pkg" ;; - yum) yum install -y -q "$pkg" ;; - *) log_error "НСизвСстный ΠΏΠ°ΠΊΠ΅Ρ‚Π½Ρ‹ΠΉ ΠΌΠ΅Π½Π΅Π΄ΠΆΠ΅Ρ€"; return 1 ;; - esac -} - -ensure_deps() { - local missing=() - for cmd in curl jq openssl git; do - if ! command -v "$cmd" &>/dev/null; then - missing+=("$cmd") - fi - done - if [ ${#missing[@]} -gt 0 ]; then - log_step "Установка зависимостСй: ${missing[*]}" - case "$(get_pkg_manager)" in - apt) apt-get update -qq && apt-get install -y -qq "${missing[@]}" ;; - dnf) dnf install -y -q "${missing[@]}" ;; - yum) yum install -y -q "${missing[@]}" ;; - esac - fi -} - -check_port() { - local port="$1" - local line - line=$(ss -tlnp 2>/dev/null | grep -E ":${port}\b" | head -1) - [ -z "$line" ] && line=$(netstat -tlnp 2>/dev/null | grep -E ":${port}\b" | head -1) - if [ -n "$line" ]; then - echo "$line" - return 0 # ΠΏΠΎΡ€Ρ‚ занят - fi - return 1 # свободСн -} - -check_disk_space() { - local min_mb="${1:-500}" - local avail_mb - avail_mb=$(df -m / | awk 'NR==2 {print $4}') - if [ "$avail_mb" -lt "$min_mb" ]; then - log_error "Мало мСста Π½Π° дискС: ${avail_mb}MB (Π½ΡƒΠΆΠ½ΠΎ ${min_mb}MB+)" - return 1 - fi - return 0 -} - -# ── ΠšΠΎΠ½Ρ„ΠΈΠ³ΡƒΡ€Π°Ρ†ΠΈΡ GoTelegram (JSON) ────────────────────────────────────────── -save_gotelegram_config() { - mkdir -p "$(dirname "$GOTELEGRAM_CONFIG")" - cat > "$GOTELEGRAM_CONFIG" << EOJSON -{ - "version": "$GOTELEGRAM_VERSION", - "engine": "${1:-telemt}", - "mode": "${2:-quick}", - "port": ${3:-443}, - "secret": "${4:-}", - "mask_host": "${5:-google.com}", - "domain": "${6:-}", - "template_id": "${7:-}", - "installed_at": "$(date -Iseconds)", - "updated_at": "$(date -Iseconds)" -} -EOJSON - chmod 600 "$GOTELEGRAM_CONFIG" -} - -load_gotelegram_config() { - if [ -f "$GOTELEGRAM_CONFIG" ]; then - cat "$GOTELEGRAM_CONFIG" - return 0 - fi - echo "{}" - return 1 -} - -config_get() { - local key="$1" - if [ ! -f "$GOTELEGRAM_CONFIG" ]; then - log_dim "ΠšΠΎΠ½Ρ„ΠΈΠ³ Π½Π΅ Π½Π°ΠΉΠ΄Π΅Π½: $GOTELEGRAM_CONFIG" >&2 - return 2 # file missing - fi - local val - val=$(jq -r ".$key // empty" "$GOTELEGRAM_CONFIG" 2>/dev/null) - if [ $? -ne 0 ]; then - log_dim "Ошибка чтСния JSON: $GOTELEGRAM_CONFIG" >&2 - return 3 # invalid JSON - fi - if [ -z "$val" ]; then - return 1 # key missing or empty - fi - echo "$val" - return 0 -} - -# ── V1 ΡΠΎΠ²ΠΌΠ΅ΡΡ‚ΠΈΠΌΠΎΡΡ‚ΡŒ ───────────────────────────────────────────────────────── -detect_v1_installation() { - # ΠŸΡ€ΠΎΠ²Π΅Ρ€ΡΠ΅ΠΌ Π½Π°Π»ΠΈΡ‡ΠΈΠ΅ mtg Docker ΠΊΠΎΠ½Ρ‚Π΅ΠΉΠ½Π΅Ρ€Π° (v1) - if command -v docker &>/dev/null; then - if docker ps -a --format '{{.Names}}' 2>/dev/null | grep -q "^${V1_CONTAINER_NAME}$"; then - return 0 # v1 ΠΎΠ±Π½Π°Ρ€ΡƒΠΆΠ΅Π½Π° - fi - fi - # ΠŸΡ€ΠΎΠ²Π΅Ρ€ΡΠ΅ΠΌ Π½Π°Π»ΠΈΡ‡ΠΈΠ΅ ΠΊΠΎΠ½Ρ„ΠΈΠ³Π° v1 - if [ -f "$V1_CONFIG_FILE" ]; then - return 0 - fi - return 1 -} - -get_v1_config() { - # ИзвлСкаСм Π΄Π°Π½Π½Ρ‹Π΅ ΠΈΠ· Ρ€Π°Π±ΠΎΡ‚Π°ΡŽΡ‰Π΅Π³ΠΎ v1 ΠΊΠΎΠ½Ρ‚Π΅ΠΉΠ½Π΅Ρ€Π° - if ! command -v docker &>/dev/null; then - echo "{}" - return 1 - fi - - local running - running=$(docker ps --format '{{.Names}}' 2>/dev/null | grep "^${V1_CONTAINER_NAME}$") - - if [ -z "$running" ]; then - # ΠŸΡ€ΠΎΠ±ΡƒΠ΅ΠΌ ΠΈΠ· сохранённого ΠΊΠΎΠ½Ρ„ΠΈΠ³Π° - if [ -f "$V1_CONFIG_FILE" ]; then - cat "$V1_CONFIG_FILE" - return 0 - fi - echo "{}" - return 1 - fi - - # Достаём ΠΈΠ· Docker - local cmd_str port secret ip - cmd_str=$(docker inspect "$V1_CONTAINER_NAME" --format='{{range .Config.Cmd}}{{.}} {{end}}' 2>/dev/null) - secret=$(echo "$cmd_str" | awk '{print $NF}') - port=$(docker inspect "$V1_CONTAINER_NAME" --format='{{range $p,$c := .HostConfig.PortBindings}}{{(index $c 0).HostPort}}{{end}}' 2>/dev/null) - ip=$(get_server_ip) - - jq -n \ - --arg secret "$secret" \ - --arg port "${port:-443}" \ - --arg ip "$ip" \ - '{secret: $secret, port: ($port | tonumber), ip: $ip, engine: "mtg"}' -} - -migrate_v1_to_v2() { - log_step "ΠœΠΈΠ³Ρ€Π°Ρ†ΠΈΡ с v1 (mtg) Π½Π° v2 (telemt)" - - local v1_config - v1_config=$(get_v1_config) - - local old_port old_secret - old_port=$(echo "$v1_config" | jq -r '.port // 443') - old_secret=$(echo "$v1_config" | jq -r '.secret // empty') - - if [ -z "$old_secret" ]; then - log_warning "НС ΡƒΠ΄Π°Π»ΠΎΡΡŒ ΠΈΠ·Π²Π»Π΅Ρ‡ΡŒ secret ΠΈΠ· v1. Π‘ΡƒΠ΄Π΅Ρ‚ создан Π½ΠΎΠ²Ρ‹ΠΉ." - return 1 - fi - - echo "" - echo -e " ${WHITE}НайдСна установка v1 (mtg):${NC}" - echo -e " ΠŸΠΎΡ€Ρ‚: ${CYAN}${old_port}${NC}" - echo -e " Secret: ${CYAN}${old_secret:0:16}...${NC}" - echo "" - echo -e " ${YELLOW}Π’Π½ΠΈΠΌΠ°Π½ΠΈΠ΅:${NC} сСкрСт mtg НЕ совмСстим с telemt Π½Π°ΠΏΡ€ΡΠΌΡƒΡŽ." - echo -e " ΠšΠ»ΠΈΠ΅Π½Ρ‚Π°ΠΌ потрСбуСтся новая ссылка." - echo "" - echo -ne " ΠžΡΡ‚Π°Π½ΠΎΠ²ΠΈΡ‚ΡŒ v1 ΠΊΠΎΠ½Ρ‚Π΅ΠΉΠ½Π΅Ρ€ ΠΈ ΠΏΠ΅Ρ€Π΅ΠΉΡ‚ΠΈ Π½Π° v2? [Y/n]: " - read -r ans - if [[ "$ans" =~ ^[Nn] ]]; then - log_info "ΠœΠΈΠ³Ρ€Π°Ρ†ΠΈΡ ΠΎΡ‚ΠΌΠ΅Π½Π΅Π½Π°. v1 оставлСн Π±Π΅Π· ΠΈΠ·ΠΌΠ΅Π½Π΅Π½ΠΈΠΉ." - return 1 - fi - - # ΠžΡΡ‚Π°Π½Π°Π²Π»ΠΈΠ²Π°Π΅ΠΌ v1 - log_info "ΠžΡΡ‚Π°Π½ΠΎΠ²ΠΊΠ° v1 ΠΊΠΎΠ½Ρ‚Π΅ΠΉΠ½Π΅Ρ€Π°..." - docker stop "$V1_CONTAINER_NAME" 2>/dev/null - docker rm "$V1_CONTAINER_NAME" 2>/dev/null - - # Π‘Π΅ΠΊΠ°ΠΏΠΈΠΌ v1 ΠΊΠΎΠ½Ρ„ΠΈΠ³ - if [ -f "$V1_CONFIG_FILE" ]; then - mkdir -p "$GOTELEGRAM_DIR" - cp "$V1_CONFIG_FILE" "$GOTELEGRAM_DIR/v1_backup_proxy.json" 2>/dev/null - log_success "ΠšΠΎΠ½Ρ„ΠΈΠ³ v1 сохранён Π² $GOTELEGRAM_DIR/v1_backup_proxy.json" - fi - - log_success "v1 остановлСн. ΠŸΠΎΡ€Ρ‚ $old_port освобоТдён." - return 0 -} - -# ── ΠŸΠΎΠ΄Ρ‚Π²Π΅Ρ€ΠΆΠ΄Π΅Π½ΠΈΠ΅ ──────────────────────────────────────────────────────────── -confirm() { - local msg="${1:-ΠŸΡ€ΠΎΠ΄ΠΎΠ»ΠΆΠΈΡ‚ΡŒ?}" - echo -ne " ${msg} [Y/n]: " - read -r ans - [[ ! "$ans" =~ ^[Nn] ]] -} - -# ── Π’Ρ‹Π±ΠΎΡ€ ΠΈΠ· списка ────────────────────────────────────────────────────────── -select_option() { - local title="$1" - shift - local options=("$@") - - echo "" - echo -e " ${BOLD}${WHITE}${title}${NC}" - echo -e " ${DIM}$(printf '─%.0s' {1..50})${NC}" - local i=1 - for opt in "${options[@]}"; do - echo -e " ${CYAN}${i})${NC} ${opt}" - ((i++)) - done - echo -e " ${DIM}$(printf '─%.0s' {1..50})${NC}" - echo -ne " ${WHITE}Π’Ρ‹Π±ΠΎΡ€:${NC} " - read -r choice - echo "$choice" -} - -# ── ГСнСрация случайного hex ───────────────────────────────────────────────── -generate_hex() { - local len="${1:-32}" - openssl rand -hex "$((len/2))" 2>/dev/null || head -c "$((len/2))" /dev/urandom | xxd -p | tr -d '\n' -} - -# ── ΠŸΡ€ΠΎΠ²Π΅Ρ€ΠΊΠ° Π΄ΠΎΠΌΠ΅Π½Π° ────────────────────────────────────────────────────────── -validate_domain() { - local domain="$1" - if echo "$domain" | grep -qE '^([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$'; then - return 0 - fi - return 1 -} - -# ── Init: созданиС Π΄ΠΈΡ€Π΅ΠΊΡ‚ΠΎΡ€ΠΈΠΉ ──────────────────────────────────────────────── -init_dirs() { - mkdir -p "$GOTELEGRAM_DIR" "$BACKUP_DIR" /etc/telemt 2>/dev/null - touch "$LOG_FILE" 2>/dev/null -} +#!/bin/bash +# GoTelegram v2.2 β€” ΠžΠ±Ρ‰ΠΈΠ΅ ΡƒΡ‚ΠΈΠ»ΠΈΡ‚Ρ‹ +# Π¦Π²Π΅Ρ‚Π°, Π»ΠΎΠ³ΠΈΡ€ΠΎΠ²Π°Π½ΠΈΠ΅, спиннСр, систСмныС Ρ„ΡƒΠ½ΠΊΡ†ΠΈΠΈ, ΡΠΎΠ²ΠΌΠ΅ΡΡ‚ΠΈΠΌΠΎΡΡ‚ΡŒ с v1 + +# ── ВСрсия ──────────────────────────────────────────────────────────────────── +GOTELEGRAM_VERSION="2.2.1" +GOTELEGRAM_NAME="GoTelegram" + +# ── ΠŸΡƒΡ‚ΠΈ ────────────────────────────────────────────────────────────────────── +GOTELEGRAM_DIR="/opt/gotelegram" +GOTELEGRAM_CONFIG="$GOTELEGRAM_DIR/config.json" +TELEMT_CONFIG="/etc/telemt/config.toml" +TELEMT_BIN="/usr/local/bin/telemt" +TELEMT_SERVICE="telemt" +NGINX_SITE_CONF="/etc/nginx/sites-available/gotelegram" +NGINX_SITE_LINK="/etc/nginx/sites-enabled/gotelegram" +WEBSITE_ROOT="/var/www/gotelegram-site" +BACKUP_DIR="$GOTELEGRAM_DIR/backups" +LOG_FILE="/var/log/gotelegram.log" +BOT_DIR="/opt/gotelegram-bot" + +# ── V1 ΡΠΎΠ²ΠΌΠ΅ΡΡ‚ΠΈΠΌΠΎΡΡ‚ΡŒ ───────────────────────────────────────────────────────── +V1_CONTAINER_NAME="mtproto-proxy" +V1_CONFIG_FILE="/opt/gotelegram-bot/proxy.json" +V1_SERVICE_NAME="gotelegram-bot" + +# ── Π¦Π²Π΅Ρ‚Π° ──────────────────────────────────────────────────────────────────── +RED='\033[0;31m' +GREEN='\033[0;32m' +CYAN='\033[0;36m' +YELLOW='\033[1;33m' +MAGENTA='\033[0;35m' +BLUE='\033[0;34m' +WHITE='\033[1;37m' +BOLD='\033[1m' +DIM='\033[2m' +NC='\033[0m' + +# ── Π›ΠΎΠ³ΠΈΡ€ΠΎΠ²Π°Π½ΠΈΠ΅ ────────────────────────────────────────────────────────────── +log_info() { echo -e " ${CYAN}β„Ή${NC} $*"; } +log_success() { echo -e " ${GREEN}βœ“${NC} $*"; } +log_warning() { echo -e " ${YELLOW}⚠${NC} $*"; } +log_error() { echo -e " ${RED}βœ—${NC} $*"; } +log_step() { echo -e "\n${BOLD}${WHITE} $*${NC}"; } +log_dim() { echo -e " ${DIM}$*${NC}"; } + +log_to_file() { + local ts; ts=$(date '+%Y-%m-%d %H:%M:%S') + echo "[$ts] $*" >> "$LOG_FILE" 2>/dev/null +} + +# ── Π‘ΠΏΠΈΠ½Π½Π΅Ρ€ ────────────────────────────────────────────────────────────────── +_spin_pid="" +spinner_start() { + local msg="${1:-ΠŸΠΎΠ΄ΠΎΠΆΠ΄ΠΈΡ‚Π΅...}" + ( + local frames=('β ‹' 'β ™' 'β Ή' 'β Έ' 'β Ό' 'β ΄' 'β ¦' 'β §' 'β ‡' '⠏') + local i=0 + while true; do + printf "\r ${CYAN}${frames[$i]}${NC} ${msg}" >&2 + i=$(( (i+1) % ${#frames[@]} )) + sleep 0.1 + done + ) & + _spin_pid=$! +} + +spinner_stop() { + [ -n "$_spin_pid" ] && kill "$_spin_pid" 2>/dev/null && wait "$_spin_pid" 2>/dev/null + _spin_pid="" + printf "\r\033[K" >&2 +} + +# ── ΠŸΡ€ΠΎΠ³Ρ€Π΅ΡΡ-Π±Π°Ρ€ ───────────────────────────────────────────────────────────── +progress_bar() { + local current="$1" total="$2" label="${3:-}" + local pct=$(( current * 100 / total )) + local filled=$(( pct / 2 )) + local empty=$(( 50 - filled )) + local bar="" + for ((i=0; i&2 + [ "$current" -eq "$total" ] && echo "" >&2 +} + +# ── Π’Ρ‹ΠΏΠΎΠ»Π½Π΅Π½ΠΈΠ΅ с ΠΈΠ½Π΄ΠΈΠΊΠ°Ρ‚ΠΎΡ€ΠΎΠΌ ───────────────────────────────────────────────── +run_with_spinner() { + local label="$1"; shift + local err_file="/tmp/.gotelegram_spinner_err_$$" + spinner_start "$label" + "$@" >/dev/null 2>"$err_file" + local rc=$? + spinner_stop + if [ $rc -eq 0 ]; then + log_success "$label" + else + log_error "$label ${RED}(ошибка, ΠΊΠΎΠ΄: $rc)${NC}" + if [ -s "$err_file" ]; then + log_dim " $(head -3 "$err_file")" + fi + fi + rm -f "$err_file" + return $rc +} + +# ── Π‘Π°Π½Π½Π΅Ρ€ ─────────────────────────────────────────────────────────────────── +show_banner() { + echo "" + echo -e "${CYAN}╔══════════════════════════════════════════════════════════╗${NC}" + echo -e "${CYAN}β•‘${NC} ${BOLD}${WHITE}πŸš€ GoTelegram v${GOTELEGRAM_VERSION}${NC} ${CYAN}β•‘${NC}" + echo -e "${CYAN}β•‘${NC} ${DIM}MTProxy Π½Π° ядрС telemt (Rust + Tokio)${NC} ${CYAN}β•‘${NC}" + echo -e "${CYAN}β•‘${NC} ${DIM}Anti-DPI β€’ Fake TLS β€’ TCP Splice β€’ JA3/JA4${NC} ${CYAN}β•‘${NC}" + echo -e "${CYAN}β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•${NC}" + echo "" +} + +# ── Благодарности ──────────────────────────────────────────────────────────── +show_credits() { + echo "" + echo -e "${MAGENTA}╔══════════════════════════════════════════════════════════╗${NC}" + echo -e "${MAGENTA}β•‘${NC} ${BOLD}Благодарности / Credits${NC} ${MAGENTA}β•‘${NC}" + echo -e "${MAGENTA}β•Ÿβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β•’${NC}" + echo -e "${MAGENTA}β•‘${NC} ${WHITE}telemt${NC} β€” MTProxy engine (Rust) ${MAGENTA}β•‘${NC}" + echo -e "${MAGENTA}β•‘${NC} ${DIM}github.com/telemt/telemt${NC} ${MAGENTA}β•‘${NC}" + echo -e "${MAGENTA}β•‘${NC} ${MAGENTA}β•‘${NC}" + echo -e "${MAGENTA}β•‘${NC} ${WHITE}HTML5 UP${NC} β€” Π°Π΄Π°ΠΏΡ‚ΠΈΠ²Π½Ρ‹Π΅ HTML/CSS ΡˆΠ°Π±Π»ΠΎΠ½Ρ‹ ${MAGENTA}β•‘${NC}" + echo -e "${MAGENTA}β•‘${NC} ${DIM}html5up.net β€’ CC BY 3.0 β€’ @ajlkn${NC} ${MAGENTA}β•‘${NC}" + echo -e "${MAGENTA}β•‘${NC} ${MAGENTA}β•‘${NC}" + echo -e "${MAGENTA}β•‘${NC} ${WHITE}learning-zone${NC} β€” 150+ HTML5 шаблонов ${MAGENTA}β•‘${NC}" + echo -e "${MAGENTA}β•‘${NC} ${DIM}github.com/learning-zone/website-templates${NC} ${MAGENTA}β•‘${NC}" + echo -e "${MAGENTA}β•‘${NC} ${MAGENTA}β•‘${NC}" + echo -e "${MAGENTA}β•‘${NC} ${WHITE}Start Bootstrap${NC} β€” MIT лицСнзия ${MAGENTA}β•‘${NC}" + echo -e "${MAGENTA}β•‘${NC} ${DIM}startbootstrap.com${NC} ${MAGENTA}β•‘${NC}" + echo -e "${MAGENTA}β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•${NC}" + echo "" +} + +# ── БистСмныС ΡƒΡ‚ΠΈΠ»ΠΈΡ‚Ρ‹ ──────────────────────────────────────────────────────── +_valid_ip() { + # Validate that each octet is 0-255 + local ip="$1" + local IFS='.' + read -ra octets <<< "$ip" + [ ${#octets[@]} -ne 4 ] && return 1 + for octet in "${octets[@]}"; do + [[ "$octet" =~ ^[0-9]+$ ]] || return 1 + [ "$octet" -gt 255 ] && return 1 + done + return 0 +} + +get_server_ip() { + local ip raw + for url in "https://api.ipify.org" "https://icanhazip.com" "https://ifconfig.me"; do + raw=$(curl -s -4 --max-time 5 "$url" 2>/dev/null) + ip=$(echo "$raw" | grep -Eo '([0-9]{1,3}\.){3}[0-9]{1,3}' | head -1) + if [ -n "$ip" ] && _valid_ip "$ip"; then + echo "$ip" + return 0 + fi + done + echo "0.0.0.0" + return 1 +} + +check_root() { + if [ "$EUID" -ne 0 ]; then + log_error "ЗапуститС скрипт с sudo / ΠΎΡ‚ root" + exit 1 + fi +} + +check_os() { + if [ ! -f /etc/os-release ]; then + log_error "НС ΡƒΠ΄Π°Π»ΠΎΡΡŒ ΠΎΠΏΡ€Π΅Π΄Π΅Π»ΠΈΡ‚ΡŒ ОБ. ВрСбуСтся Linux." + return 1 + fi + # Validate os-release before sourcing (reject command injection: ;, backticks, $()) + if grep -qE '(;|`|\$\(|\$\{)' /etc/os-release 2>/dev/null; then + log_warning "/etc/os-release содСрТит ΠΏΠΎΠ΄ΠΎΠ·Ρ€ΠΈΡ‚Π΅Π»ΡŒΠ½Ρ‹Π΅ строки, пропускаСм" + return 0 + fi + . /etc/os-release + case "$ID" in + ubuntu|debian|centos|rocky|almalinux|fedora|rhel) + log_dim "ОБ: $PRETTY_NAME" + return 0 + ;; + *) + log_warning "ОБ $ID ΠΌΠΎΠΆΠ΅Ρ‚ Π±Ρ‹Ρ‚ΡŒ нСсовмСстима. ΠŸΠΎΠ΄Π΄Π΅Ρ€ΠΆΠΈΠ²Π°ΡŽΡ‚ΡΡ: Ubuntu, Debian, CentOS, Rocky." + return 0 + ;; + esac +} + +get_arch() { + local arch + arch=$(uname -m) + case "$arch" in + x86_64|amd64) echo "amd64" ;; + aarch64|arm64) echo "arm64" ;; + armv7*|armhf) echo "armv7" ;; + *) echo "$arch" ;; + esac +} + +get_pkg_manager() { + if command -v apt-get &>/dev/null; then echo "apt" + elif command -v dnf &>/dev/null; then echo "dnf" + elif command -v yum &>/dev/null; then echo "yum" + else echo "unknown" + fi +} + +install_pkg() { + local pkg="$1" + case "$(get_pkg_manager)" in + apt) apt-get install -y -qq "$pkg" ;; + dnf) dnf install -y -q "$pkg" ;; + yum) yum install -y -q "$pkg" ;; + *) log_error "НСизвСстный ΠΏΠ°ΠΊΠ΅Ρ‚Π½Ρ‹ΠΉ ΠΌΠ΅Π½Π΅Π΄ΠΆΠ΅Ρ€"; return 1 ;; + esac +} + +ensure_deps() { + local missing=() + for cmd in curl jq openssl git; do + if ! command -v "$cmd" &>/dev/null; then + missing+=("$cmd") + fi + done + if [ ${#missing[@]} -gt 0 ]; then + log_step "Установка зависимостСй: ${missing[*]}" + case "$(get_pkg_manager)" in + apt) apt-get update -qq && apt-get install -y -qq "${missing[@]}" ;; + dnf) dnf install -y -q "${missing[@]}" ;; + yum) yum install -y -q "${missing[@]}" ;; + esac + fi +} + +check_port() { + local port="$1" + local line + line=$(ss -tlnp 2>/dev/null | grep -E ":${port}\b" | head -1) + [ -z "$line" ] && line=$(netstat -tlnp 2>/dev/null | grep -E ":${port}\b" | head -1) + if [ -n "$line" ]; then + echo "$line" + return 0 # ΠΏΠΎΡ€Ρ‚ занят + fi + return 1 # свободСн +} + +check_disk_space() { + local min_mb="${1:-500}" + local avail_mb + avail_mb=$(df -m / | awk 'NR==2 {print $4}') + if [ "$avail_mb" -lt "$min_mb" ]; then + log_error "Мало мСста Π½Π° дискС: ${avail_mb}MB (Π½ΡƒΠΆΠ½ΠΎ ${min_mb}MB+)" + return 1 + fi + return 0 +} + +# ── ΠšΠΎΠ½Ρ„ΠΈΠ³ΡƒΡ€Π°Ρ†ΠΈΡ GoTelegram (JSON) ────────────────────────────────────────── +save_gotelegram_config() { + mkdir -p "$(dirname "$GOTELEGRAM_CONFIG")" + cat > "$GOTELEGRAM_CONFIG" << EOJSON +{ + "version": "$GOTELEGRAM_VERSION", + "engine": "${1:-telemt}", + "mode": "${2:-quick}", + "port": ${3:-443}, + "secret": "${4:-}", + "mask_host": "${5:-google.com}", + "domain": "${6:-}", + "template_id": "${7:-}", + "installed_at": "$(date -Iseconds)", + "updated_at": "$(date -Iseconds)" +} +EOJSON + chmod 600 "$GOTELEGRAM_CONFIG" +} + +load_gotelegram_config() { + if [ -f "$GOTELEGRAM_CONFIG" ]; then + cat "$GOTELEGRAM_CONFIG" + return 0 + fi + echo "{}" + return 1 +} + +config_get() { + local key="$1" + if [ ! -f "$GOTELEGRAM_CONFIG" ]; then + log_dim "ΠšΠΎΠ½Ρ„ΠΈΠ³ Π½Π΅ Π½Π°ΠΉΠ΄Π΅Π½: $GOTELEGRAM_CONFIG" >&2 + return 2 # file missing + fi + local val + val=$(jq -r ".$key // empty" "$GOTELEGRAM_CONFIG" 2>/dev/null) + if [ $? -ne 0 ]; then + log_dim "Ошибка чтСния JSON: $GOTELEGRAM_CONFIG" >&2 + return 3 # invalid JSON + fi + if [ -z "$val" ]; then + return 1 # key missing or empty + fi + echo "$val" + return 0 +} + +# ── V1 ΡΠΎΠ²ΠΌΠ΅ΡΡ‚ΠΈΠΌΠΎΡΡ‚ΡŒ ───────────────────────────────────────────────────────── +detect_v1_installation() { + # ΠŸΡ€ΠΎΠ²Π΅Ρ€ΡΠ΅ΠΌ Π½Π°Π»ΠΈΡ‡ΠΈΠ΅ mtg Docker ΠΊΠΎΠ½Ρ‚Π΅ΠΉΠ½Π΅Ρ€Π° (v1) + if command -v docker &>/dev/null; then + if docker ps -a --format '{{.Names}}' 2>/dev/null | grep -q "^${V1_CONTAINER_NAME}$"; then + return 0 # v1 ΠΎΠ±Π½Π°Ρ€ΡƒΠΆΠ΅Π½Π° + fi + fi + # ΠŸΡ€ΠΎΠ²Π΅Ρ€ΡΠ΅ΠΌ Π½Π°Π»ΠΈΡ‡ΠΈΠ΅ ΠΊΠΎΠ½Ρ„ΠΈΠ³Π° v1 + if [ -f "$V1_CONFIG_FILE" ]; then + return 0 + fi + return 1 +} + +get_v1_config() { + # ИзвлСкаСм Π΄Π°Π½Π½Ρ‹Π΅ ΠΈΠ· Ρ€Π°Π±ΠΎΡ‚Π°ΡŽΡ‰Π΅Π³ΠΎ v1 ΠΊΠΎΠ½Ρ‚Π΅ΠΉΠ½Π΅Ρ€Π° + if ! command -v docker &>/dev/null; then + echo "{}" + return 1 + fi + + local running + running=$(docker ps --format '{{.Names}}' 2>/dev/null | grep "^${V1_CONTAINER_NAME}$") + + if [ -z "$running" ]; then + # ΠŸΡ€ΠΎΠ±ΡƒΠ΅ΠΌ ΠΈΠ· сохранённого ΠΊΠΎΠ½Ρ„ΠΈΠ³Π° + if [ -f "$V1_CONFIG_FILE" ]; then + cat "$V1_CONFIG_FILE" + return 0 + fi + echo "{}" + return 1 + fi + + # Достаём ΠΈΠ· Docker + local cmd_str port secret ip + cmd_str=$(docker inspect "$V1_CONTAINER_NAME" --format='{{range .Config.Cmd}}{{.}} {{end}}' 2>/dev/null) + secret=$(echo "$cmd_str" | awk '{print $NF}') + port=$(docker inspect "$V1_CONTAINER_NAME" --format='{{range $p,$c := .HostConfig.PortBindings}}{{(index $c 0).HostPort}}{{end}}' 2>/dev/null) + ip=$(get_server_ip) + + jq -n \ + --arg secret "$secret" \ + --arg port "${port:-443}" \ + --arg ip "$ip" \ + '{secret: $secret, port: ($port | tonumber), ip: $ip, engine: "mtg"}' +} + +migrate_v1_to_v2() { + log_step "ΠœΠΈΠ³Ρ€Π°Ρ†ΠΈΡ с v1 (mtg) Π½Π° v2 (telemt)" + + local v1_config + v1_config=$(get_v1_config) + + local old_port old_secret + old_port=$(echo "$v1_config" | jq -r '.port // 443') + old_secret=$(echo "$v1_config" | jq -r '.secret // empty') + + if [ -z "$old_secret" ]; then + log_warning "НС ΡƒΠ΄Π°Π»ΠΎΡΡŒ ΠΈΠ·Π²Π»Π΅Ρ‡ΡŒ secret ΠΈΠ· v1. Π‘ΡƒΠ΄Π΅Ρ‚ создан Π½ΠΎΠ²Ρ‹ΠΉ." + return 1 + fi + + echo "" + echo -e " ${WHITE}НайдСна установка v1 (mtg):${NC}" + echo -e " ΠŸΠΎΡ€Ρ‚: ${CYAN}${old_port}${NC}" + echo -e " Secret: ${CYAN}${old_secret:0:16}...${NC}" + echo "" + echo -e " ${YELLOW}Π’Π½ΠΈΠΌΠ°Π½ΠΈΠ΅:${NC} сСкрСт mtg НЕ совмСстим с telemt Π½Π°ΠΏΡ€ΡΠΌΡƒΡŽ." + echo -e " ΠšΠ»ΠΈΠ΅Π½Ρ‚Π°ΠΌ потрСбуСтся новая ссылка." + echo "" + echo -ne " ΠžΡΡ‚Π°Π½ΠΎΠ²ΠΈΡ‚ΡŒ v1 ΠΊΠΎΠ½Ρ‚Π΅ΠΉΠ½Π΅Ρ€ ΠΈ ΠΏΠ΅Ρ€Π΅ΠΉΡ‚ΠΈ Π½Π° v2? [Y/n]: " + read -r ans + if [[ "$ans" =~ ^[Nn] ]]; then + log_info "ΠœΠΈΠ³Ρ€Π°Ρ†ΠΈΡ ΠΎΡ‚ΠΌΠ΅Π½Π΅Π½Π°. v1 оставлСн Π±Π΅Π· ΠΈΠ·ΠΌΠ΅Π½Π΅Π½ΠΈΠΉ." + return 1 + fi + + # ΠžΡΡ‚Π°Π½Π°Π²Π»ΠΈΠ²Π°Π΅ΠΌ v1 + log_info "ΠžΡΡ‚Π°Π½ΠΎΠ²ΠΊΠ° v1 ΠΊΠΎΠ½Ρ‚Π΅ΠΉΠ½Π΅Ρ€Π°..." + docker stop "$V1_CONTAINER_NAME" 2>/dev/null + docker rm "$V1_CONTAINER_NAME" 2>/dev/null + + # Π‘Π΅ΠΊΠ°ΠΏΠΈΠΌ v1 ΠΊΠΎΠ½Ρ„ΠΈΠ³ + if [ -f "$V1_CONFIG_FILE" ]; then + mkdir -p "$GOTELEGRAM_DIR" + cp "$V1_CONFIG_FILE" "$GOTELEGRAM_DIR/v1_backup_proxy.json" 2>/dev/null + log_success "ΠšΠΎΠ½Ρ„ΠΈΠ³ v1 сохранён Π² $GOTELEGRAM_DIR/v1_backup_proxy.json" + fi + + log_success "v1 остановлСн. ΠŸΠΎΡ€Ρ‚ $old_port освобоТдён." + return 0 +} + +# ── ΠŸΠΎΠ΄Ρ‚Π²Π΅Ρ€ΠΆΠ΄Π΅Π½ΠΈΠ΅ ──────────────────────────────────────────────────────────── +confirm() { + local msg="${1:-ΠŸΡ€ΠΎΠ΄ΠΎΠ»ΠΆΠΈΡ‚ΡŒ?}" + echo -ne " ${msg} [Y/n]: " + read -r ans + [[ ! "$ans" =~ ^[Nn] ]] +} + +# ── Π’Ρ‹Π±ΠΎΡ€ ΠΈΠ· списка ────────────────────────────────────────────────────────── +select_option() { + local title="$1" + shift + local options=("$@") + + echo "" + echo -e " ${BOLD}${WHITE}${title}${NC}" + echo -e " ${DIM}$(printf '─%.0s' {1..50})${NC}" + local i=1 + for opt in "${options[@]}"; do + echo -e " ${CYAN}${i})${NC} ${opt}" + ((i++)) + done + echo -e " ${DIM}$(printf '─%.0s' {1..50})${NC}" + echo -ne " ${WHITE}Π’Ρ‹Π±ΠΎΡ€:${NC} " + read -r choice + echo "$choice" +} + +# ── ГСнСрация случайного hex ───────────────────────────────────────────────── +generate_hex() { + local len="${1:-32}" + openssl rand -hex "$((len/2))" 2>/dev/null || head -c "$((len/2))" /dev/urandom | xxd -p | tr -d '\n' +} + +# ── ΠŸΡ€ΠΎΠ²Π΅Ρ€ΠΊΠ° Π΄ΠΎΠΌΠ΅Π½Π° ────────────────────────────────────────────────────────── +validate_domain() { + local domain="$1" + if echo "$domain" | grep -qE '^([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$'; then + return 0 + fi + return 1 +} + +# ── Init: созданиС Π΄ΠΈΡ€Π΅ΠΊΡ‚ΠΎΡ€ΠΈΠΉ ──────────────────────────────────────────────── +init_dirs() { + mkdir -p "$GOTELEGRAM_DIR" "$BACKUP_DIR" /etc/telemt 2>/dev/null + touch "$LOG_FILE" 2>/dev/null +} diff --git a/lib/telemt.sh b/lib/telemt.sh index 7cb1a95..8fcd23a 100644 --- a/lib/telemt.sh +++ b/lib/telemt.sh @@ -1,305 +1,305 @@ -#!/bin/bash -# GoTelegram v2.2 β€” Π£ΠΏΡ€Π°Π²Π»Π΅Π½ΠΈΠ΅ telemt binary -# Π‘ΠΊΠ°Ρ‡ΠΈΠ²Π°Π½ΠΈΠ΅, ΠΎΠ±Π½ΠΎΠ²Π»Π΅Π½ΠΈΠ΅, запуск, остановка Ρ‡Π΅Ρ€Π΅Π· systemd - -TELEMT_GITHUB="telemt/telemt" -TELEMT_RELEASE_API="https://api.github.com/repos/${TELEMT_GITHUB}/releases/latest" -TELEMT_USER="telemt" -TELEMT_GROUP="telemt" - -# ── ΠŸΠΎΠ»ΡƒΡ‡Π΅Π½ΠΈΠ΅ послСднСй вСрсии ─────────────────────────────────────────────── -get_latest_telemt_version() { - local resp - resp=$(curl -s --max-time 10 "$TELEMT_RELEASE_API" 2>/dev/null) - if [ $? -ne 0 ] || [ -z "$resp" ]; then - log_error "НС ΡƒΠ΄Π°Π»ΠΎΡΡŒ ΠΏΠΎΠ»ΡƒΡ‡ΠΈΡ‚ΡŒ ΠΈΠ½Ρ„ΠΎΡ€ΠΌΠ°Ρ†ΠΈΡŽ ΠΎ Ρ€Π΅Π»ΠΈΠ·Π°Ρ… telemt" - return 1 - fi - echo "$resp" | jq -r '.tag_name // empty' -} - -get_telemt_download_url() { - local arch - arch=$(get_arch) - local resp - resp=$(curl -s --max-time 10 "$TELEMT_RELEASE_API" 2>/dev/null) - if [ -z "$resp" ]; then return 1; fi - - local pattern - case "$arch" in - amd64) pattern="linux.*amd64\|linux.*x86_64" ;; - arm64) pattern="linux.*arm64\|linux.*aarch64" ;; - armv7) pattern="linux.*armv7\|linux.*arm" ;; - *) pattern="linux.*${arch}" ;; - esac - - echo "$resp" | jq -r ".assets[].browser_download_url" 2>/dev/null | grep -i "$pattern" | head -1 -} - -# ── УстановлСнная вСрсия ───────────────────────────────────────────────────── -get_installed_telemt_version() { - if [ -x "$TELEMT_BIN" ]; then - "$TELEMT_BIN" --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1 - else - echo "" - fi -} - -is_telemt_installed() { - [ -x "$TELEMT_BIN" ] -} - -# ── Π‘ΠΊΠ°Ρ‡ΠΈΠ²Π°Π½ΠΈΠ΅ ΠΈ установка ─────────────────────────────────────────────────── -download_telemt() { - local url - url=$(get_telemt_download_url) - if [ -z "$url" ]; then - log_error "НС Π½Π°ΠΉΠ΄Π΅Π½ Π±ΠΈΠ½Π°Ρ€Π½ΠΈΠΊ telemt для Π°Ρ€Ρ…ΠΈΡ‚Π΅ΠΊΡ‚ΡƒΡ€Ρ‹ $(get_arch)" - return 1 - fi - - local tmp_file="/tmp/telemt_download_$$" - log_info "Π‘ΠΊΠ°Ρ‡ΠΈΠ²Π°Π½ΠΈΠ΅: $url" - - if ! curl -L -s --max-time 120 -o "$tmp_file" "$url"; then - log_error "Ошибка скачивания telemt" - rm -f "$tmp_file" - return 1 - fi - - # ΠžΠΏΡ€Π΅Π΄Π΅Π»ΡΠ΅ΠΌ Ρ‚ΠΈΠΏ Ρ„Π°ΠΉΠ»Π° ΠΈ распаковываСм - local mime - mime=$(file -b --mime-type "$tmp_file" 2>/dev/null) - - case "$mime" in - application/gzip|application/x-gzip) - tar xzf "$tmp_file" -C /tmp/ 2>/dev/null - local extracted - extracted=$(find /tmp -maxdepth 2 -name "telemt" -type f -newer "$tmp_file" 2>/dev/null | head -1) - if [ -z "$extracted" ]; then - # ΠœΠΎΠΆΠ΅Ρ‚ Π±Ρ‹Ρ‚ΡŒ просто gzip Π±Π΅Π· tar - gunzip -c "$tmp_file" > /tmp/telemt_bin_$$ 2>/dev/null - extracted="/tmp/telemt_bin_$$" - fi - ;; - application/x-tar) - tar xf "$tmp_file" -C /tmp/ 2>/dev/null - extracted=$(find /tmp -maxdepth 2 -name "telemt" -type f -newer "$tmp_file" 2>/dev/null | head -1) - ;; - application/zip) - unzip -o "$tmp_file" -d /tmp/telemt_extract_$$ 2>/dev/null - extracted=$(find /tmp/telemt_extract_$$ -name "telemt" -type f 2>/dev/null | head -1) - ;; - application/octet-stream|application/x-executable) - # Π£ΠΆΠ΅ Π±ΠΈΠ½Π°Ρ€Π½ΠΈΠΊ - extracted="$tmp_file" - ;; - *) - # ΠŸΡ€ΠΎΠ±ΡƒΠ΅ΠΌ ΠΊΠ°ΠΊ Π±ΠΈΠ½Π°Ρ€Π½ΠΈΠΊ - extracted="$tmp_file" - ;; - esac - - if [ -z "$extracted" ] || [ ! -f "$extracted" ]; then - log_error "НС ΡƒΠ΄Π°Π»ΠΎΡΡŒ ΠΈΠ·Π²Π»Π΅Ρ‡ΡŒ Π±ΠΈΠ½Π°Ρ€Π½ΠΈΠΊ telemt" - rm -f "$tmp_file" - return 1 - fi - - # УстанавливаСм - cp "$extracted" "$TELEMT_BIN" - chmod 755 "$TELEMT_BIN" - rm -f "$tmp_file" - rm -rf /tmp/telemt_extract_$$ /tmp/telemt_bin_$$ - - # ΠŸΡ€ΠΎΠ²Π΅Ρ€ΡΠ΅ΠΌ - if "$TELEMT_BIN" --version &>/dev/null; then - log_success "telemt $(get_installed_telemt_version) установлСн Π² $TELEMT_BIN" - return 0 - else - log_error "Π‘ΠΈΠ½Π°Ρ€Π½ΠΈΠΊ telemt Π½Π΅ запускаСтся" - return 1 - fi -} - -# ── БистСмный ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»ΡŒ ─────────────────────────────────────────────────── -create_telemt_user() { - if ! id "$TELEMT_USER" &>/dev/null; then - useradd -r -s /usr/sbin/nologin -d /etc/telemt "$TELEMT_USER" 2>/dev/null - log_dim "Π‘ΠΎΠ·Π΄Π°Π½ систСмный ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»ΡŒ: $TELEMT_USER" - fi -} - -# ── Systemd сСрвис ─────────────────────────────────────────────────────────── -install_telemt_service() { - local config_path="${1:-$TELEMT_CONFIG}" - - cat > "/etc/systemd/system/${TELEMT_SERVICE}.service" << EOF -[Unit] -Description=GoTelegram MTProxy (telemt engine) -Documentation=https://github.com/telemt/telemt -After=network-online.target -Wants=network-online.target - -[Service] -Type=simple -User=root -ExecStart=$TELEMT_BIN run $config_path -Restart=always -RestartSec=5 -LimitNOFILE=65535 - -# Π‘Π΅Π·ΠΎΠΏΠ°ΡΠ½ΠΎΡΡ‚ΡŒ -NoNewPrivileges=true -ProtectSystem=strict -ProtectHome=true -ReadWritePaths=/etc/telemt /var/log -PrivateTmp=true - -[Install] -WantedBy=multi-user.target -EOF - - systemctl daemon-reload - log_success "Systemd сСрвис $TELEMT_SERVICE создан" -} - -# ── Π£ΠΏΡ€Π°Π²Π»Π΅Π½ΠΈΠ΅ сСрвисом ────────────────────────────────────────────────────── -start_telemt() { - systemctl start "$TELEMT_SERVICE" 2>/dev/null - sleep 2 - if systemctl is-active --quiet "$TELEMT_SERVICE"; then - log_success "telemt Π·Π°ΠΏΡƒΡ‰Π΅Π½" - return 0 - else - log_error "telemt Π½Π΅ запустился" - journalctl -u "$TELEMT_SERVICE" --no-pager -n 10 2>/dev/null - return 1 - fi -} - -stop_telemt() { - if systemctl is-active --quiet "$TELEMT_SERVICE" 2>/dev/null; then - systemctl stop "$TELEMT_SERVICE" - log_success "telemt остановлСн" - else - log_dim "telemt ΡƒΠΆΠ΅ остановлСн" - fi -} - -restart_telemt() { - systemctl restart "$TELEMT_SERVICE" 2>/dev/null - sleep 2 - if systemctl is-active --quiet "$TELEMT_SERVICE"; then - log_success "telemt ΠΏΠ΅Ρ€Π΅Π·Π°ΠΏΡƒΡ‰Π΅Π½" - return 0 - else - log_error "telemt Π½Π΅ пСрСзапустился" - return 1 - fi -} - -enable_telemt() { - systemctl enable "$TELEMT_SERVICE" 2>/dev/null -} - -telemt_status() { - if ! is_telemt_installed; then - echo "not_installed" - return - fi - if systemctl is-active --quiet "$TELEMT_SERVICE" 2>/dev/null; then - echo "running" - elif systemctl is-enabled --quiet "$TELEMT_SERVICE" 2>/dev/null; then - echo "stopped" - else - echo "disabled" - fi -} - -telemt_logs() { - local lines="${1:-40}" - journalctl -u "$TELEMT_SERVICE" --no-pager -n "$lines" 2>/dev/null -} - -telemt_uptime() { - local started - started=$(systemctl show "$TELEMT_SERVICE" --property=ActiveEnterTimestamp --value 2>/dev/null) - if [ -n "$started" ] && [ "$started" != "" ]; then - echo "$started" - else - echo "N/A" - fi -} - -# ── ОбновлСниС ─────────────────────────────────────────────────────────────── -check_telemt_update() { - local current latest - current=$(get_installed_telemt_version) - latest=$(get_latest_telemt_version) - - if [ -z "$current" ] || [ -z "$latest" ]; then - return 1 - fi - - if [ "$current" != "$latest" ]; then - echo "$latest" - return 0 # Π΅ΡΡ‚ΡŒ ΠΎΠ±Π½ΠΎΠ²Π»Π΅Π½ΠΈΠ΅ - fi - return 1 # Π°ΠΊΡ‚ΡƒΠ°Π»ΡŒΠ½ΠΎ -} - -update_telemt() { - local latest - latest=$(check_telemt_update) - if [ $? -ne 0 ]; then - log_info "telemt ΡƒΠΆΠ΅ послСднСй вСрсии ($(get_installed_telemt_version))" - return 0 - fi - - log_info "Доступно ΠΎΠ±Π½ΠΎΠ²Π»Π΅Π½ΠΈΠ΅: $(get_installed_telemt_version) β†’ $latest" - if ! confirm "ΠžΠ±Π½ΠΎΠ²ΠΈΡ‚ΡŒ telemt?"; then - return 0 - fi - - stop_telemt - if download_telemt; then - start_telemt - log_success "telemt ΠΎΠ±Π½ΠΎΠ²Π»Ρ‘Π½ Π΄ΠΎ $latest" - else - start_telemt # запускаСм ΡΡ‚Π°Ρ€ΡƒΡŽ Π²Π΅Ρ€ΡΠΈΡŽ ΠΎΠ±Ρ€Π°Ρ‚Π½ΠΎ - log_error "ОбновлСниС Π½Π΅ ΡƒΠ΄Π°Π»ΠΎΡΡŒ" - return 1 - fi -} - -# ── Полная установка telemt ────────────────────────────────────────────────── -install_telemt_full() { - log_step "Установка telemt" - - # Π‘ΠΎΠ·Π΄Π°Ρ‘ΠΌ Π΄ΠΈΡ€Π΅ΠΊΡ‚ΠΎΡ€ΠΈΠΈ - mkdir -p /etc/telemt - - # Π‘ΠΊΠ°Ρ‡ΠΈΠ²Π°Π΅ΠΌ Π±ΠΈΠ½Π°Ρ€Π½ΠΈΠΊ - run_with_spinner "Π‘ΠΊΠ°Ρ‡ΠΈΠ²Π°Π½ΠΈΠ΅ telemt" download_telemt || return 1 - - # УстанавливаСм systemd сСрвис - install_telemt_service - - # Π’ΠΊΠ»ΡŽΡ‡Π°Π΅ΠΌ автозапуск - enable_telemt - - log_success "telemt Π³ΠΎΡ‚ΠΎΠ² ΠΊ Ρ€Π°Π±ΠΎΡ‚Π΅" - return 0 -} - -# ── Π£Π΄Π°Π»Π΅Π½ΠΈΠ΅ telemt ────────────────────────────────────────────────────────── -remove_telemt() { - stop_telemt - systemctl disable "$TELEMT_SERVICE" 2>/dev/null - rm -f "/etc/systemd/system/${TELEMT_SERVICE}.service" - systemctl daemon-reload - rm -f "$TELEMT_BIN" - rm -rf /etc/telemt - log_success "telemt ΠΏΠΎΠ»Π½ΠΎΡΡ‚ΡŒΡŽ ΡƒΠ΄Π°Π»Ρ‘Π½" -} +#!/bin/bash +# GoTelegram v2.2 β€” Π£ΠΏΡ€Π°Π²Π»Π΅Π½ΠΈΠ΅ telemt binary +# Π‘ΠΊΠ°Ρ‡ΠΈΠ²Π°Π½ΠΈΠ΅, ΠΎΠ±Π½ΠΎΠ²Π»Π΅Π½ΠΈΠ΅, запуск, остановка Ρ‡Π΅Ρ€Π΅Π· systemd + +TELEMT_GITHUB="telemt/telemt" +TELEMT_RELEASE_API="https://api.github.com/repos/${TELEMT_GITHUB}/releases/latest" +TELEMT_USER="telemt" +TELEMT_GROUP="telemt" + +# ── ΠŸΠΎΠ»ΡƒΡ‡Π΅Π½ΠΈΠ΅ послСднСй вСрсии ─────────────────────────────────────────────── +get_latest_telemt_version() { + local resp + resp=$(curl -s --max-time 10 "$TELEMT_RELEASE_API" 2>/dev/null) + if [ $? -ne 0 ] || [ -z "$resp" ]; then + log_error "НС ΡƒΠ΄Π°Π»ΠΎΡΡŒ ΠΏΠΎΠ»ΡƒΡ‡ΠΈΡ‚ΡŒ ΠΈΠ½Ρ„ΠΎΡ€ΠΌΠ°Ρ†ΠΈΡŽ ΠΎ Ρ€Π΅Π»ΠΈΠ·Π°Ρ… telemt" + return 1 + fi + echo "$resp" | jq -r '.tag_name // empty' +} + +get_telemt_download_url() { + local arch + arch=$(get_arch) + local resp + resp=$(curl -s --max-time 10 "$TELEMT_RELEASE_API" 2>/dev/null) + if [ -z "$resp" ]; then return 1; fi + + local pattern + case "$arch" in + amd64) pattern="linux.*(amd64|x86_64)" ;; + arm64) pattern="linux.*(arm64|aarch64)" ;; + armv7) pattern="linux.*(armv7|arm)" ;; + *) pattern="linux.*${arch}" ;; + esac + + echo "$resp" | jq -r ".assets[].browser_download_url" 2>/dev/null | grep -iE "$pattern" | head -1 +} + +# ── УстановлСнная вСрсия ───────────────────────────────────────────────────── +get_installed_telemt_version() { + if [ -x "$TELEMT_BIN" ]; then + "$TELEMT_BIN" --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1 + else + echo "" + fi +} + +is_telemt_installed() { + [ -x "$TELEMT_BIN" ] +} + +# ── Π‘ΠΊΠ°Ρ‡ΠΈΠ²Π°Π½ΠΈΠ΅ ΠΈ установка ─────────────────────────────────────────────────── +download_telemt() { + local url + url=$(get_telemt_download_url) + if [ -z "$url" ]; then + log_error "НС Π½Π°ΠΉΠ΄Π΅Π½ Π±ΠΈΠ½Π°Ρ€Π½ΠΈΠΊ telemt для Π°Ρ€Ρ…ΠΈΡ‚Π΅ΠΊΡ‚ΡƒΡ€Ρ‹ $(get_arch)" + return 1 + fi + + local tmp_file="/tmp/telemt_download_$$" + log_info "Π‘ΠΊΠ°Ρ‡ΠΈΠ²Π°Π½ΠΈΠ΅: $url" + + if ! curl -L -s --max-time 120 -o "$tmp_file" "$url"; then + log_error "Ошибка скачивания telemt" + rm -f "$tmp_file" + return 1 + fi + + # ΠžΠΏΡ€Π΅Π΄Π΅Π»ΡΠ΅ΠΌ Ρ‚ΠΈΠΏ Ρ„Π°ΠΉΠ»Π° ΠΈ распаковываСм + local mime + mime=$(file -b --mime-type "$tmp_file" 2>/dev/null) + + case "$mime" in + application/gzip|application/x-gzip) + tar xzf "$tmp_file" -C /tmp/ 2>/dev/null + local extracted + extracted=$(find /tmp -maxdepth 2 -name "telemt" -type f -newer "$tmp_file" 2>/dev/null | head -1) + if [ -z "$extracted" ]; then + # ΠœΠΎΠΆΠ΅Ρ‚ Π±Ρ‹Ρ‚ΡŒ просто gzip Π±Π΅Π· tar + gunzip -c "$tmp_file" > /tmp/telemt_bin_$$ 2>/dev/null + extracted="/tmp/telemt_bin_$$" + fi + ;; + application/x-tar) + tar xf "$tmp_file" -C /tmp/ 2>/dev/null + extracted=$(find /tmp -maxdepth 2 -name "telemt" -type f -newer "$tmp_file" 2>/dev/null | head -1) + ;; + application/zip) + unzip -o "$tmp_file" -d /tmp/telemt_extract_$$ 2>/dev/null + extracted=$(find /tmp/telemt_extract_$$ -name "telemt" -type f 2>/dev/null | head -1) + ;; + application/octet-stream|application/x-executable) + # Π£ΠΆΠ΅ Π±ΠΈΠ½Π°Ρ€Π½ΠΈΠΊ + extracted="$tmp_file" + ;; + *) + # ΠŸΡ€ΠΎΠ±ΡƒΠ΅ΠΌ ΠΊΠ°ΠΊ Π±ΠΈΠ½Π°Ρ€Π½ΠΈΠΊ + extracted="$tmp_file" + ;; + esac + + if [ -z "$extracted" ] || [ ! -f "$extracted" ]; then + log_error "НС ΡƒΠ΄Π°Π»ΠΎΡΡŒ ΠΈΠ·Π²Π»Π΅Ρ‡ΡŒ Π±ΠΈΠ½Π°Ρ€Π½ΠΈΠΊ telemt" + rm -f "$tmp_file" + return 1 + fi + + # УстанавливаСм + cp "$extracted" "$TELEMT_BIN" + chmod 755 "$TELEMT_BIN" + rm -f "$tmp_file" + rm -rf /tmp/telemt_extract_$$ /tmp/telemt_bin_$$ + + # ΠŸΡ€ΠΎΠ²Π΅Ρ€ΡΠ΅ΠΌ + if "$TELEMT_BIN" --version &>/dev/null; then + log_success "telemt $(get_installed_telemt_version) установлСн Π² $TELEMT_BIN" + return 0 + else + log_error "Π‘ΠΈΠ½Π°Ρ€Π½ΠΈΠΊ telemt Π½Π΅ запускаСтся" + return 1 + fi +} + +# ── БистСмный ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»ΡŒ ─────────────────────────────────────────────────── +create_telemt_user() { + if ! id "$TELEMT_USER" &>/dev/null; then + useradd -r -s /usr/sbin/nologin -d /etc/telemt "$TELEMT_USER" 2>/dev/null + log_dim "Π‘ΠΎΠ·Π΄Π°Π½ систСмный ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»ΡŒ: $TELEMT_USER" + fi +} + +# ── Systemd сСрвис ─────────────────────────────────────────────────────────── +install_telemt_service() { + local config_path="${1:-$TELEMT_CONFIG}" + + cat > "/etc/systemd/system/${TELEMT_SERVICE}.service" << EOF +[Unit] +Description=GoTelegram MTProxy (telemt engine) +Documentation=https://github.com/telemt/telemt +After=network-online.target +Wants=network-online.target + +[Service] +Type=simple +User=root +ExecStart=$TELEMT_BIN run $config_path +Restart=always +RestartSec=5 +LimitNOFILE=65535 + +# Π‘Π΅Π·ΠΎΠΏΠ°ΡΠ½ΠΎΡΡ‚ΡŒ +NoNewPrivileges=true +ProtectSystem=strict +ProtectHome=true +ReadWritePaths=/etc/telemt /var/log +PrivateTmp=true + +[Install] +WantedBy=multi-user.target +EOF + + systemctl daemon-reload + log_success "Systemd сСрвис $TELEMT_SERVICE создан" +} + +# ── Π£ΠΏΡ€Π°Π²Π»Π΅Π½ΠΈΠ΅ сСрвисом ────────────────────────────────────────────────────── +start_telemt() { + systemctl start "$TELEMT_SERVICE" 2>/dev/null + sleep 2 + if systemctl is-active --quiet "$TELEMT_SERVICE"; then + log_success "telemt Π·Π°ΠΏΡƒΡ‰Π΅Π½" + return 0 + else + log_error "telemt Π½Π΅ запустился" + journalctl -u "$TELEMT_SERVICE" --no-pager -n 10 2>/dev/null + return 1 + fi +} + +stop_telemt() { + if systemctl is-active --quiet "$TELEMT_SERVICE" 2>/dev/null; then + systemctl stop "$TELEMT_SERVICE" + log_success "telemt остановлСн" + else + log_dim "telemt ΡƒΠΆΠ΅ остановлСн" + fi +} + +restart_telemt() { + systemctl restart "$TELEMT_SERVICE" 2>/dev/null + sleep 2 + if systemctl is-active --quiet "$TELEMT_SERVICE"; then + log_success "telemt ΠΏΠ΅Ρ€Π΅Π·Π°ΠΏΡƒΡ‰Π΅Π½" + return 0 + else + log_error "telemt Π½Π΅ пСрСзапустился" + return 1 + fi +} + +enable_telemt() { + systemctl enable "$TELEMT_SERVICE" 2>/dev/null +} + +telemt_status() { + if ! is_telemt_installed; then + echo "not_installed" + return + fi + if systemctl is-active --quiet "$TELEMT_SERVICE" 2>/dev/null; then + echo "running" + elif systemctl is-enabled --quiet "$TELEMT_SERVICE" 2>/dev/null; then + echo "stopped" + else + echo "disabled" + fi +} + +telemt_logs() { + local lines="${1:-40}" + journalctl -u "$TELEMT_SERVICE" --no-pager -n "$lines" 2>/dev/null +} + +telemt_uptime() { + local started + started=$(systemctl show "$TELEMT_SERVICE" --property=ActiveEnterTimestamp --value 2>/dev/null) + if [ -n "$started" ] && [ "$started" != "" ]; then + echo "$started" + else + echo "N/A" + fi +} + +# ── ОбновлСниС ─────────────────────────────────────────────────────────────── +check_telemt_update() { + local current latest + current=$(get_installed_telemt_version) + latest=$(get_latest_telemt_version) + + if [ -z "$current" ] || [ -z "$latest" ]; then + return 1 + fi + + if [ "$current" != "$latest" ]; then + echo "$latest" + return 0 # Π΅ΡΡ‚ΡŒ ΠΎΠ±Π½ΠΎΠ²Π»Π΅Π½ΠΈΠ΅ + fi + return 1 # Π°ΠΊΡ‚ΡƒΠ°Π»ΡŒΠ½ΠΎ +} + +update_telemt() { + local latest + latest=$(check_telemt_update) + if [ $? -ne 0 ]; then + log_info "telemt ΡƒΠΆΠ΅ послСднСй вСрсии ($(get_installed_telemt_version))" + return 0 + fi + + log_info "Доступно ΠΎΠ±Π½ΠΎΠ²Π»Π΅Π½ΠΈΠ΅: $(get_installed_telemt_version) β†’ $latest" + if ! confirm "ΠžΠ±Π½ΠΎΠ²ΠΈΡ‚ΡŒ telemt?"; then + return 0 + fi + + stop_telemt + if download_telemt; then + start_telemt + log_success "telemt ΠΎΠ±Π½ΠΎΠ²Π»Ρ‘Π½ Π΄ΠΎ $latest" + else + start_telemt # запускаСм ΡΡ‚Π°Ρ€ΡƒΡŽ Π²Π΅Ρ€ΡΠΈΡŽ ΠΎΠ±Ρ€Π°Ρ‚Π½ΠΎ + log_error "ОбновлСниС Π½Π΅ ΡƒΠ΄Π°Π»ΠΎΡΡŒ" + return 1 + fi +} + +# ── Полная установка telemt ────────────────────────────────────────────────── +install_telemt_full() { + log_step "Установка telemt" + + # Π‘ΠΎΠ·Π΄Π°Ρ‘ΠΌ Π΄ΠΈΡ€Π΅ΠΊΡ‚ΠΎΡ€ΠΈΠΈ + mkdir -p /etc/telemt + + # Π‘ΠΊΠ°Ρ‡ΠΈΠ²Π°Π΅ΠΌ Π±ΠΈΠ½Π°Ρ€Π½ΠΈΠΊ + run_with_spinner "Π‘ΠΊΠ°Ρ‡ΠΈΠ²Π°Π½ΠΈΠ΅ telemt" download_telemt || return 1 + + # УстанавливаСм systemd сСрвис + install_telemt_service + + # Π’ΠΊΠ»ΡŽΡ‡Π°Π΅ΠΌ автозапуск + enable_telemt + + log_success "telemt Π³ΠΎΡ‚ΠΎΠ² ΠΊ Ρ€Π°Π±ΠΎΡ‚Π΅" + return 0 +} + +# ── Π£Π΄Π°Π»Π΅Π½ΠΈΠ΅ telemt ────────────────────────────────────────────────────────── +remove_telemt() { + stop_telemt + systemctl disable "$TELEMT_SERVICE" 2>/dev/null + rm -f "/etc/systemd/system/${TELEMT_SERVICE}.service" + systemctl daemon-reload + rm -f "$TELEMT_BIN" + rm -rf /etc/telemt + log_success "telemt ΠΏΠΎΠ»Π½ΠΎΡΡ‚ΡŒΡŽ ΡƒΠ΄Π°Π»Ρ‘Π½" +}