feat(v2.4.2): bot non-interactive install.sh actions

- install.sh: new bot_action_dispatch entry point for --action=X --json CLI invocation from the bot/scripts
- install.sh: bot_action_change_template — reuses download_template + deploy_template_to_nginx, updates config.json template_id
- install.sh: bot_action_change_lite_domain — regenerates telemt TOML with new fake-TLS mask domain, restarts telemt
- bot.py: run_bot_action() subprocess wrapper parses JSON result
- bot.py: cb_pro_confirm now performs real change-template when already in pro mode (fresh install still routes to CLI)
- bot.py: cb_lite_domain now performs real change-lite-domain when already in lite mode
- version -> 2.4.2
This commit is contained in:
anten-ka
2026-04-10 13:19:26 +03:00
parent 7b53566dad
commit fc28a1a099
3 changed files with 555 additions and 53 deletions

View File

@@ -100,13 +100,14 @@ logger = logging.getLogger(__name__)
# CONFIGURATION # CONFIGURATION
# ============================================================================ # ============================================================================
GOTELEGRAM_VERSION = "2.4.0" GOTELEGRAM_VERSION = "2.4.2"
GOTELEGRAM_CONFIG = "/opt/gotelegram/config.json" GOTELEGRAM_CONFIG = "/opt/gotelegram/config.json"
TELEMT_CONFIG = "/etc/telemt/config.toml" TELEMT_CONFIG = "/etc/telemt/config.toml"
TELEMT_SERVICE = "telemt" TELEMT_SERVICE = "telemt"
WEBSITE_ROOT = "/var/www/gotelegram-site" WEBSITE_ROOT = "/var/www/gotelegram-site"
BACKUP_DIR = "/opt/gotelegram/backups" BACKUP_DIR = "/opt/gotelegram/backups"
TEMPLATES_CATALOG = "/opt/gotelegram/templates_catalog.json" TEMPLATES_CATALOG = "/opt/gotelegram/templates_catalog.json"
INSTALL_SH = "/opt/gotelegram/install.sh"
PROMO_LINK_1 = "https://vk.cc/ct29NQ" PROMO_LINK_1 = "https://vk.cc/ct29NQ"
PROMO_LINK_2 = "https://vk.cc/cUxAhj" 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)) 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]: def load_json(path: str) -> Optional[Dict]:
"""Load JSON file.""" """Load JSON file."""
try: 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: 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 Branches on current mode:
/opt/gotelegram/config.json with a minimal fake config (no secret, no * lite mode (active): invoke `install.sh --action=change-lite-domain`
mask_host, no engine), silently corrupting the live proxy and showing a which regenerates the telemt TOML with a new fake-TLS mask domain and
fake "✅ Lite mode installed!" — while never actually invoking install.sh. restarts the service. Preserves secret/port.
* any other mode: route to CLI. Fresh Lite install is interactive.
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.
""" """
query = update.callback_query query = update.callback_query
data = query.data data = query.data
@@ -830,14 +880,52 @@ async def cb_lite_domain(update: Update, context: ContextTypes.DEFAULT_TYPE) ->
await query.answer() await query.answer()
text = ( config = load_json(GOTELEGRAM_CONFIG) or {}
"<b>⚠️ Установка из бота пока не поддерживается</b>\n\n" current_mode = config.get("mode", "")
f"Выбранный домен: <code>{html.escape(domain)}</code>\n\n"
"Чтобы установить или переключить режим Lite, запустите на сервере:\n" if current_mode != "lite":
"<code>gotelegram</code>\n\n" text = (
"Затем: <b>1) Прокси → 1) Установить/Обновить → Lite</b>.\n\n" "<b>⚠️ Установка Lite из бота пока не поддерживается</b>\n\n"
"Существующая конфигурация <b>не была изменена</b>." f"Выбранный домен: <code>{html.escape(domain)}</code>\n\n"
"Чтобы установить Lite, запустите на сервере:\n"
"<code>gotelegram</code> → <b>1) Прокси → 1) Установить/Обновить → Lite</b>\n\n"
"Существующая конфигурация <b>не была изменена</b>."
)
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 = (
"<b>⏳ Меняю маскировочный домен...</b>\n\n"
f"Новый домен: <code>{html.escape(domain)}</code>\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 = (
"<b>✅ Маскировочный домен обновлён</b>\n\n"
f"Новый домен: <code>{html.escape(domain)}</code>\n\n"
"telemt перезапущен. <b>Важно:</b> старые ссылки подключения больше "
"не будут работать — нужно заново раздать новые."
)
else:
err_msg = result.get("message", "unknown error")
err_code = result.get("code", "")
text = (
"<b>❌ Не удалось сменить домен</b>\n\n"
f"Домен: <code>{html.escape(domain)}</code>\n"
f"Причина: <code>{html.escape(err_msg)}</code>"
+ (f" (<code>{html.escape(err_code)}</code>)" if err_code else "")
+ "\n\n"
"Существующая конфигурация <b>не была изменена</b>."
)
keyboard = InlineKeyboardMarkup( keyboard = InlineKeyboardMarkup(
[[InlineKeyboardButton(_t(_uid(update), "btn_back"), callback_data="menu_main")]] [[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: 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 Branches on current mode:
/opt/gotelegram/config.json with a fake {"mode":"pro","template":..., * pro mode (active deployment): invoke `install.sh --action=change-template`
"port":443} blob — no secret, no domain, no mask_host — silently which downloads the new template and redeploys it to nginx. Reuses the
corrupting the live proxy so link generation and status broke. At the existing domain + SSL cert.
same time the user saw "✅ Pro mode installed!" although install.sh was * any other mode (or no install at all): route to CLI. Fresh Pro install
never invoked, no template was actually downloaded, nginx was not still requires interactive flow (domain, email, DNS check) — not safe
reconfigured and certbot was not touched. to run headless from the bot.
Non-interactive Pro install/change from the bot is not yet implemented. Historic context: v2.4.1 stub used to overwrite config.json with a fake
Until it is, refuse cleanly and route the user to the CLI instead of blob; that was replaced with a safe message in v2.4.1 hotfix; now in
damaging the working configuration. v2.4.2 we wire the real change-template path through install.sh.
""" """
query = update.callback_query query = update.callback_query
data = query.data data = query.data
@@ -1131,19 +1219,63 @@ async def cb_pro_confirm(update: Update, context: ContextTypes.DEFAULT_TYPE) ->
await query.answer() await query.answer()
text = ( # Read current config to decide: in-place change-template vs fresh install
"<b>⚠️ Установка шаблона из бота пока не поддерживается</b>\n\n" config = load_json(GOTELEGRAM_CONFIG) or {}
f"Выбранный шаблон: <code>{html.escape(tpl_id)}</code>\n\n" current_mode = config.get("mode", "")
"Чтобы установить или сменить шаблон, запустите на сервере:\n"
"<code>gotelegram</code>\n\n" if current_mode != "pro":
"Затем: <b>1) Прокси → 1) Установить/Обновить → Pro</b> " # Fresh install / mode switch — still routes to CLI (needs domain, SSL)
"(или <b>7) Сменить режим/шаблон</b> для смены текущего).\n\n" text = (
"Существующая конфигурация <b>не была изменена</b>." "<b>⚠️ Установка Pro из бота пока не поддерживается</b>\n\n"
f"Выбранный шаблон: <code>{html.escape(tpl_id)}</code>\n\n"
"Pro-режим требует ввода домена, email и проверки DNS. "
"Чтобы установить Pro, запустите на сервере:\n"
"<code>gotelegram</code> → <b>1) Прокси → 1) Установить/Обновить → Pro</b>\n\n"
"Существующая конфигурация <b>не была изменена</b>."
)
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 = (
"<b>⏳ Меняю шаблон сайта...</b>\n\n"
f"Шаблон: <code>{html.escape(tpl_id)}</code>\n\n"
"Скачиваю репозиторий и разворачиваю в nginx. "
"Это может занять 3090 секунд."
) )
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 = (
"<b>✅ Шаблон обновлён</b>\n\n"
f"Новый шаблон: <code>{html.escape(tpl_id)}</code>\n"
f"Сайт: <a href=\"https://{html.escape(domain, quote=True)}\">https://{html.escape(domain)}</a>\n\n"
"Прокси продолжает работать без перерыва."
)
else:
err_msg = result.get("message", "unknown error")
err_code = result.get("code", "")
text = (
"<b>❌ Не удалось сменить шаблон</b>\n\n"
f"Шаблон: <code>{html.escape(tpl_id)}</code>\n"
f"Причина: <code>{html.escape(err_msg)}</code>"
+ (f" (<code>{html.escape(err_code)}</code>)" if err_code else "")
+ "\n\n"
"Существующая конфигурация <b>не была изменена</b>. "
"Попробуйте другой шаблон или запустите <code>gotelegram</code> из консоли."
)
keyboard = InlineKeyboardMarkup( keyboard = InlineKeyboardMarkup(
[[InlineKeyboardButton(_t(_uid(update), "btn_back"), callback_data="menu_main")]] [[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)
# ============================================================================ # ============================================================================

View File

@@ -1136,11 +1136,257 @@ menu_language() {
esac 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 <status> <message> [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 ─────────────────────────────────────────────── # ── Точка входа / Entry point ───────────────────────────────────────────────
main() { 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 check_root
init_dirs 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 (before banner so banner appears in chosen lang)
first_run_language_picker first_run_language_picker

156
lib/common.sh Normal file → Executable file
View File

@@ -3,7 +3,7 @@
# Colors, logging, spinner, system helpers, v1 compat, i18n-aware # Colors, logging, spinner, system helpers, v1 compat, i18n-aware
# ── Version ─────────────────────────────────────────────────────────────────── # ── Version ───────────────────────────────────────────────────────────────────
GOTELEGRAM_VERSION="2.4.1" GOTELEGRAM_VERSION="2.4.2"
GOTELEGRAM_NAME="GoTelegram" GOTELEGRAM_NAME="GoTelegram"
# ── Пути ────────────────────────────────────────────────────────────────────── # ── Пути ──────────────────────────────────────────────────────────────────────
@@ -247,25 +247,149 @@ install_pkg() {
esac 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 <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() { ensure_deps() {
local missing=() # Критические зависимости — без них скрипт не работает
for cmd in curl jq openssl git qrencode; do local critical=(curl jq openssl git xxd tar dig)
if ! command -v "$cmd" &>/dev/null; then # Желательные — есть fallback, устанавливать всё равно, но не падать если не смогли
missing+=("$cmd") local optional=(qrencode bc)
fi
local missing_critical=() missing_optional=() cmd
for cmd in "${critical[@]}"; do
command -v "$cmd" &>/dev/null || missing_critical+=("$cmd")
done done
if [ ${#missing[@]} -gt 0 ]; then for cmd in "${optional[@]}"; do
if type tf &>/dev/null; then command -v "$cmd" &>/dev/null || missing_optional+=("$cmd")
log_step "$(tf deps_installing "${missing[*]}")" done
else
log_step "Installing dependencies: ${missing[*]}" local all_missing=("${missing_critical[@]}" "${missing_optional[@]}")
fi [ ${#all_missing[@]} -eq 0 ] && return 0
case "$(get_pkg_manager)" in
apt) apt-get update -qq && apt-get install -y -qq "${missing[@]}" ;; # Собираем список пакетов для выбранного менеджера
dnf) dnf install -y -q "${missing[@]}" ;; local pkg_mgr pkg pkgs=()
yum) yum install -y -q "${missing[@]}" ;; 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 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 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() { check_port() {