mirror of
https://github.com/anten-ka/gotelegram_pro.git
synced 2026-05-19 19:06:07 +00:00
- lib/backup.sh: complete rewrite for Unified Backup Format v2.0 * metadata.json with backup_id (GT-YYMMDD-<last6hex>) and SHA-256 fingerprint * secrets.json with raw_secret, faketls_secret, proxy_link, bot_token * structured dirs: telemt/, gotelegram/, nginx/, letsencrypt/, site/, bot/ * auto-detect and auto-migrate v1.1 -> v2.0 on restore * parse_manual_secret: accepts tg://proxy URL, ee-prefix, or raw 32-hex * manual_secret_input: interactive entry with env var export - install.sh: new 3-option menu_install (new / restore / existing key) * install_lite_mode + install_pro_mode respect GOTELEGRAM_EXISTING_* env vars - lib/lang/ru.sh + en.sh: v2.4.9 i18n strings (backup_*, manual_secret_*, install_menu_*) - lib/common.sh + gotelegram-bot/bot.py: version bump to 2.4.9
1698 lines
64 KiB
Bash
1698 lines
64 KiB
Bash
#!/bin/bash
|
||
# ══════════════════════════════════════════════════════════════════════════════
|
||
# GoTelegram v2.4.0 — MTProxy powered by telemt (Rust + Tokio)
|
||
# Anti-DPI • Fake TLS • TCP Splice • JA3/JA4 Resistance • i18n (EN/RU)
|
||
#
|
||
# Install:
|
||
# curl -sL URL/install.sh | sudo bash
|
||
# ══════════════════════════════════════════════════════════════════════════════
|
||
|
||
set -uo pipefail
|
||
|
||
# Script path and libraries
|
||
SCRIPT_DIR="$(cd "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")" && pwd)"
|
||
LIB_DIR="$SCRIPT_DIR/lib"
|
||
|
||
# Load libraries
|
||
source "$LIB_DIR/common.sh"
|
||
source "$LIB_DIR/i18n.sh"
|
||
source "$LIB_DIR/telemt.sh"
|
||
source "$LIB_DIR/telemt_config.sh"
|
||
source "$LIB_DIR/website.sh"
|
||
source "$LIB_DIR/templates_catalog.sh"
|
||
source "$LIB_DIR/backup.sh"
|
||
[ -f "$LIB_DIR/stats.sh" ] && source "$LIB_DIR/stats.sh"
|
||
|
||
# Load language (from config.json or marker file, default en)
|
||
load_language "$(detect_language)"
|
||
|
||
# ── Главное меню (Compact Dashboard + 5 Top-Level Items) ──────────────────────
|
||
show_main_menu() {
|
||
local proxy_status bot_status nginx_st mode domain secret port ip link ssl_expiry
|
||
proxy_status=$(telemt_status)
|
||
bot_status=$(bot_service_status)
|
||
nginx_st=$(nginx_status 2>/dev/null || echo "stopped")
|
||
mode=$(config_get mode 2>/dev/null || echo "—")
|
||
domain=$(config_get domain 2>/dev/null || echo "")
|
||
secret=$(get_config_value secret 2>/dev/null || echo "")
|
||
port=$(get_config_value port 2>/dev/null || echo "443")
|
||
ip=$(get_server_ip 2>/dev/null || echo "N/A")
|
||
|
||
local W=54
|
||
local line; line=$(printf '━%.0s' $(seq 1 $W))
|
||
local line2; line2=$(printf '─%.0s' $(seq 1 $W))
|
||
|
||
# ── Header (no right border — ANSI breaks alignment) ──
|
||
echo ""
|
||
echo -e " ${BOLD}${CYAN}━${line}━${NC}"
|
||
echo -e " ${BOLD}${WHITE} GoTelegram v${GOTELEGRAM_VERSION}${NC} ${DIM}— $(t dashboard_title)${NC}"
|
||
echo -e " ${BOLD}${CYAN}━${line}━${NC}"
|
||
|
||
# ── Service health ──
|
||
echo ""
|
||
echo -e " ${DIM}${line2}${NC}"
|
||
|
||
# Proxy
|
||
local proxy_icon proxy_color
|
||
case "$proxy_status" in
|
||
running) proxy_icon="●"; proxy_color="${GREEN}" ;;
|
||
stopped) proxy_icon="○"; proxy_color="${YELLOW}" ;;
|
||
*) proxy_icon="✗"; proxy_color="${RED}" ;;
|
||
esac
|
||
echo -e " ${proxy_color}${proxy_icon}${NC} $(t svc_proxy) ${proxy_color}${proxy_status}${NC} ${DIM}(telemt ${mode})${NC}"
|
||
|
||
# nginx
|
||
local nginx_icon nginx_color
|
||
case "$nginx_st" in
|
||
running) nginx_icon="●"; nginx_color="${GREEN}" ;;
|
||
*) nginx_icon="✗"; nginx_color="${RED}" ;;
|
||
esac
|
||
echo -e " ${nginx_icon}${nginx_color}${NC} $(t svc_nginx) ${nginx_color}${nginx_st}${NC} ${DIM}(127.0.0.1:8443)${NC}"
|
||
|
||
# Site (pro)
|
||
if [ "$mode" = "pro" ] && [ -n "$domain" ]; then
|
||
local site_icon site_color
|
||
if curl -sk --max-time 3 "https://${domain}/" -o /dev/null 2>/dev/null; then
|
||
site_icon="●"; site_color="${GREEN}"
|
||
else
|
||
site_icon="✗"; site_color="${RED}"
|
||
fi
|
||
echo -e " ${site_color}${site_icon}${NC} $(t svc_site) ${site_color}https://${domain}${NC}"
|
||
|
||
ssl_expiry=$(get_ssl_expiry "$domain" 2>/dev/null || echo "N/A")
|
||
echo -e " ${GREEN}●${NC} $(t svc_ssl) ${DIM}$(tf ssl_until "$ssl_expiry")${NC}"
|
||
fi
|
||
|
||
# Bot
|
||
case "$bot_status" in
|
||
running) echo -e " ${GREEN}●${NC} $(t svc_bot) ${GREEN}$(t running)${NC}" ;;
|
||
stopped) echo -e " ${YELLOW}○${NC} $(t svc_bot) ${YELLOW}$(t stopped)${NC}" ;;
|
||
esac
|
||
|
||
echo -e " ${DIM}${line2}${NC}"
|
||
|
||
# ── Network parameters ──
|
||
echo -e " ${WHITE}$(t net_ip)${NC} ${CYAN}${ip}${NC} ${WHITE}$(t net_port)${NC} ${CYAN}${port}${NC} ${WHITE}$(t net_mode)${NC} ${CYAN}${mode}${NC}"
|
||
if [ -n "$domain" ]; then
|
||
echo -e " ${WHITE}$(t net_domain)${NC} ${CYAN}${domain}${NC}"
|
||
fi
|
||
|
||
echo -e " ${DIM}${line2}${NC}"
|
||
|
||
# ── Proxy link + QR ──
|
||
local mask_host
|
||
mask_host=$(config_get mask_host 2>/dev/null || echo "")
|
||
if [ -n "$secret" ] && [ "$proxy_status" = "running" ]; then
|
||
if [ "$mode" = "pro" ] && [ -n "$domain" ]; then
|
||
link=$(generate_proxy_link "$domain" "$port" "$secret" "$domain")
|
||
else
|
||
link=$(generate_proxy_link "$ip" "$port" "$secret" "$mask_host")
|
||
fi
|
||
|
||
echo -e " ${BOLD}${WHITE}$(t connection_link)${NC}"
|
||
echo -e " ${GREEN}${link}${NC}"
|
||
|
||
if command -v qrencode &>/dev/null; then
|
||
echo ""
|
||
qrencode -t UTF8 -m 2 "$link" 2>/dev/null | while IFS= read -r qr_line; do
|
||
echo " ${qr_line}"
|
||
done
|
||
echo ""
|
||
fi
|
||
else
|
||
echo -e " ${DIM}$(t proxy_not_configured)${NC}"
|
||
echo ""
|
||
fi
|
||
|
||
# ── Menu ──
|
||
echo -e " ${DIM}${line2}${NC}"
|
||
echo -e " ${CYAN}1${NC}) $(t menu_proxy)"
|
||
echo -e " ${CYAN}2${NC}) $(t menu_stats)"
|
||
echo -e " ${CYAN}3${NC}) $(t menu_manage)"
|
||
echo -e " ${CYAN}4${NC}) $(t menu_telegram_bot)"
|
||
echo -e " ${CYAN}5${NC}) $(t menu_about)"
|
||
echo -e " ${CYAN}0${NC}) ${DIM}$(t exit)${NC}"
|
||
echo -e " ${DIM}${line2}${NC}"
|
||
echo -e " ${DIM}$(t auto_refresh_30s)${NC}"
|
||
echo -ne " ${WHITE}▸ ${NC}"
|
||
}
|
||
|
||
# ── Submenu: Proxy ──────────────────────────────────────────────────────────
|
||
submenu_proxy() {
|
||
while true; do
|
||
echo ""
|
||
echo -e " ${BOLD}${WHITE}$(t submenu_proxy_title)${NC}"
|
||
echo -e " ${DIM}$(printf '─%.0s' {1..54})${NC}"
|
||
echo -e " ${CYAN}1${NC}) $(t proxy_install_update)"
|
||
echo -e " ${CYAN}2${NC}) $(t proxy_status_detail)"
|
||
echo -e " ${CYAN}3${NC}) $(t proxy_copy_link)"
|
||
echo -e " ${CYAN}4${NC}) $(t proxy_share)"
|
||
echo -e " ${CYAN}5${NC}) $(t proxy_restart)"
|
||
echo -e " ${CYAN}6${NC}) $(t proxy_logs)"
|
||
echo -e " ${CYAN}7${NC}) $(t proxy_change_mode)"
|
||
echo -e " ${CYAN}0${NC}) $(t back)"
|
||
echo -e " ${DIM}$(printf '─%.0s' {1..54})${NC}"
|
||
echo -ne " ${WHITE}$(t choose):${NC} "
|
||
read -r ch
|
||
|
||
case "$ch" in
|
||
1) menu_install ;;
|
||
2) menu_status ;;
|
||
3) menu_link ;;
|
||
4) menu_share ;;
|
||
5) menu_restart ;;
|
||
6) menu_logs ;;
|
||
7) menu_change_mode ;;
|
||
0) break ;;
|
||
*) log_error "$(t invalid_choice)" ;;
|
||
esac
|
||
|
||
echo ""
|
||
echo -ne " ${DIM}$(t press_enter)${NC}"
|
||
read -r
|
||
done
|
||
}
|
||
|
||
# ── Submenu: Management ─────────────────────────────────────────────────────
|
||
submenu_manage() {
|
||
while true; do
|
||
echo ""
|
||
echo -e " ${BOLD}${WHITE}$(t submenu_manage_title)${NC}"
|
||
echo -e " ${DIM}$(printf '─%.0s' {1..54})${NC}"
|
||
echo -e " ${CYAN}1${NC}) $(t manage_backup)"
|
||
echo -e " ${CYAN}2${NC}) $(t manage_restore)"
|
||
echo -e " ${CYAN}3${NC}) $(t manage_update_telemt)"
|
||
echo -e " ${CYAN}4${NC}) $(t manage_site_ssl)"
|
||
echo -e " ${CYAN}5${NC}) $(t manage_remove)"
|
||
echo -e " ${CYAN}6${NC}) $(t manage_language)"
|
||
echo -e " ${CYAN}0${NC}) $(t back)"
|
||
echo -e " ${DIM}$(printf '─%.0s' {1..54})${NC}"
|
||
echo -ne " ${WHITE}$(t choose):${NC} "
|
||
read -r ch
|
||
|
||
case "$ch" in
|
||
1) interactive_backup ;;
|
||
2) interactive_restore ;;
|
||
3) update_telemt ;;
|
||
4) menu_website ;;
|
||
5) menu_remove ;;
|
||
6) menu_language ;;
|
||
0) break ;;
|
||
*) log_error "$(t invalid_choice)" ;;
|
||
esac
|
||
|
||
echo ""
|
||
echo -ne " ${DIM}$(t press_enter)${NC}"
|
||
read -r
|
||
done
|
||
}
|
||
|
||
# ── Submenu: About ──────────────────────────────────────────────────────────
|
||
submenu_about() {
|
||
while true; do
|
||
echo ""
|
||
echo -e " ${BOLD}${WHITE}$(t submenu_about_title)${NC}"
|
||
echo -e " ${DIM}$(printf '─%.0s' {1..54})${NC}"
|
||
echo -e " ${CYAN}1${NC}) $(t about_version_info)"
|
||
echo -e " ${CYAN}2${NC}) $(t about_promo)"
|
||
echo -e " ${CYAN}0${NC}) $(t back)"
|
||
echo -e " ${DIM}$(printf '─%.0s' {1..54})${NC}"
|
||
echo -ne " ${WHITE}$(t choose):${NC} "
|
||
read -r ch
|
||
|
||
case "$ch" in
|
||
1) menu_version ;;
|
||
2) menu_promo ;;
|
||
0) break ;;
|
||
*) log_error "$(t invalid_choice)" ;;
|
||
esac
|
||
|
||
echo ""
|
||
echo -ne " ${DIM}$(t press_enter)${NC}"
|
||
read -r
|
||
done
|
||
}
|
||
|
||
# ── Version info ────────────────────────────────────────────────────────────
|
||
menu_version() {
|
||
echo ""
|
||
echo -e " ${BOLD}${WHITE}$(t version_title)${NC}"
|
||
echo -e " ${DIM}$(printf '─%.0s' {1..54})${NC}"
|
||
echo -e " ${WHITE}$(t version_label)${NC} v${GOTELEGRAM_VERSION}"
|
||
echo -e " ${WHITE}$(t version_engine)${NC} telemt (Rust + Tokio)"
|
||
echo -e " ${WHITE}$(t version_tech)${NC} Anti-DPI, Fake TLS, TCP Splice"
|
||
echo -e " ${WHITE}$(t version_license)${NC} MIT"
|
||
echo -e " ${DIM}$(printf '─%.0s' {1..54})${NC}"
|
||
}
|
||
|
||
# ── Install: mode selection ─────────────────────────────────────────────────
|
||
menu_install() {
|
||
# Check for v1
|
||
if detect_v1_installation; then
|
||
echo ""
|
||
echo -e " ${YELLOW}$(t v1_detected)${NC}"
|
||
echo -e " ${DIM}$(tf v1_container "$V1_CONTAINER_NAME")${NC}"
|
||
echo ""
|
||
if ! migrate_v1_to_v2; then
|
||
return
|
||
fi
|
||
fi
|
||
|
||
# Always start from a clean slate — any leftover env vars from a previous
|
||
# manual-key entry must not leak into a "new install" flow.
|
||
unset GOTELEGRAM_EXISTING_SECRET GOTELEGRAM_EXISTING_DOMAIN GOTELEGRAM_EXISTING_PORT
|
||
|
||
# ── Step 1: install source picker ────────────────────────────────────────
|
||
echo ""
|
||
echo -e " ${BOLD}${WHITE}$(_t_or install_source_title 'Источник установки')${NC}"
|
||
echo -e " ${DIM}$(printf '─%.0s' {1..55})${NC}"
|
||
echo -e " ${CYAN}1)${NC} ${GREEN}$(_t_or install_menu_new 'Новая установка')${NC}"
|
||
echo -e " ${DIM}$(_t_or install_menu_new_desc 'Сгенерировать новый ключ и настроить с нуля')${NC}"
|
||
echo ""
|
||
echo -e " ${CYAN}2)${NC} ${BLUE}$(_t_or install_menu_restore 'Восстановить из бекапа')${NC}"
|
||
echo -e " ${DIM}$(_t_or install_menu_restore_desc 'Полное восстановление из файла .tar.gz[.enc]')${NC}"
|
||
echo ""
|
||
echo -e " ${CYAN}3)${NC} ${YELLOW}$(_t_or install_menu_existing_key 'Использовать существующий ключ')${NC}"
|
||
echo -e " ${DIM}$(_t_or install_menu_existing_key_desc 'Ввести ссылку tg://proxy или ключ вручную')${NC}"
|
||
echo -e " ${DIM}$(printf '─%.0s' {1..55})${NC}"
|
||
echo -ne " ${WHITE}$(_t_or install_source_choice 'Выберите источник')${NC} "
|
||
read -r src_choice
|
||
src_choice="${src_choice:-}"
|
||
|
||
case "$src_choice" in
|
||
1) : ;; # fall through to mode picker
|
||
2)
|
||
if type interactive_restore &>/dev/null; then
|
||
interactive_restore
|
||
else
|
||
log_error "backup.sh not loaded"
|
||
fi
|
||
return
|
||
;;
|
||
3)
|
||
if type manual_secret_input &>/dev/null; then
|
||
manual_secret_input || return
|
||
else
|
||
log_error "backup.sh not loaded"
|
||
return
|
||
fi
|
||
;;
|
||
*) log_error "$(tf install_bad_choice "${src_choice:-<empty>}")" ; return ;;
|
||
esac
|
||
|
||
# ── Step 2: lite/pro mode picker ─────────────────────────────────────────
|
||
echo ""
|
||
echo -e " ${BOLD}${WHITE}$(t install_select_mode)${NC}"
|
||
echo -e " ${DIM}$(printf '─%.0s' {1..55})${NC}"
|
||
echo -e " ${CYAN}1)${NC} ${GREEN}$(t install_lite_title)${NC}"
|
||
echo -e " ${DIM}$(t install_lite_desc1)${NC}"
|
||
echo -e " ${DIM}$(t install_lite_desc2)${NC}"
|
||
echo ""
|
||
echo -e " ${CYAN}2)${NC} ${MAGENTA}$(t install_pro_title)${NC}"
|
||
echo -e " ${DIM}$(t install_pro_desc1)${NC}"
|
||
echo -e " ${DIM}$(t install_pro_desc2)${NC}"
|
||
echo -e " ${DIM}$(t install_pro_desc3)${NC}"
|
||
echo -e " ${DIM}$(printf '─%.0s' {1..55})${NC}"
|
||
echo -ne " ${WHITE}$(t install_mode_choice)${NC} "
|
||
read -r mode_choice
|
||
mode_choice="${mode_choice:-}"
|
||
|
||
# If user provided an ee-prefixed key with a domain, hint at pro mode
|
||
if [ -n "${GOTELEGRAM_EXISTING_DOMAIN:-}" ] && [ "$mode_choice" = "1" ]; then
|
||
log_warning "$(_t_or install_hint_pro_mode 'Ключ содержит домен — обычно это Pro режим')"
|
||
fi
|
||
|
||
case "$mode_choice" in
|
||
1) install_lite_mode ;;
|
||
2) install_pro_mode ;;
|
||
*) log_error "$(tf install_bad_choice "${mode_choice:-<empty>}")" ;;
|
||
esac
|
||
|
||
# Clean up env vars after install, just in case
|
||
unset GOTELEGRAM_EXISTING_SECRET GOTELEGRAM_EXISTING_DOMAIN GOTELEGRAM_EXISTING_PORT
|
||
}
|
||
|
||
# ── Lite mode ───────────────────────────────────────────────────────────────
|
||
install_lite_mode() {
|
||
log_step "$(t install_lite_step)"
|
||
|
||
# Domain selection
|
||
local domain
|
||
domain=$(select_quick_domain)
|
||
[ $? -ne 0 ] && return
|
||
|
||
# Port selection — if user provided a port via existing-key flow, reuse it
|
||
local port
|
||
if [ -n "${GOTELEGRAM_EXISTING_PORT:-}" ] && [[ "$GOTELEGRAM_EXISTING_PORT" =~ ^[0-9]+$ ]]; then
|
||
port="$GOTELEGRAM_EXISTING_PORT"
|
||
log_info "$(_t_or install_reuse_port 'Используется порт из ключа'): ${port}"
|
||
else
|
||
port=$(select_port)
|
||
[ $? -ne 0 ] && return
|
||
fi
|
||
|
||
# Preflight: port conflict check (checks the external port only for lite)
|
||
if ! preflight_check "lite" "$port"; then
|
||
show_promo_with_qr 15
|
||
return
|
||
fi
|
||
|
||
# Secret: reuse if provided via manual_secret_input, otherwise generate new
|
||
local secret
|
||
if [ -n "${GOTELEGRAM_EXISTING_SECRET:-}" ]; then
|
||
secret="$GOTELEGRAM_EXISTING_SECRET"
|
||
log_info "$(_t_or install_reuse_secret 'Используется переданный ключ'): ${secret:0:8}...${secret: -4}"
|
||
else
|
||
secret=$(generate_hex 32)
|
||
fi
|
||
|
||
# Confirm
|
||
local ip
|
||
ip=$(get_server_ip)
|
||
echo ""
|
||
echo -e " ${BOLD}${WHITE}$(t install_config_title)${NC}"
|
||
echo -e " $(t install_cfg_ip) ${CYAN}${ip}${NC}"
|
||
echo -e " $(t install_cfg_port) ${CYAN}${port}${NC}"
|
||
echo -e " $(t install_cfg_mask) ${CYAN}${domain}${NC}"
|
||
echo -e " $(t install_cfg_mode) ${GREEN}Lite${NC}"
|
||
echo ""
|
||
|
||
if ! confirm "$(t install_confirm_proxy)"; then
|
||
return
|
||
fi
|
||
|
||
# Install
|
||
ensure_deps
|
||
install_telemt_full || return
|
||
|
||
# Generate telemt config
|
||
generate_telemt_toml "$secret" "$port" "lite" "$domain" "443"
|
||
|
||
# Validate
|
||
validate_telemt_config || return
|
||
|
||
# Start
|
||
start_telemt || return
|
||
|
||
# Save GoTelegram config
|
||
save_gotelegram_config "telemt" "lite" "$port" "$secret" "$domain" "" ""
|
||
|
||
# Credits
|
||
show_credits
|
||
|
||
# Result
|
||
show_proxy_info
|
||
log_success "$(tf install_done "$GOTELEGRAM_VERSION" "Lite")"
|
||
}
|
||
|
||
# ── Pro mode ────────────────────────────────────────────────────────────────
|
||
install_pro_mode() {
|
||
log_step "$(t install_pro_step)"
|
||
|
||
# Preflight: pro mode needs 443, 80 and 8443 (internal nginx mask)
|
||
if ! preflight_check "pro"; then
|
||
show_promo_with_qr 15
|
||
return
|
||
fi
|
||
|
||
# Enter domain — if provided via existing-key flow, reuse it
|
||
local user_domain=""
|
||
if [ -n "${GOTELEGRAM_EXISTING_DOMAIN:-}" ]; then
|
||
user_domain="$GOTELEGRAM_EXISTING_DOMAIN"
|
||
log_info "$(_t_or install_reuse_domain 'Используется домен из ключа'): ${user_domain}"
|
||
else
|
||
echo ""
|
||
echo -ne " ${WHITE}$(t install_enter_domain)${NC} "
|
||
read -r user_domain
|
||
fi
|
||
|
||
if [ -z "$user_domain" ] || ! validate_domain "$user_domain"; then
|
||
log_error "$(tf install_bad_domain "${user_domain:-<empty>}")"
|
||
return
|
||
fi
|
||
|
||
# Check DNS
|
||
local resolved_ip server_ip
|
||
resolved_ip=$(dig +short "$user_domain" A 2>/dev/null | head -1)
|
||
server_ip=$(get_server_ip)
|
||
|
||
if [ -n "$resolved_ip" ] && [ "$resolved_ip" != "$server_ip" ]; then
|
||
log_warning "$(tf install_dns_mismatch "$user_domain" "$resolved_ip" "$server_ip")"
|
||
if ! confirm "$(t install_continue_anyway)"; then
|
||
return
|
||
fi
|
||
fi
|
||
|
||
# Email for Let's Encrypt
|
||
echo -ne " ${WHITE}$(t install_enter_email)${NC} "
|
||
read -r ssl_email
|
||
|
||
# Template selection
|
||
local template_dir
|
||
template_dir=$(interactive_template_selection)
|
||
[ $? -ne 0 ] && return
|
||
|
||
# Pro architecture:
|
||
# telemt listens on 0.0.0.0:443 (accepts ALL connections)
|
||
# nginx listens on 127.0.0.1:8443 with SSL (serves website)
|
||
# MTProxy client → :443 → telemt (proxies)
|
||
# Regular browser → :443 → telemt → 127.0.0.1:8443 → nginx (website)
|
||
# ISP only sees HTTPS on 443 to domain
|
||
local nginx_internal_port=8443
|
||
echo ""
|
||
echo -e " ${DIM}$(t install_arch_desc1)${NC}"
|
||
echo -e " ${DIM}$(tf install_arch_desc2 "$nginx_internal_port")${NC}"
|
||
echo -e " ${DIM}$(tf install_arch_desc3 "$user_domain")${NC}"
|
||
|
||
# Generate fake-TLS secret (ee + secret + hex domain)
|
||
# ee prefix tells Telegram client to masquerade traffic as TLS to domain
|
||
# Reuse existing secret if manual_secret_input provided it
|
||
local raw_secret
|
||
if [ -n "${GOTELEGRAM_EXISTING_SECRET:-}" ]; then
|
||
raw_secret="$GOTELEGRAM_EXISTING_SECRET"
|
||
log_info "$(_t_or install_reuse_secret 'Используется переданный ключ'): ${raw_secret:0:8}...${raw_secret: -4}"
|
||
else
|
||
raw_secret=$(generate_hex 32)
|
||
fi
|
||
local domain_hex
|
||
domain_hex=$(printf '%s' "$user_domain" | xxd -p | tr -d '\n')
|
||
local faketls_secret="ee${raw_secret}${domain_hex}"
|
||
|
||
# Confirmation
|
||
echo ""
|
||
echo -e " ${BOLD}${WHITE}$(t install_config_title)${NC}"
|
||
echo -e " $(t install_cfg_domain) ${CYAN}${user_domain}${NC}"
|
||
echo -e " $(t install_cfg_port) ${CYAN}443 (telemt + nginx)${NC}"
|
||
echo -e " $(t install_cfg_mode) ${MAGENTA}Pro (fake-TLS)${NC}"
|
||
echo ""
|
||
|
||
if ! confirm "$(t install_confirm_proxy_site)"; then
|
||
return
|
||
fi
|
||
|
||
# Install
|
||
ensure_deps
|
||
install_telemt_full || return
|
||
|
||
# telemt config: listen 443, masquerade to local nginx via dns_override
|
||
generate_telemt_toml "$raw_secret" "443" "pro" "$user_domain" "$nginx_internal_port"
|
||
|
||
# Website setup (nginx on internal port + certbot + template)
|
||
setup_pro_mode "$user_domain" "$template_dir" "$nginx_internal_port" "$ssl_email" || return
|
||
|
||
# Stop nginx on 443 before starting telemt (telemt will take 443)
|
||
# nginx already reconfigured to internal port
|
||
systemctl restart nginx 2>/dev/null
|
||
|
||
# Start telemt
|
||
start_telemt || return
|
||
|
||
# Save config
|
||
local tpl_id
|
||
tpl_id=$(basename "$template_dir")
|
||
save_gotelegram_config "telemt" "pro" "443" "$raw_secret" "$user_domain" "$user_domain" "$tpl_id"
|
||
|
||
# Result — use domain and fake-TLS link
|
||
show_proxy_info_pro "$user_domain" "$faketls_secret"
|
||
echo -e " ${WHITE}$(t svc_site):${NC} ${GREEN}https://${user_domain}${NC}"
|
||
log_success "$(tf install_done "$GOTELEGRAM_VERSION" "Pro")"
|
||
}
|
||
|
||
# ── Статус ───────────────────────────────────────────────────────────────────
|
||
menu_status() {
|
||
show_proxy_info
|
||
|
||
# Extras for pro
|
||
local mode
|
||
mode=$(config_get mode 2>/dev/null)
|
||
if [ "$mode" = "pro" ]; then
|
||
local domain
|
||
domain=$(config_get domain 2>/dev/null)
|
||
if [ -n "$domain" ]; then
|
||
local ssl_expiry
|
||
ssl_expiry=$(get_ssl_expiry "$domain")
|
||
local nginx_st
|
||
nginx_st=$(nginx_status)
|
||
echo -e " ${WHITE}$(t svc_nginx):${NC} ${nginx_st}"
|
||
echo -e " ${WHITE}$(t website_ssl_until)${NC} ${ssl_expiry}"
|
||
echo -e " ${WHITE}$(t svc_site):${NC} https://${domain}"
|
||
echo ""
|
||
fi
|
||
fi
|
||
}
|
||
|
||
# ── Ссылка ───────────────────────────────────────────────────────────────────
|
||
menu_link() {
|
||
local secret port ip link mode domain mask_host
|
||
secret=$(get_config_value secret)
|
||
port=$(get_config_value port)
|
||
ip=$(get_server_ip)
|
||
mode=$(config_get mode 2>/dev/null || echo "lite")
|
||
domain=$(config_get domain 2>/dev/null || echo "")
|
||
mask_host=$(config_get mask_host 2>/dev/null || echo "")
|
||
|
||
if [ "$mode" = "pro" ] && [ -n "$domain" ]; then
|
||
link=$(generate_proxy_link "$domain" "$port" "$secret" "$domain")
|
||
else
|
||
link=$(generate_proxy_link "$ip" "$port" "$secret" "$mask_host")
|
||
fi
|
||
|
||
echo ""
|
||
echo -e " ${BOLD}${WHITE}$(t link_title)${NC}"
|
||
echo ""
|
||
echo -e " ${GREEN}${link}${NC}"
|
||
echo ""
|
||
|
||
if command -v qrencode &>/dev/null; then
|
||
qrencode -t UTF8 -m 2 "$link" 2>/dev/null
|
||
fi
|
||
}
|
||
|
||
# ── Поделиться ───────────────────────────────────────────────────────────────
|
||
menu_share() {
|
||
local secret port ip link mode domain mask_host server_display
|
||
secret=$(get_config_value secret)
|
||
port=$(get_config_value port)
|
||
ip=$(get_server_ip)
|
||
mode=$(config_get mode 2>/dev/null || echo "lite")
|
||
domain=$(config_get domain 2>/dev/null || echo "")
|
||
mask_host=$(config_get mask_host 2>/dev/null || echo "")
|
||
|
||
if [ "$mode" = "pro" ] && [ -n "$domain" ]; then
|
||
link=$(generate_proxy_link "$domain" "$port" "$secret" "$domain")
|
||
server_display="$domain"
|
||
else
|
||
link=$(generate_proxy_link "$ip" "$port" "$secret" "$mask_host")
|
||
server_display="$ip"
|
||
fi
|
||
|
||
echo ""
|
||
echo -e " ${BOLD}$(t share_title)${NC}"
|
||
echo ""
|
||
printf "$(t share_line1)\n" "$GOTELEGRAM_VERSION"
|
||
echo ""
|
||
printf "$(t share_server)\n" "$server_display"
|
||
printf "$(t share_port)\n" "$port"
|
||
echo ""
|
||
echo "$(t share_connect_cta)"
|
||
echo "$link"
|
||
echo ""
|
||
echo "$(t share_footer)"
|
||
echo ""
|
||
}
|
||
|
||
# ── Перезапуск ───────────────────────────────────────────────────────────────
|
||
menu_restart() {
|
||
restart_telemt
|
||
local mode
|
||
mode=$(config_get mode 2>/dev/null)
|
||
if [ "$mode" = "pro" ]; then
|
||
restart_nginx
|
||
fi
|
||
}
|
||
|
||
# ── Logs ────────────────────────────────────────────────────────────────────
|
||
menu_logs() {
|
||
echo ""
|
||
echo -e " ${BOLD}${WHITE}$(tf logs_telemt_title 40)${NC}"
|
||
echo -e " ${DIM}$(printf '─%.0s' {1..55})${NC}"
|
||
telemt_logs 40
|
||
echo -e " ${DIM}$(printf '─%.0s' {1..55})${NC}"
|
||
}
|
||
|
||
# ── Change mode / template ──────────────────────────────────────────────────
|
||
menu_change_mode() {
|
||
local current_mode
|
||
current_mode=$(config_get mode 2>/dev/null)
|
||
echo ""
|
||
echo -e " ${WHITE}$(t change_current_mode)${NC} ${CYAN}${current_mode}${NC}"
|
||
echo ""
|
||
echo -e " ${CYAN}1${NC}) $(t change_template)"
|
||
echo -e " ${CYAN}2${NC}) $(t change_mode_switch)"
|
||
echo -e " ${CYAN}0${NC}) $(t back)"
|
||
echo -ne " ${WHITE}$(t choose):${NC} "
|
||
read -r ch
|
||
|
||
case "$ch" in
|
||
1)
|
||
if [ "$current_mode" != "pro" ]; then
|
||
log_error "$(t change_only_pro)"
|
||
return
|
||
fi
|
||
local template_dir
|
||
template_dir=$(interactive_template_selection)
|
||
[ $? -ne 0 ] && return
|
||
switch_template "$template_dir"
|
||
;;
|
||
2)
|
||
log_warning "$(t change_requires_reinstall)"
|
||
if confirm "$(t change_reinstall_confirm)"; then
|
||
menu_install
|
||
fi
|
||
;;
|
||
esac
|
||
}
|
||
|
||
# ── Website management ─────────────────────────────────────────────────────
|
||
menu_website() {
|
||
local mode
|
||
mode=$(config_get mode 2>/dev/null)
|
||
|
||
if [ "$mode" != "pro" ]; then
|
||
log_info "$(t website_only_pro)"
|
||
return
|
||
fi
|
||
|
||
local domain
|
||
domain=$(config_get domain 2>/dev/null)
|
||
|
||
echo ""
|
||
echo -e " ${BOLD}${WHITE}$(t website_title)${NC}"
|
||
echo -e " $(t website_domain) ${CYAN}${domain}${NC}"
|
||
echo -e " $(t website_ssl_until) $(get_ssl_expiry "$domain")"
|
||
echo ""
|
||
echo -e " ${CYAN}1${NC}) $(t website_renew_ssl)"
|
||
echo -e " ${CYAN}2${NC}) $(t website_restart_nginx)"
|
||
echo -e " ${CYAN}3${NC}) $(t website_change_template)"
|
||
echo -e " ${CYAN}0${NC}) $(t back)"
|
||
echo -ne " ${WHITE}$(t choose):${NC} "
|
||
read -r ch
|
||
|
||
case "$ch" in
|
||
1) renew_ssl_certificate ;;
|
||
2) restart_nginx ;;
|
||
3)
|
||
local template_dir
|
||
template_dir=$(interactive_template_selection)
|
||
[ $? -ne 0 ] && return
|
||
switch_template "$template_dir"
|
||
;;
|
||
esac
|
||
}
|
||
|
||
# ── Remove ─────────────────────────────────────────────────────────────────
|
||
menu_remove() {
|
||
echo ""
|
||
echo -e " ${BOLD}${RED}$(t remove_title)${NC}"
|
||
echo -e " ${DIM}$(printf '─%.0s' {1..55})${NC}"
|
||
echo -e " ${CYAN}1${NC}) $(t remove_proxy_only)"
|
||
echo -e " ${CYAN}2${NC}) $(t remove_bot_only)"
|
||
echo -e " ${CYAN}3${NC}) $(t remove_all)"
|
||
echo -e " ${CYAN}0${NC}) $(t back)"
|
||
echo -ne " ${WHITE}$(t choose):${NC} "
|
||
read -r rm_choice
|
||
|
||
case "$rm_choice" in
|
||
1)
|
||
log_warning "$(t remove_warn_proxy)"
|
||
if ! confirm "$(t remove_confirm_proxy)"; then return; fi
|
||
if confirm "$(t remove_backup_before)"; then
|
||
interactive_backup
|
||
fi
|
||
remove_telemt
|
||
local mode
|
||
mode=$(config_get mode 2>/dev/null)
|
||
if [ "$mode" = "pro" ]; then
|
||
remove_pro_mode
|
||
fi
|
||
rm -f "$GOTELEGRAM_CONFIG"
|
||
log_success "$(t remove_proxy_done)"
|
||
;;
|
||
2)
|
||
bot_remove
|
||
;;
|
||
3)
|
||
log_warning "$(t remove_warn_all)"
|
||
if ! confirm "$(t remove_confirm_all)"; then return; fi
|
||
if confirm "$(t remove_backup_before)"; then
|
||
interactive_backup
|
||
fi
|
||
# Proxy
|
||
remove_telemt
|
||
local mode
|
||
mode=$(config_get mode 2>/dev/null)
|
||
if [ "$mode" = "pro" ]; then
|
||
remove_pro_mode
|
||
fi
|
||
rm -f "$GOTELEGRAM_CONFIG"
|
||
# Bot
|
||
if [ "$(bot_service_status)" != "not_installed" ]; then
|
||
systemctl stop "$BOT_SERVICE" 2>/dev/null
|
||
systemctl disable "$BOT_SERVICE" 2>/dev/null
|
||
rm -f "/etc/systemd/system/${BOT_SERVICE}.service"
|
||
systemctl daemon-reload
|
||
rm -rf "$BOT_DIR"
|
||
fi
|
||
log_success "$(t remove_all_done)"
|
||
;;
|
||
esac
|
||
}
|
||
|
||
# ── Telegram-бот ────────────────────────────────────────────────────────────
|
||
BOT_DIR="/opt/gotelegram-bot"
|
||
BOT_SERVICE="gotelegram-bot"
|
||
|
||
bot_service_status() {
|
||
if ! systemctl list-unit-files "$BOT_SERVICE.service" &>/dev/null 2>&1; then
|
||
echo "not_installed"
|
||
elif systemctl is-active "$BOT_SERVICE" &>/dev/null 2>&1; then
|
||
echo "running"
|
||
else
|
||
echo "stopped"
|
||
fi
|
||
}
|
||
|
||
menu_bot() {
|
||
local st
|
||
st=$(bot_service_status)
|
||
|
||
echo ""
|
||
echo -e " ${BOLD}${WHITE}$(t bot_title)${NC}"
|
||
echo -e " ${DIM}$(printf '─%.0s' {1..55})${NC}"
|
||
|
||
case "$st" in
|
||
running)
|
||
echo -e " $(t bot_status_colon) ${GREEN}$(t bot_status_running)${NC}"
|
||
echo ""
|
||
echo -e " ${CYAN}1${NC}) $(t bot_menu_status)"
|
||
echo -e " ${CYAN}2${NC}) $(t bot_menu_logs)"
|
||
echo -e " ${CYAN}3${NC}) $(t bot_menu_restart)"
|
||
echo -e " ${CYAN}4${NC}) $(t bot_menu_stop)"
|
||
echo -e " ${CYAN}5${NC}) $(t bot_menu_settings)"
|
||
echo -e " ${CYAN}6${NC}) $(t bot_menu_remove)"
|
||
;;
|
||
stopped)
|
||
echo -e " $(t bot_status_colon) ${YELLOW}$(t bot_status_stopped)${NC}"
|
||
echo ""
|
||
echo -e " ${CYAN}1${NC}) $(t bot_menu_status)"
|
||
echo -e " ${CYAN}2${NC}) $(t bot_menu_logs)"
|
||
echo -e " ${CYAN}3${NC}) $(t bot_menu_start)"
|
||
echo -e " ${CYAN}5${NC}) $(t bot_menu_settings)"
|
||
echo -e " ${CYAN}6${NC}) $(t bot_menu_remove)"
|
||
;;
|
||
*)
|
||
echo -e " $(t bot_status_colon) ${RED}$(t bot_status_not_installed)${NC}"
|
||
echo ""
|
||
echo -e " ${DIM}$(t bot_intro1)${NC}"
|
||
echo -e " ${DIM}$(t bot_intro2)${NC}"
|
||
echo ""
|
||
echo -e " ${CYAN}1${NC}) $(t bot_menu_install)"
|
||
;;
|
||
esac
|
||
|
||
echo -e " ${CYAN}0${NC}) $(t back)"
|
||
echo -e " ${DIM}$(printf '─%.0s' {1..55})${NC}"
|
||
echo -ne " ${WHITE}$(t choose):${NC} "
|
||
read -r ch
|
||
|
||
case "$st" in
|
||
running)
|
||
case "$ch" in
|
||
1) bot_show_status ;;
|
||
2) bot_show_logs ;;
|
||
3) systemctl restart "$BOT_SERVICE" && log_success "$(t bot_restarted)" ;;
|
||
4) systemctl stop "$BOT_SERVICE" && log_info "$(t bot_stopped)" ;;
|
||
5) bot_edit_config ;;
|
||
6) bot_remove ;;
|
||
esac
|
||
;;
|
||
stopped)
|
||
case "$ch" in
|
||
1) bot_show_status ;;
|
||
2) bot_show_logs ;;
|
||
3) systemctl start "$BOT_SERVICE" && log_success "$(t bot_started)" ;;
|
||
5) bot_edit_config ;;
|
||
6) bot_remove ;;
|
||
esac
|
||
;;
|
||
*)
|
||
case "$ch" in
|
||
1) bot_install ;;
|
||
esac
|
||
;;
|
||
esac
|
||
}
|
||
|
||
bot_install() {
|
||
log_step "$(t bot_install_step)"
|
||
|
||
# Python + venv + pip (always ensure — python3 can be present without venv/pip)
|
||
local need_py=0
|
||
command -v python3 &>/dev/null || need_py=1
|
||
# python3-venv not having its own command; probe by trying 'python3 -m venv --help'
|
||
if ! python3 -m venv --help &>/dev/null; then need_py=1; fi
|
||
# pip check
|
||
if ! python3 -m pip --version &>/dev/null; then need_py=1; fi
|
||
|
||
if [ "$need_py" = "1" ]; then
|
||
log_info "$(t bot_install_python)"
|
||
if command -v apt-get &>/dev/null; then
|
||
# Detect Python version for versioned venv package (Debian 12 / Ubuntu 24.04 need python3.12-venv)
|
||
local py_ver=""
|
||
if command -v python3 &>/dev/null; then
|
||
py_ver=$(python3 -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")' 2>/dev/null)
|
||
fi
|
||
|
||
apt_update
|
||
|
||
# Build package list with versioned venv fallback
|
||
local pkg_list=(python3 python3-venv python3-pip)
|
||
[ -n "$py_ver" ] && pkg_list+=("python${py_ver}-venv")
|
||
# python3-full optional
|
||
if ! apt_install "${pkg_list[@]}" python3-full; then
|
||
log_warning "python3-full unavailable, installing core packages only..."
|
||
apt_install "${pkg_list[@]}" || {
|
||
log_error "Failed to install Python packages. Run manually: apt install ${pkg_list[*]}"
|
||
return 1
|
||
}
|
||
fi
|
||
elif command -v dnf &>/dev/null; then
|
||
dnf install -y -q python3 python3-pip
|
||
elif command -v yum &>/dev/null; then
|
||
yum install -y -q python3 python3-pip
|
||
fi
|
||
fi
|
||
|
||
# Copy bot files
|
||
mkdir -p "$BOT_DIR"
|
||
if [ -f "$SCRIPT_DIR/gotelegram-bot/bot.py" ]; then
|
||
cp "$SCRIPT_DIR/gotelegram-bot/bot.py" "$BOT_DIR/"
|
||
cp "$SCRIPT_DIR/gotelegram-bot/requirements.txt" "$BOT_DIR/"
|
||
[ -f "$SCRIPT_DIR/gotelegram-bot/config.example.env" ] && \
|
||
cp "$SCRIPT_DIR/gotelegram-bot/config.example.env" "$BOT_DIR/"
|
||
# Copy i18n language files for bot
|
||
if [ -d "$SCRIPT_DIR/gotelegram-bot/lang" ]; then
|
||
mkdir -p "$BOT_DIR/lang"
|
||
cp -f "$SCRIPT_DIR/gotelegram-bot/lang/"*.json "$BOT_DIR/lang/" 2>/dev/null
|
||
fi
|
||
[ -f "$SCRIPT_DIR/gotelegram-bot/i18n.py" ] && \
|
||
cp "$SCRIPT_DIR/gotelegram-bot/i18n.py" "$BOT_DIR/"
|
||
else
|
||
log_error "$(tf bot_files_not_found "$SCRIPT_DIR/gotelegram-bot/")"
|
||
return 1
|
||
fi
|
||
|
||
# Templates catalog — skip if source and dest are the same file (symlink install case)
|
||
if [ -f "$SCRIPT_DIR/templates_catalog.json" ]; then
|
||
local src_tc="$SCRIPT_DIR/templates_catalog.json"
|
||
local dst_tc="$GOTELEGRAM_DIR/templates_catalog.json"
|
||
if [ "$(readlink -f "$src_tc" 2>/dev/null)" != "$(readlink -f "$dst_tc" 2>/dev/null)" ]; then
|
||
cp "$src_tc" "$dst_tc"
|
||
fi
|
||
fi
|
||
|
||
# Venv — create, and verify pip exists (python3-venv can silently create broken venv)
|
||
if [ ! -d "$BOT_DIR/venv" ] || [ ! -x "$BOT_DIR/venv/bin/pip" ]; then
|
||
log_info "$(t bot_create_venv)"
|
||
rm -rf "$BOT_DIR/venv"
|
||
if ! python3 -m venv "$BOT_DIR/venv" 2>/tmp/venv_err; then
|
||
log_error "venv creation failed:"
|
||
cat /tmp/venv_err >&2 2>/dev/null
|
||
# Try to fix by installing versioned python3.X-venv package
|
||
if command -v apt-get &>/dev/null; then
|
||
local py_ver
|
||
py_ver=$(python3 -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")' 2>/dev/null)
|
||
log_info "reinstalling python${py_ver}-venv..."
|
||
apt_install python3-venv python3-pip "python${py_ver}-venv" python3-full || \
|
||
apt_install python3-venv python3-pip "python${py_ver}-venv" || true
|
||
rm -rf "$BOT_DIR/venv"
|
||
python3 -m venv "$BOT_DIR/venv" || { log_error "venv still broken, aborting. Manual fix: apt install python${py_ver}-venv python3-pip"; return 1; }
|
||
else
|
||
return 1
|
||
fi
|
||
fi
|
||
fi
|
||
|
||
if [ ! -x "$BOT_DIR/venv/bin/pip" ]; then
|
||
log_info "bootstrapping pip via ensurepip..."
|
||
"$BOT_DIR/venv/bin/python" -m ensurepip --upgrade 2>/dev/null || true
|
||
fi
|
||
|
||
if [ ! -x "$BOT_DIR/venv/bin/pip" ]; then
|
||
log_error "pip missing in venv — install python3-venv manually: apt install python3-venv python3-pip"
|
||
return 1
|
||
fi
|
||
|
||
log_info "$(t bot_install_deps)"
|
||
if ! "$BOT_DIR/venv/bin/pip" install -r "$BOT_DIR/requirements.txt" -q 2>/tmp/pip_err; then
|
||
log_error "pip install failed:"
|
||
tail -n 5 /tmp/pip_err >&2
|
||
return 1
|
||
fi
|
||
|
||
# Sanity check: verify critical imports succeed
|
||
if ! "$BOT_DIR/venv/bin/python" -c "import telegram, toml, dotenv" 2>/tmp/imp_err; then
|
||
log_error "dependency import check failed:"
|
||
cat /tmp/imp_err >&2
|
||
return 1
|
||
fi
|
||
|
||
# Configuration
|
||
if [ ! -f "$BOT_DIR/.env" ]; then
|
||
echo ""
|
||
echo -e " ${YELLOW}$(t bot_enter_token)${NC}"
|
||
local token=""
|
||
while [ -z "$token" ]; do
|
||
echo -ne " ${WHITE}$(t bot_token)${NC} "
|
||
read -r token
|
||
token=$(echo "$token" | tr -d '[:space:]')
|
||
[ -z "$token" ] && log_error "$(t bot_token_empty)"
|
||
done
|
||
|
||
echo ""
|
||
echo -e " ${WHITE}$(t bot_add_admin_how)${NC}"
|
||
echo -e " ${CYAN}1${NC}) $(t bot_admin_auto)"
|
||
echo -e " ${CYAN}2${NC}) $(t bot_admin_manual)"
|
||
echo -ne " ${WHITE}$(t choose) [1]:${NC} "
|
||
read -r admin_mode
|
||
admin_mode="${admin_mode:-1}"
|
||
|
||
local admin_ids=""
|
||
if [ "$admin_mode" = "2" ]; then
|
||
echo -ne " ${WHITE}$(t bot_admin_ids_prompt)${NC} "
|
||
read -r admin_ids
|
||
admin_ids=$(echo "$admin_ids" | tr ' ' ',' | sed 's/,,*/,/g; s/^,//; s/,$//')
|
||
fi
|
||
|
||
# Propagate selected language to bot so UI matches
|
||
local bot_lang
|
||
bot_lang=$(get_language 2>/dev/null || echo en)
|
||
{
|
||
echo "BOT_TOKEN=$token"
|
||
[ -n "$admin_ids" ] && echo "ALLOWED_IDS=$admin_ids"
|
||
echo "BOT_LANG=$bot_lang"
|
||
} > "$BOT_DIR/.env"
|
||
chmod 600 "$BOT_DIR/.env"
|
||
log_success "$(t bot_env_created)"
|
||
else
|
||
log_info "$(t bot_env_exists)"
|
||
fi
|
||
|
||
# Systemd
|
||
cat > "/etc/systemd/system/${BOT_SERVICE}.service" << SVCEOF
|
||
[Unit]
|
||
Description=GoTelegram v${GOTELEGRAM_VERSION} Telegram Bot
|
||
After=network.target
|
||
|
||
[Service]
|
||
Type=simple
|
||
WorkingDirectory=$BOT_DIR
|
||
ExecStart=$BOT_DIR/venv/bin/python $BOT_DIR/bot.py
|
||
Restart=always
|
||
RestartSec=5
|
||
Environment=PATH=$BOT_DIR/venv/bin:/usr/bin
|
||
|
||
[Install]
|
||
WantedBy=multi-user.target
|
||
SVCEOF
|
||
|
||
systemctl daemon-reload
|
||
systemctl enable "$BOT_SERVICE" &>/dev/null
|
||
systemctl restart "$BOT_SERVICE" 2>/dev/null || systemctl start "$BOT_SERVICE"
|
||
|
||
# If auto mode — wait until bot captures first admin
|
||
local has_ids
|
||
has_ids=$(grep "^ALLOWED_IDS=" "$BOT_DIR/.env" 2>/dev/null | cut -d= -f2)
|
||
if [ -z "$has_ids" ]; then
|
||
echo ""
|
||
# Simple bullet-style block (no box — printf %-Ns breaks on UTF-8 multibyte chars)
|
||
echo -e " ${YELLOW}▸${NC} ${BOLD}$(t bot_wait_admin_title)${NC}"
|
||
echo ""
|
||
echo -e " $(t bot_wait_admin_msg1) ${CYAN}/start${NC}"
|
||
echo -e " $(t bot_wait_admin_msg2)"
|
||
echo ""
|
||
echo -e " ${DIM}$(t bot_wait_admin_skip)${NC}"
|
||
echo ""
|
||
|
||
local frames=('⠋' '⠙' '⠹' '⠸' '⠼' '⠴' '⠦' '⠧' '⠇' '⠏')
|
||
local i=0
|
||
local waited=0
|
||
local max_wait=300 # 5 min max
|
||
|
||
# Catch Ctrl+C to skip waiting without killing the script
|
||
local interrupted=0
|
||
trap 'interrupted=1' INT
|
||
|
||
while [ $waited -lt $max_wait ] && [ $interrupted -eq 0 ]; do
|
||
printf "\r ${CYAN}${frames[$i]}${NC} $(tf bot_wait_spinner "$waited") " >&2
|
||
i=$(( (i+1) % ${#frames[@]} ))
|
||
sleep 1
|
||
waited=$((waited + 1))
|
||
|
||
# Check if ALLOWED_IDS has appeared
|
||
has_ids=$(grep "^ALLOWED_IDS=" "$BOT_DIR/.env" 2>/dev/null | cut -d= -f2)
|
||
if [ -n "$has_ids" ]; then
|
||
break
|
||
fi
|
||
done
|
||
|
||
trap - INT
|
||
printf "\r\033[K" >&2 # clear spinner line
|
||
|
||
if [ -n "$has_ids" ]; then
|
||
echo ""
|
||
log_success "$(t bot_admin_assigned)"
|
||
echo -e " ${WHITE}ID:${NC} ${GREEN}${has_ids}${NC}"
|
||
elif [ $interrupted -eq 1 ]; then
|
||
echo ""
|
||
log_warning "$(t bot_wait_skipped)"
|
||
else
|
||
echo ""
|
||
log_warning "$(t bot_wait_timeout)"
|
||
fi
|
||
fi
|
||
|
||
echo ""
|
||
log_success "$(t bot_installed)"
|
||
echo -e " ${DIM}systemctl status $BOT_SERVICE${NC}"
|
||
echo -e " ${DIM}journalctl -u $BOT_SERVICE -f${NC}"
|
||
}
|
||
|
||
bot_show_status() {
|
||
echo ""
|
||
echo -e " ${BOLD}${WHITE}$(t bot_status_title)${NC}"
|
||
echo -e " ${DIM}$(printf '─%.0s' {1..55})${NC}"
|
||
systemctl status "$BOT_SERVICE" --no-pager -l 2>/dev/null | head -15 | while IFS= read -r line; do
|
||
echo " $line"
|
||
done
|
||
echo -e " ${DIM}$(printf '─%.0s' {1..55})${NC}"
|
||
|
||
if [ -f "$BOT_DIR/.env" ]; then
|
||
local has_token has_ids
|
||
has_token=$(grep -c "BOT_TOKEN=" "$BOT_DIR/.env" 2>/dev/null || echo 0)
|
||
has_ids=$(grep "ALLOWED_IDS=" "$BOT_DIR/.env" 2>/dev/null | cut -d= -f2)
|
||
if [ "${has_token:-0}" -gt 0 ]; then
|
||
echo -e " $(t bot_token) ${GREEN}✓ $(t bot_token_configured)${NC}"
|
||
fi
|
||
if [ -n "$has_ids" ]; then
|
||
echo -e " $(t bot_access_colon) $(tf bot_access_ids_fmt "$has_ids")"
|
||
else
|
||
echo -e " $(t bot_access_colon) ${YELLOW}$(t bot_access_open)${NC}"
|
||
fi
|
||
fi
|
||
}
|
||
|
||
bot_show_logs() {
|
||
echo ""
|
||
echo -e " ${BOLD}${WHITE}$(t bot_logs_title)${NC}"
|
||
echo -e " ${DIM}$(printf '─%.0s' {1..55})${NC}"
|
||
journalctl -u "$BOT_SERVICE" --no-pager -n 30 2>/dev/null | while IFS= read -r line; do
|
||
echo " $line"
|
||
done
|
||
echo -e " ${DIM}$(printf '─%.0s' {1..55})${NC}"
|
||
}
|
||
|
||
bot_edit_config() {
|
||
echo ""
|
||
echo -e " ${BOLD}${WHITE}$(t bot_settings_title)${NC}"
|
||
echo -e " ${DIM}$(printf '─%.0s' {1..55})${NC}"
|
||
|
||
if [ -f "$BOT_DIR/.env" ]; then
|
||
echo -e " ${DIM}$(t bot_current_env)${NC}"
|
||
while IFS= read -r line; do
|
||
# Mask token for security
|
||
if [[ "$line" == BOT_TOKEN=* ]]; then
|
||
local tok="${line#BOT_TOKEN=}"
|
||
echo -e " BOT_TOKEN=${tok:0:10}...${tok: -5}"
|
||
else
|
||
echo " $line"
|
||
fi
|
||
done < "$BOT_DIR/.env"
|
||
fi
|
||
|
||
echo ""
|
||
echo -e " ${CYAN}1${NC}) $(t bot_change_token)"
|
||
echo -e " ${CYAN}2${NC}) $(t bot_change_allowed)"
|
||
echo -e " ${CYAN}0${NC}) $(t back)"
|
||
echo -ne " ${WHITE}$(t choose):${NC} "
|
||
read -r ch
|
||
|
||
case "$ch" in
|
||
1)
|
||
echo -ne " ${WHITE}$(t bot_new_token)${NC} "
|
||
read -r new_token
|
||
new_token=$(echo "$new_token" | tr -d '[:space:]')
|
||
if [ -n "$new_token" ]; then
|
||
sed -i "s|^BOT_TOKEN=.*|BOT_TOKEN=$new_token|" "$BOT_DIR/.env"
|
||
systemctl restart "$BOT_SERVICE"
|
||
log_success "$(t bot_token_updated)"
|
||
else
|
||
log_error "$(t bot_token_empty_err)"
|
||
fi
|
||
;;
|
||
2)
|
||
echo -ne " ${WHITE}$(t bot_allowed_prompt)${NC} "
|
||
read -r new_ids
|
||
# Normalize: spaces and commas → commas, strip extras
|
||
new_ids=$(echo "$new_ids" | tr ' ' ',' | sed 's/,,*/,/g; s/^,//; s/,$//')
|
||
if grep -q "^ALLOWED_IDS=" "$BOT_DIR/.env" 2>/dev/null; then
|
||
if [ -n "$new_ids" ]; then
|
||
sed -i "s|^ALLOWED_IDS=.*|ALLOWED_IDS=$new_ids|" "$BOT_DIR/.env"
|
||
else
|
||
sed -i '/^ALLOWED_IDS=/d' "$BOT_DIR/.env"
|
||
fi
|
||
else
|
||
[ -n "$new_ids" ] && echo "ALLOWED_IDS=$new_ids" >> "$BOT_DIR/.env"
|
||
fi
|
||
systemctl restart "$BOT_SERVICE"
|
||
log_success "$(t bot_access_updated)"
|
||
;;
|
||
esac
|
||
}
|
||
|
||
bot_remove() {
|
||
echo ""
|
||
log_warning "$(t bot_remove_warn)"
|
||
if ! confirm "$(t bot_remove_confirm)"; then
|
||
return
|
||
fi
|
||
|
||
systemctl stop "$BOT_SERVICE" 2>/dev/null
|
||
systemctl disable "$BOT_SERVICE" 2>/dev/null
|
||
rm -f "/etc/systemd/system/${BOT_SERVICE}.service"
|
||
systemctl daemon-reload
|
||
rm -rf "$BOT_DIR"
|
||
log_success "$(t bot_removed)"
|
||
}
|
||
|
||
# ── Promo ────────────────────────────────────────────────────────────────────
|
||
_promo_block() {
|
||
# Print a promo section without width-fragile box borders (i18n safe)
|
||
local line2; line2=$(printf '─%.0s' {1..54})
|
||
echo ""
|
||
echo -e " ${DIM}${line2}${NC}"
|
||
echo -e " ${BOLD}${YELLOW}$(t promo_host1_title)${NC}"
|
||
echo -e " $(t promo_link_label) ${CYAN}https://vk.cc/ct29NQ${NC}"
|
||
echo -e " ${WHITE}OFF60${NC} — $(tf promo_off60)"
|
||
echo -e " ${WHITE}antenka20${NC} — $(tf promo_ant20)"
|
||
echo -e " ${WHITE}antenka6${NC} — $(tf promo_ant6)"
|
||
echo -e " ${DIM}${line2}${NC}"
|
||
echo -e " ${BOLD}${YELLOW}$(t promo_host2_title)${NC}"
|
||
echo -e " $(t promo_link_label) ${CYAN}https://vk.cc/cUxAhj${NC}"
|
||
echo -e " ${WHITE}OFF60${NC} — $(tf promo_off60)"
|
||
echo -e " ${DIM}${line2}${NC}"
|
||
echo -e " ${BOLD}${YELLOW}$(t promo_tips_title)${NC}"
|
||
echo -e " ${CYAN}https://pay.cloudtips.ru/p/7410814f${NC}"
|
||
echo -e " ${DIM}${line2}${NC}"
|
||
echo ""
|
||
}
|
||
|
||
menu_promo() {
|
||
_promo_block
|
||
}
|
||
|
||
# ── Проверка: показывать ли промо (раз в сутки) ────────────────────────────
|
||
should_show_promo() {
|
||
local stamp_file="$GOTELEGRAM_DIR/.promo_last_shown"
|
||
if [ ! -f "$stamp_file" ]; then
|
||
return 0 # никогда не показывали
|
||
fi
|
||
local last_shown now diff
|
||
last_shown=$(cat "$stamp_file" 2>/dev/null || echo "0")
|
||
last_shown="${last_shown//[^0-9]/}"
|
||
last_shown="${last_shown:-0}"
|
||
now=$(date +%s)
|
||
diff=$(( now - last_shown ))
|
||
# 86400 = 24 часа
|
||
[ "$diff" -ge 86400 ]
|
||
}
|
||
|
||
mark_promo_shown() {
|
||
mkdir -p "$GOTELEGRAM_DIR"
|
||
date +%s > "$GOTELEGRAM_DIR/.promo_last_shown"
|
||
}
|
||
|
||
# ── Promo with QR + delay (on install + once per day) ───────────────────
|
||
# QR показываем ТОЛЬКО для чаевых/донатов. Для хостеров оставлены только
|
||
# текстовые ссылки и промокоды (см. _promo_block) — QR-коды хостеров
|
||
# визуально конкурировали с чаевыми и перегружали экран.
|
||
show_promo_with_qr() {
|
||
local countdown="${1:-5}"
|
||
_promo_block
|
||
|
||
# QR только для чаевых
|
||
if command -v qrencode &>/dev/null; then
|
||
echo -e " ${DIM}$(t promo_qr_tips)${NC}"
|
||
qrencode -t UTF8 -m 1 "https://pay.cloudtips.ru/p/7410814f" 2>/dev/null | while IFS= read -r qr_line; do
|
||
echo " $qr_line"
|
||
done
|
||
fi
|
||
|
||
mark_promo_shown
|
||
|
||
# Countdown (default 5s, caller may pass longer for preflight abort)
|
||
local i
|
||
for ((i=countdown; i>0; i--)); do
|
||
echo -ne "\r ${DIM}$(tf promo_menu_in "$i")${NC} "
|
||
sleep 1
|
||
done
|
||
echo -ne "\r \r"
|
||
}
|
||
|
||
# ── First-run: pick language ─────────────────────────────────────────────────
|
||
first_run_language_picker() {
|
||
# Show picker only if language not yet saved
|
||
local marker="${GOTELEGRAM_DIR:-/opt/gotelegram}/.language"
|
||
local cfg_lang=""
|
||
if [ -f "$GOTELEGRAM_CONFIG" ] && command -v jq >/dev/null 2>&1; then
|
||
cfg_lang=$(jq -r '.language // empty' "$GOTELEGRAM_CONFIG" 2>/dev/null)
|
||
fi
|
||
if [ -f "$marker" ] || [ -n "$cfg_lang" ]; then
|
||
return 0
|
||
fi
|
||
|
||
local chosen
|
||
chosen=$(pick_language_interactive)
|
||
save_language "$chosen"
|
||
load_language "$chosen"
|
||
}
|
||
|
||
# ── Change language on demand ────────────────────────────────────────────────
|
||
menu_language() {
|
||
echo ""
|
||
echo -e " ${BOLD}${WHITE}$(t lang_change_prompt)${NC}"
|
||
echo -e " ${DIM}$(printf '─%.0s' {1..55})${NC}"
|
||
echo -e " ${CYAN}1${NC}) English"
|
||
echo -e " ${CYAN}2${NC}) Русский"
|
||
echo -e " ${CYAN}0${NC}) $(t back)"
|
||
echo -ne " ${WHITE}$(t choose):${NC} "
|
||
read -r ch
|
||
case "$ch" in
|
||
1) save_language "en"; load_language "en"; log_success "$(tf lang_saved English)" ;;
|
||
2) save_language "ru"; load_language "ru"; log_success "$(tf lang_saved Русский)" ;;
|
||
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.
|
||
# Uses `date -Iseconds` rather than jq's `now | todate` — the latter requires
|
||
# jq 1.6+ which is not available on Debian 10 or older CentOS.
|
||
bot_update_config_field() {
|
||
local key="$1"
|
||
local value="$2"
|
||
if [ ! -f "$GOTELEGRAM_CONFIG" ]; then
|
||
return 1
|
||
fi
|
||
local tmp now
|
||
tmp=$(mktemp) || return 1
|
||
now=$(date -Iseconds 2>/dev/null || date +%Y-%m-%dT%H:%M:%S%z)
|
||
if jq --arg k "$key" --arg v "$value" --arg t "$now" \
|
||
'.[$k] = $v | .updated_at = $t' \
|
||
"$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.
|
||
# Uses a file lock (flock) so concurrent CLI invocations (from multiple bot
|
||
# users, or from bot + manual CLI) serialize cleanly. Without this, two
|
||
# parallel `change-lite-domain` calls raced on the jq-rewrite of config.json
|
||
# and one process would see a truncated file ("no secret in config").
|
||
bot_action_dispatch() {
|
||
local lock_file="/var/lock/gotelegram-bot-action.lock"
|
||
# Make sure /var/lock exists (it does on Debian/Ubuntu; be defensive for minimal images)
|
||
[ -d /var/lock ] || mkdir -p /var/lock 2>/dev/null || true
|
||
|
||
if command -v flock >/dev/null 2>&1; then
|
||
# Wait up to 30 seconds for the lock — bot actions are fast (<5s
|
||
# typical), so 30s is plenty for legitimate serialization but short
|
||
# enough to surface a stuck process.
|
||
(
|
||
flock -w 30 9 || {
|
||
# If we time out, emit JSON error for the bot parent.
|
||
local json_out=0 a
|
||
for a in "$@"; do
|
||
[ "$a" = "--json" ] && json_out=1
|
||
done
|
||
if [ "$json_out" = "1" ]; then
|
||
bot_emit_json "error" "another action in progress (lock timeout)" "code=lock_timeout"
|
||
fi
|
||
exit 75 # EX_TEMPFAIL
|
||
}
|
||
_bot_action_dispatch_locked "$@"
|
||
) 9>"$lock_file"
|
||
return $?
|
||
else
|
||
# No flock installed — run unlocked with a warning. ensure_deps/check_deps
|
||
# normally ensures util-linux is present, so this branch is defensive.
|
||
log_warning "flock not available — bot actions not serialized"
|
||
_bot_action_dispatch_locked "$@"
|
||
return $?
|
||
fi
|
||
}
|
||
|
||
_bot_action_dispatch_locked() {
|
||
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
|
||
|
||
show_banner
|
||
|
||
# Pre-flight
|
||
check_os
|
||
check_disk_space 500
|
||
|
||
# Promo once per day
|
||
if should_show_promo; then
|
||
show_promo_with_qr
|
||
fi
|
||
|
||
while true; do
|
||
clear
|
||
show_main_menu
|
||
# Auto-refresh: 30 sec timeout
|
||
if read -t 30 -r choice; then
|
||
case "$choice" in
|
||
1) submenu_proxy ;;
|
||
2) submenu_stats ;;
|
||
3) submenu_manage ;;
|
||
4) menu_bot ;;
|
||
5) submenu_about ;;
|
||
0|q|exit) echo ""; log_info "$(t bye)"; exit 0 ;;
|
||
*) log_error "$(t invalid_choice)" ;;
|
||
esac
|
||
|
||
# Pause after submenu (except stats — it has its own loop)
|
||
if [ "$choice" != "2" ]; then
|
||
echo ""
|
||
echo -ne " ${DIM}$(t press_enter_to_return)${NC}"
|
||
read -r
|
||
fi
|
||
fi
|
||
# If read timed out, loop refreshes the dashboard
|
||
done
|
||
}
|
||
|
||
# ── Статистика (авто-обновление 1 сек, без мерцания) ───────────────────────
|
||
submenu_stats() {
|
||
# Инициализируем статистику при первом входе
|
||
if type stats_init &>/dev/null; then
|
||
stats_init 2>/dev/null
|
||
fi
|
||
|
||
local line2; line2=$(printf '─%.0s' {1..54})
|
||
local first_draw=1
|
||
|
||
# Скрываем курсор для плавного обновления
|
||
tput civis 2>/dev/null
|
||
|
||
# Восстанавливаем курсор при выходе из функции
|
||
trap 'tput cnorm 2>/dev/null; trap - RETURN' RETURN
|
||
|
||
while true; do
|
||
if [ "$first_draw" -eq 1 ]; then
|
||
clear
|
||
first_draw=0
|
||
else
|
||
# Перемещаем курсор в начало экрана вместо clear — нет мерцания
|
||
tput cup 0 0 2>/dev/null || printf '\033[H'
|
||
fi
|
||
|
||
# Draw the whole screen over the previous content
|
||
echo -e "\033[J" # erase from cursor to end (removes trails)
|
||
echo -e " ${BOLD}${WHITE}$(t stats_title)${NC}"
|
||
echo -e " ${DIM}${line2}${NC}"
|
||
|
||
if type show_traffic_stats &>/dev/null; then
|
||
show_traffic_stats
|
||
else
|
||
echo -e " ${DIM}$(t stats_module_missing)${NC}"
|
||
echo -e " ${DIM}$(t stats_file_missing)${NC}"
|
||
echo ""
|
||
fi
|
||
|
||
echo -e " ${DIM}${line2}${NC}"
|
||
local stats_on
|
||
stats_on=$(t stats_on)
|
||
if type toggle_stats &>/dev/null; then
|
||
local cfg_val
|
||
cfg_val=$(config_get stats_enabled 2>/dev/null || echo "true")
|
||
[ "$cfg_val" = "false" ] && stats_on=$(t stats_off)
|
||
fi
|
||
echo -e " ${CYAN}1${NC}) $(tf stats_toggle "$stats_on")"
|
||
echo -e " ${CYAN}2${NC}) $(t stats_install_collector)"
|
||
echo -e " ${CYAN}0${NC}) ${DIM}$(t back)${NC}"
|
||
echo -e " ${DIM}${line2}${NC}"
|
||
echo -e " ${DIM}$(t stats_auto_refresh)${NC}"
|
||
|
||
# Show cursor for input, then hide again
|
||
tput cnorm 2>/dev/null
|
||
echo -ne " ${WHITE}▸ ${NC}"
|
||
|
||
if read -t 3 -r ch; then
|
||
tput civis 2>/dev/null
|
||
case "$ch" in
|
||
1)
|
||
if type toggle_stats &>/dev/null; then
|
||
toggle_stats
|
||
echo -ne " ${DIM}$(t press_enter)${NC}"; read -r
|
||
first_draw=1 # full redraw after action
|
||
fi
|
||
;;
|
||
2)
|
||
if type install_stats_collector &>/dev/null; then
|
||
install_stats_collector
|
||
echo -ne " ${DIM}$(t press_enter)${NC}"; read -r
|
||
first_draw=1
|
||
fi
|
||
;;
|
||
0|"") return ;;
|
||
esac
|
||
fi
|
||
tput civis 2>/dev/null
|
||
done
|
||
}
|
||
|
||
main "$@"
|