diff --git a/gotelegram-bot/bot.py b/gotelegram-bot/bot.py
index 1d63852..408b64f 100644
--- a/gotelegram-bot/bot.py
+++ b/gotelegram-bot/bot.py
@@ -100,13 +100,14 @@ logger = logging.getLogger(__name__)
# CONFIGURATION
# ============================================================================
-GOTELEGRAM_VERSION = "2.4.0"
+GOTELEGRAM_VERSION = "2.4.2"
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"
+INSTALL_SH = "/opt/gotelegram/install.sh"
PROMO_LINK_1 = "https://vk.cc/ct29NQ"
PROMO_LINK_2 = "https://vk.cc/cUxAhj"
@@ -239,6 +240,58 @@ async def sh(*args, timeout: int = 60) -> Tuple[int, str, str]:
return (-1, "", str(e))
+async def run_bot_action(action: str, timeout: int = 300, **params) -> Dict:
+ """Invoke install.sh --action=X --json and parse the JSON result.
+
+ Args:
+ action: action name (e.g. "change-template", "change-lite-domain")
+ timeout: seconds to wait for completion (long ops: template download can take time)
+ **params: arbitrary key→value pairs, each passed as --key=value
+
+ Returns:
+ dict with at least {"status": "success|error", "message": "..."}.
+ Transport errors are mapped to {"status":"error","message":..., "code":"transport"}
+ """
+ cmd = ["bash", INSTALL_SH, f"--action={action}", "--json"]
+ for k, v in params.items():
+ if v is None:
+ continue
+ cmd.append(f"--{k.replace('_', '-')}={v}")
+
+ code, stdout, stderr = await sh(*cmd, timeout=timeout)
+ stdout = (stdout or "").strip()
+
+ # install.sh may print multiple log lines to stderr; the JSON is on stdout.
+ # Pick the last non-empty line that looks like JSON (robust to any stray output).
+ json_line = None
+ for line in reversed(stdout.splitlines()):
+ line = line.strip()
+ if line.startswith("{") and line.endswith("}"):
+ json_line = line
+ break
+
+ if json_line:
+ try:
+ data = json.loads(json_line)
+ if isinstance(data, dict) and "status" in data:
+ return data
+ except json.JSONDecodeError as e:
+ logger.warning(f"run_bot_action: JSON parse failed: {e} | line={json_line!r}")
+
+ # No JSON from install.sh — synthesize an error result
+ tail = (stderr or "")[-300:] if stderr else ""
+ logger.error(
+ f"run_bot_action({action}): no JSON output, rc={code}, "
+ f"stdout={stdout[-300:]!r}, stderr={tail!r}"
+ )
+ return {
+ "status": "error",
+ "message": "install.sh did not return a JSON result",
+ "code": "transport",
+ "rc": str(code),
+ }
+
+
def load_json(path: str) -> Optional[Dict]:
"""Load JSON file."""
try:
@@ -808,16 +861,13 @@ async def cb_install_mode_lite(update: Update, context: ContextTypes.DEFAULT_TYP
async def cb_lite_domain(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Lite domain selection callback — safe stub.
+ """Lite domain selection callback — real implementation (v2.4.2+).
- Historic bug (v2.4.1): this callback used to overwrite
- /opt/gotelegram/config.json with a minimal fake config (no secret, no
- mask_host, no engine), silently corrupting the live proxy and showing a
- fake "✅ Lite mode installed!" — while never actually invoking install.sh.
-
- Installing/changing modes non-interactively from the bot is not yet wired
- up. Until it is, refuse cleanly and route the user to the CLI instead of
- damaging the working configuration.
+ Branches on current mode:
+ * lite mode (active): invoke `install.sh --action=change-lite-domain`
+ which regenerates the telemt TOML with a new fake-TLS mask domain and
+ restarts the service. Preserves secret/port.
+ * any other mode: route to CLI. Fresh Lite install is interactive.
"""
query = update.callback_query
data = query.data
@@ -830,14 +880,52 @@ async def cb_lite_domain(update: Update, context: ContextTypes.DEFAULT_TYPE) ->
await query.answer()
- text = (
- "⚠️ Установка из бота пока не поддерживается\n\n"
- f"Выбранный домен: {html.escape(domain)}\n\n"
- "Чтобы установить или переключить режим Lite, запустите на сервере:\n"
- "gotelegram\n\n"
- "Затем: 1) Прокси → 1) Установить/Обновить → Lite.\n\n"
- "Существующая конфигурация не была изменена."
+ config = load_json(GOTELEGRAM_CONFIG) or {}
+ current_mode = config.get("mode", "")
+
+ if current_mode != "lite":
+ text = (
+ "⚠️ Установка Lite из бота пока не поддерживается\n\n"
+ f"Выбранный домен: {html.escape(domain)}\n\n"
+ "Чтобы установить Lite, запустите на сервере:\n"
+ "gotelegram → 1) Прокси → 1) Установить/Обновить → Lite\n\n"
+ "Существующая конфигурация не была изменена."
+ )
+ keyboard = InlineKeyboardMarkup(
+ [[InlineKeyboardButton(_t(_uid(update), "btn_back"), callback_data="menu_main")]]
+ )
+ await safe_edit_message(query, text, reply_markup=keyboard, parse_mode="HTML")
+ return
+
+ # Lite active — switch fake-TLS mask domain in place
+ progress_text = (
+ "⏳ Меняю маскировочный домен...\n\n"
+ f"Новый домен: {html.escape(domain)}\n\n"
+ "Перегенерирую конфиг telemt и перезапускаю сервис."
)
+ await safe_edit_message(query, progress_text, parse_mode="HTML")
+
+ result = await run_bot_action("change-lite-domain", timeout=30, domain=domain)
+
+ if result.get("status") == "success":
+ text = (
+ "✅ Маскировочный домен обновлён\n\n"
+ f"Новый домен: {html.escape(domain)}\n\n"
+ "telemt перезапущен. Важно: старые ссылки подключения больше "
+ "не будут работать — нужно заново раздать новые."
+ )
+ else:
+ err_msg = result.get("message", "unknown error")
+ err_code = result.get("code", "")
+ text = (
+ "❌ Не удалось сменить домен\n\n"
+ f"Домен: {html.escape(domain)}\n"
+ f"Причина: {html.escape(err_msg)}"
+ + (f" ({html.escape(err_code)})" if err_code else "")
+ + "\n\n"
+ "Существующая конфигурация не была изменена."
+ )
+
keyboard = InlineKeyboardMarkup(
[[InlineKeyboardButton(_t(_uid(update), "btn_back"), callback_data="menu_main")]]
)
@@ -1111,19 +1199,19 @@ async def cb_pro_template(update: Update, context: ContextTypes.DEFAULT_TYPE) ->
async def cb_pro_confirm(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
- """Confirm Pro template selection — safe stub.
+ """Confirm Pro template selection — real implementation (v2.4.2+).
- Historic bug (v2.4.1): this callback used to overwrite
- /opt/gotelegram/config.json with a fake {"mode":"pro","template":...,
- "port":443} blob — no secret, no domain, no mask_host — silently
- corrupting the live proxy so link generation and status broke. At the
- same time the user saw "✅ Pro mode installed!" although install.sh was
- never invoked, no template was actually downloaded, nginx was not
- reconfigured and certbot was not touched.
+ Branches on current mode:
+ * pro mode (active deployment): invoke `install.sh --action=change-template`
+ which downloads the new template and redeploys it to nginx. Reuses the
+ existing domain + SSL cert.
+ * any other mode (or no install at all): route to CLI. Fresh Pro install
+ still requires interactive flow (domain, email, DNS check) — not safe
+ to run headless from the bot.
- Non-interactive Pro install/change from the bot is not yet implemented.
- Until it is, refuse cleanly and route the user to the CLI instead of
- damaging the working configuration.
+ Historic context: v2.4.1 stub used to overwrite config.json with a fake
+ blob; that was replaced with a safe message in v2.4.1 hotfix; now in
+ v2.4.2 we wire the real change-template path through install.sh.
"""
query = update.callback_query
data = query.data
@@ -1131,19 +1219,63 @@ async def cb_pro_confirm(update: Update, context: ContextTypes.DEFAULT_TYPE) ->
await query.answer()
- text = (
- "⚠️ Установка шаблона из бота пока не поддерживается\n\n"
- f"Выбранный шаблон: {html.escape(tpl_id)}\n\n"
- "Чтобы установить или сменить шаблон, запустите на сервере:\n"
- "gotelegram\n\n"
- "Затем: 1) Прокси → 1) Установить/Обновить → Pro "
- "(или 7) Сменить режим/шаблон для смены текущего).\n\n"
- "Существующая конфигурация не была изменена."
+ # Read current config to decide: in-place change-template vs fresh install
+ config = load_json(GOTELEGRAM_CONFIG) or {}
+ current_mode = config.get("mode", "")
+
+ if current_mode != "pro":
+ # Fresh install / mode switch — still routes to CLI (needs domain, SSL)
+ text = (
+ "⚠️ Установка Pro из бота пока не поддерживается\n\n"
+ f"Выбранный шаблон: {html.escape(tpl_id)}\n\n"
+ "Pro-режим требует ввода домена, email и проверки DNS. "
+ "Чтобы установить Pro, запустите на сервере:\n"
+ "gotelegram → 1) Прокси → 1) Установить/Обновить → Pro\n\n"
+ "Существующая конфигурация не была изменена."
+ )
+ keyboard = InlineKeyboardMarkup(
+ [[InlineKeyboardButton(_t(_uid(update), "btn_back"), callback_data="menu_main")]]
+ )
+ await safe_edit_message(query, text, reply_markup=keyboard, parse_mode="HTML")
+ return
+
+ # Pro mode is active — perform change-template in place
+ progress_text = (
+ "⏳ Меняю шаблон сайта...\n\n"
+ f"Шаблон: {html.escape(tpl_id)}\n\n"
+ "Скачиваю репозиторий и разворачиваю в nginx. "
+ "Это может занять 30–90 секунд."
)
+ await safe_edit_message(query, progress_text, parse_mode="HTML")
+
+ # Template download + git clone can be slow — generous timeout
+ result = await run_bot_action("change-template", timeout=180, template=tpl_id)
+
+ if result.get("status") == "success":
+ domain = result.get("domain", config.get("domain", ""))
+ text = (
+ "✅ Шаблон обновлён\n\n"
+ f"Новый шаблон: {html.escape(tpl_id)}\n"
+ f"Сайт: https://{html.escape(domain)}\n\n"
+ "Прокси продолжает работать без перерыва."
+ )
+ else:
+ err_msg = result.get("message", "unknown error")
+ err_code = result.get("code", "")
+ text = (
+ "❌ Не удалось сменить шаблон\n\n"
+ f"Шаблон: {html.escape(tpl_id)}\n"
+ f"Причина: {html.escape(err_msg)}"
+ + (f" ({html.escape(err_code)})" if err_code else "")
+ + "\n\n"
+ "Существующая конфигурация не была изменена. "
+ "Попробуйте другой шаблон или запустите gotelegram из консоли."
+ )
+
keyboard = InlineKeyboardMarkup(
[[InlineKeyboardButton(_t(_uid(update), "btn_back"), callback_data="menu_main")]]
)
- await safe_edit_message(query, text, reply_markup=keyboard, parse_mode="HTML")
+ await safe_edit_message(query, text, reply_markup=keyboard, parse_mode="HTML", disable_web_page_preview=True)
# ============================================================================
diff --git a/install.sh b/install.sh
index 0bab9c0..a9c5d10 100755
--- a/install.sh
+++ b/install.sh
@@ -1136,11 +1136,257 @@ menu_language() {
esac
}
+# ══════════════════════════════════════════════════════════════════════════════
+# Non-interactive action dispatcher (bot / CI / scripting interface)
+# ══════════════════════════════════════════════════════════════════════════════
+# Usage examples:
+# gotelegram --action=change-template --template=th_ariclaw --json
+# gotelegram --action=change-lite-domain --domain=google.com --json
+#
+# Rules for action handlers:
+# - Only JSON may be written to stdout (the caller parses it).
+# - All human-oriented logging must go to stderr (log_* already do that).
+# - Exit code 0 on success, non-zero on failure (caller still parses JSON).
+# ══════════════════════════════════════════════════════════════════════════════
+
+bot_emit_json() {
+ # bot_emit_json [key=value ...]
+ local status="$1"; shift
+ local message="$1"; shift
+ local extra="" kv k v
+ for kv in "$@"; do
+ k="${kv%%=*}"
+ v="${kv#*=}"
+ # escape backslashes and double quotes in value
+ v="${v//\\/\\\\}"
+ v="${v//\"/\\\"}"
+ extra="${extra},\"${k}\":\"${v}\""
+ done
+ # escape message
+ local msg_esc="${message//\\/\\\\}"
+ msg_esc="${msg_esc//\"/\\\"}"
+ printf '{"status":"%s","message":"%s"%s}\n' "$status" "$msg_esc" "$extra"
+}
+
+# Update a single key in config.json without rewriting the whole file
+bot_update_config_field() {
+ local key="$1"
+ local value="$2"
+ if [ ! -f "$GOTELEGRAM_CONFIG" ]; then
+ return 1
+ fi
+ local tmp
+ tmp=$(mktemp) || return 1
+ if jq --arg k "$key" --arg v "$value" \
+ '.[$k] = $v | .updated_at = (now | todate)' \
+ "$GOTELEGRAM_CONFIG" > "$tmp" 2>/dev/null; then
+ mv "$tmp" "$GOTELEGRAM_CONFIG"
+ chmod 600 "$GOTELEGRAM_CONFIG"
+ return 0
+ fi
+ rm -f "$tmp"
+ return 1
+}
+
+# ── Action: change-template (pro mode only) ──────────────────────────────────
+bot_action_change_template() {
+ local tpl_id="$1"
+ local json_out="${2:-0}"
+
+ if [ -z "$tpl_id" ]; then
+ [ "$json_out" = "1" ] && bot_emit_json "error" "template id is required" "code=missing_arg"
+ log_error "change-template: --template is required"
+ return 2
+ fi
+
+ # Must be in pro mode
+ local mode
+ mode=$(config_get mode 2>/dev/null || echo "")
+ if [ "$mode" != "pro" ]; then
+ [ "$json_out" = "1" ] && bot_emit_json "error" "change-template requires pro mode (current: ${mode:-none})" "code=wrong_mode"
+ log_error "change-template: current mode is '${mode:-none}', requires 'pro'"
+ return 3
+ fi
+
+ # Validate template id exists in catalog
+ if ! get_template_info "$tpl_id" >/dev/null 2>&1; then
+ [ "$json_out" = "1" ] && bot_emit_json "error" "unknown template: $tpl_id" "code=unknown_template"
+ log_error "change-template: template not found in catalog: $tpl_id"
+ return 4
+ fi
+
+ # Make sure git (and other deps) are present. download_template uses git
+ # clone under the hood — on a minimal host (bootstrap-only install) git may
+ # not be installed yet, and the clone would fail silently.
+ ensure_deps >&2
+
+ log_info "change-template: downloading $tpl_id..."
+ local template_dir
+ template_dir=$(download_template "$tpl_id")
+ if [ $? -ne 0 ] || [ -z "$template_dir" ] || [ ! -d "$template_dir" ] || [ ! -f "$template_dir/index.html" ]; then
+ [ "$json_out" = "1" ] && bot_emit_json "error" "download failed for $tpl_id" "code=download_failed"
+ log_error "change-template: download_template failed for $tpl_id"
+ return 5
+ fi
+
+ log_info "change-template: deploying to nginx..."
+ if ! deploy_template_to_nginx "$template_dir" >&2; then
+ [ "$json_out" = "1" ] && bot_emit_json "error" "deploy failed" "code=deploy_failed"
+ return 6
+ fi
+
+ # Reload nginx (no full restart needed for static files — but be safe)
+ systemctl reload nginx 2>/dev/null || systemctl restart nginx 2>/dev/null
+
+ # Update config.json template_id field
+ bot_update_config_field "template_id" "$tpl_id" || \
+ log_warning "change-template: could not update config.json template_id"
+
+ local domain
+ domain=$(config_get domain 2>/dev/null || echo "")
+ log_success "change-template: $tpl_id deployed"
+
+ if [ "$json_out" = "1" ]; then
+ bot_emit_json "success" "template changed to $tpl_id" \
+ "template=$tpl_id" "domain=$domain" "mode=pro"
+ fi
+ return 0
+}
+
+# ── Action: change-lite-domain ───────────────────────────────────────────────
+# Regenerates telemt TOML with a new fake-TLS mask domain. Lite mode only.
+bot_action_change_lite_domain() {
+ local new_domain="$1"
+ local json_out="${2:-0}"
+
+ if [ -z "$new_domain" ]; then
+ [ "$json_out" = "1" ] && bot_emit_json "error" "domain is required" "code=missing_arg"
+ log_error "change-lite-domain: --domain is required"
+ return 2
+ fi
+
+ if ! validate_domain "$new_domain" 2>/dev/null; then
+ [ "$json_out" = "1" ] && bot_emit_json "error" "invalid domain: $new_domain" "code=invalid_domain"
+ log_error "change-lite-domain: invalid domain: $new_domain"
+ return 3
+ fi
+
+ local mode
+ mode=$(config_get mode 2>/dev/null || echo "")
+ if [ "$mode" != "lite" ]; then
+ [ "$json_out" = "1" ] && bot_emit_json "error" "change-lite-domain requires lite mode (current: ${mode:-none})" "code=wrong_mode"
+ log_error "change-lite-domain: current mode is '${mode:-none}', requires 'lite'"
+ return 4
+ fi
+
+ local secret port
+ secret=$(get_config_value secret 2>/dev/null || echo "")
+ port=$(get_config_value port 2>/dev/null || echo "443")
+
+ if [ -z "$secret" ]; then
+ [ "$json_out" = "1" ] && bot_emit_json "error" "no secret in config" "code=no_secret"
+ log_error "change-lite-domain: no secret in config.json"
+ return 5
+ fi
+
+ log_info "change-lite-domain: regenerating telemt TOML..."
+ generate_telemt_toml "$secret" "$port" "lite" "$new_domain" "443" >&2 || {
+ [ "$json_out" = "1" ] && bot_emit_json "error" "config generation failed" "code=gen_failed"
+ return 6
+ }
+
+ validate_telemt_config >&2 || {
+ [ "$json_out" = "1" ] && bot_emit_json "error" "config validation failed" "code=validate_failed"
+ return 7
+ }
+
+ restart_telemt >&2 || {
+ [ "$json_out" = "1" ] && bot_emit_json "error" "telemt restart failed" "code=restart_failed"
+ return 8
+ }
+
+ # Update both domain and mask_host fields in config.json
+ bot_update_config_field "mask_host" "$new_domain" || \
+ log_warning "change-lite-domain: could not update mask_host"
+ bot_update_config_field "domain" "$new_domain" || \
+ log_warning "change-lite-domain: could not update domain"
+
+ log_success "change-lite-domain: switched to $new_domain"
+
+ if [ "$json_out" = "1" ]; then
+ bot_emit_json "success" "lite mask domain changed to $new_domain" \
+ "domain=$new_domain" "mode=lite" "port=$port"
+ fi
+ return 0
+}
+
+# Main dispatcher — called from main() when --action=X is present
+bot_action_dispatch() {
+ local action="" tpl_id="" domain="" json_out=0 arg
+ for arg in "$@"; do
+ case "$arg" in
+ --action=*) action="${arg#--action=}" ;;
+ --template=*) tpl_id="${arg#--template=}" ;;
+ --domain=*) domain="${arg#--domain=}" ;;
+ --json) json_out=1 ;;
+ esac
+ done
+
+ case "$action" in
+ change-template)
+ bot_action_change_template "$tpl_id" "$json_out"
+ return $?
+ ;;
+ change-lite-domain)
+ bot_action_change_lite_domain "$domain" "$json_out"
+ return $?
+ ;;
+ "")
+ log_error "no --action specified"
+ return 64
+ ;;
+ *)
+ [ "$json_out" = "1" ] && bot_emit_json "error" "unknown action: $action" "code=unknown_action"
+ log_error "unknown action: $action"
+ return 64
+ ;;
+ esac
+}
+
# ── Точка входа / Entry point ───────────────────────────────────────────────
main() {
+ # Non-interactive action mode: if --action=X is in args, dispatch and exit.
+ # Must run BEFORE interactive banner/menus so the bot gets clean JSON.
+ local a has_action=0
+ for a in "$@"; do
+ case "$a" in --action=*) has_action=1; break ;; esac
+ done
+ if [ "$has_action" = "1" ]; then
+ check_root
+ init_dirs
+ # Для bot-экшенов тоже нужны зависимости (git для change-template), но
+ # без шумного apt-get update если всё уже на месте.
+ if ! check_deps_present; then
+ ensure_deps >&2 || exit 1
+ fi
+ bot_action_dispatch "$@"
+ exit $?
+ fi
+
check_root
init_dirs
+ # Первый запуск: если критические зависимости отсутствуют — ставим их ДО
+ # того как пользователь дойдёт до меню. На последующих запусках это просто
+ # дёшево проверяет command -v по всем командам и ничего не делает.
+ if ! check_deps_present; then
+ log_step "Первый запуск: проверяю зависимости..."
+ ensure_deps || {
+ log_error "Не удалось установить зависимости. См. сообщения выше."
+ exit 1
+ }
+ fi
+
# First-run language picker (before banner so banner appears in chosen lang)
first_run_language_picker
diff --git a/lib/common.sh b/lib/common.sh
old mode 100644
new mode 100755
index a24e884..4366676
--- a/lib/common.sh
+++ b/lib/common.sh
@@ -3,7 +3,7 @@
# Colors, logging, spinner, system helpers, v1 compat, i18n-aware
# ── Version ───────────────────────────────────────────────────────────────────
-GOTELEGRAM_VERSION="2.4.1"
+GOTELEGRAM_VERSION="2.4.2"
GOTELEGRAM_NAME="GoTelegram"
# ── Пути ──────────────────────────────────────────────────────────────────────
@@ -247,25 +247,149 @@ install_pkg() {
esac
}
+# ── Зависимости GoTelegram ──────────────────────────────────────────────────
+# Полный список внешних команд, которые скрипт использует. Для каждой команды
+# указан пакет на apt и dnf/yum (имена различаются: например dig = dnsutils на
+# Debian, bind-utils на RHEL).
+#
+# КРИТИЧЕСКИЕ (без них скрипт просто не работает):
+# jq — парсинг config.json, templates_catalog.json
+# curl — скачивание telemt и проверки HTTPS
+# openssl — генерация секретов, шифрование бекапов, SSL проверка
+# git — клонирование шаблонов через download_template
+# xxd — hex-encode домена для fake-TLS секрета (ee-prefix)
+# tar — распаковка telemt архива и бекапы
+# dig — DNS-проверка домена в Pro-режиме
+#
+# ЖЕЛАТЕЛЬНЫЕ (есть fallback, но с ними лучше):
+# qrencode — QR-коды для прокси-ссылок
+# bc — красивое форматирование чисел в статистике
+#
+# Pro-режим доустанавливает nginx/certbot через install_nginx/install_certbot
+# (они большие и нужны только если пользователь выбрал Pro).
+
+# Маппинг команды -> (apt_pkg, dnf_pkg). apt_pkg_for_cmd
+apt_pkg_for_cmd() {
+ case "$1" in
+ dig) echo "dnsutils" ;;
+ xxd) echo "xxd" ;; # Ubuntu 22+: отдельный пакет, fallback ниже
+ nslookup) echo "dnsutils" ;;
+ host) echo "dnsutils" ;;
+ ss) echo "iproute2" ;;
+ netstat) echo "net-tools" ;;
+ *) echo "$1" ;; # команда == имя пакета
+ esac
+}
+
+dnf_pkg_for_cmd() {
+ case "$1" in
+ dig|nslookup|host) echo "bind-utils" ;;
+ xxd) echo "vim-common" ;;
+ ss) echo "iproute" ;;
+ netstat) echo "net-tools" ;;
+ *) echo "$1" ;;
+ esac
+}
+
ensure_deps() {
- local missing=()
- for cmd in curl jq openssl git qrencode; do
- if ! command -v "$cmd" &>/dev/null; then
- missing+=("$cmd")
- fi
+ # Критические зависимости — без них скрипт не работает
+ local critical=(curl jq openssl git xxd tar dig)
+ # Желательные — есть fallback, устанавливать всё равно, но не падать если не смогли
+ local optional=(qrencode bc)
+
+ local missing_critical=() missing_optional=() cmd
+ for cmd in "${critical[@]}"; do
+ command -v "$cmd" &>/dev/null || missing_critical+=("$cmd")
done
- if [ ${#missing[@]} -gt 0 ]; then
- if type tf &>/dev/null; then
- log_step "$(tf deps_installing "${missing[*]}")"
- else
- log_step "Installing dependencies: ${missing[*]}"
- fi
- 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[@]}" ;;
+ for cmd in "${optional[@]}"; do
+ command -v "$cmd" &>/dev/null || missing_optional+=("$cmd")
+ done
+
+ local all_missing=("${missing_critical[@]}" "${missing_optional[@]}")
+ [ ${#all_missing[@]} -eq 0 ] && return 0
+
+ # Собираем список пакетов для выбранного менеджера
+ local pkg_mgr pkg pkgs=()
+ pkg_mgr=$(get_pkg_manager)
+
+ for cmd in "${all_missing[@]}"; do
+ case "$pkg_mgr" in
+ apt) pkg=$(apt_pkg_for_cmd "$cmd") ;;
+ dnf|yum) pkg=$(dnf_pkg_for_cmd "$cmd") ;;
+ *) pkg="$cmd" ;;
esac
+ pkgs+=("$pkg")
+ done
+
+ # Убираем дубликаты (например dig+nslookup оба = dnsutils)
+ local uniq_pkgs=()
+ for pkg in "${pkgs[@]}"; do
+ local found=0 p
+ for p in "${uniq_pkgs[@]}"; do
+ [ "$p" = "$pkg" ] && { found=1; break; }
+ done
+ [ "$found" = "0" ] && uniq_pkgs+=("$pkg")
+ done
+
+ if type tf &>/dev/null; then
+ log_step "$(tf deps_installing "${all_missing[*]}")"
+ else
+ log_step "Installing dependencies: ${all_missing[*]} (packages: ${uniq_pkgs[*]})"
fi
+
+ case "$pkg_mgr" in
+ apt)
+ export DEBIAN_FRONTEND=noninteractive
+ apt-get update -qq 2>/dev/null
+ apt-get install -y -qq "${uniq_pkgs[@]}" 2>/dev/null
+ ;;
+ dnf) dnf install -y -q "${uniq_pkgs[@]}" 2>/dev/null ;;
+ yum) yum install -y -q "${uniq_pkgs[@]}" 2>/dev/null ;;
+ *)
+ log_error "Unknown package manager — install manually: ${uniq_pkgs[*]}"
+ return 1
+ ;;
+ esac
+
+ # Фолбэки для xxd: на некоторых системах нужен vim-common вместо xxd
+ if ! command -v xxd &>/dev/null && [ "$pkg_mgr" = "apt" ]; then
+ apt-get install -y -qq vim-common 2>/dev/null
+ fi
+
+ # Повторная проверка критических команд
+ local still_missing=()
+ for cmd in "${critical[@]}"; do
+ command -v "$cmd" &>/dev/null || still_missing+=("$cmd")
+ done
+
+ if [ ${#still_missing[@]} -gt 0 ]; then
+ log_error "Critical dependencies still missing: ${still_missing[*]}"
+ log_error "Install manually and re-run gotelegram"
+ return 1
+ fi
+
+ # Опциональные — только предупреждение
+ local still_missing_opt=()
+ for cmd in "${optional[@]}"; do
+ command -v "$cmd" &>/dev/null || still_missing_opt+=("$cmd")
+ done
+ if [ ${#still_missing_opt[@]} -gt 0 ]; then
+ log_warning "Optional deps missing (features degraded): ${still_missing_opt[*]}"
+ fi
+
+ log_success "Dependencies ready"
+ return 0
+}
+
+# Быстрая проверка — только смотрит что критические установлены, ничего не ставит.
+# Возвращает 0 если всё ок, 1 если что-то отсутствует. Используется на старте
+# main() чтобы не дёргать apt-get update при каждом запуске меню.
+check_deps_present() {
+ local cmd
+ for cmd in curl jq openssl git xxd tar dig; do
+ command -v "$cmd" &>/dev/null || return 1
+ done
+ return 0
}
check_port() {